From 604fcab54863f1e15401ec062c6d456a375b61d8 Mon Sep 17 00:00:00 2001 From: Guy Daich Date: Mon, 22 Jul 2024 19:56:56 -0500 Subject: [PATCH] [release/v1.1] cherry-pick main (#3935) * fix quickstart link in helm chart (#3793) Signed-off-by: Huabing Zhao * build(deps): bump golang.org/x/sys from 0.21.0 to 0.22.0 (#3780) Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.21.0 to 0.22.0. - [Commits](https://github.com/golang/sys/compare/v0.21.0...v0.22.0) --- updated-dependencies: - dependency-name: golang.org/x/sys dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: zirain Co-authored-by: Xunzhuo * build(deps): bump distroless/static from `e9ac71e` to `8dd8d3c` in /tools/docker/envoy-gateway (#3778) build(deps): bump distroless/static in /tools/docker/envoy-gateway Bumps distroless/static from `e9ac71e` to `8dd8d3c`. --- updated-dependencies: - dependency-name: distroless/static dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Xunzhuo * build(deps): bump fortio.org/log from 1.12.2 to 1.14.0 (#3782) Bumps [fortio.org/log](https://github.com/fortio/log) from 1.12.2 to 1.14.0. - [Release notes](https://github.com/fortio/log/releases) - [Commits](https://github.com/fortio/log/compare/v1.12.2...v1.14.0) --- updated-dependencies: - dependency-name: fortio.org/log dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: zirain Co-authored-by: Xunzhuo * build(deps): bump google.golang.org/grpc from 1.64.0 to 1.65.0 (#3783) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.64.0 to 1.65.0. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.64.0...v1.65.0) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * docs: move release-notes out of version (#3765) * move release-notes out of version Signed-off-by: zirain * fix Signed-off-by: zirain * update release-notes Signed-off-by: zirain --------- Signed-off-by: zirain * ci: update cherry-pick v1.1.0 (#3803) Signed-off-by: Guy Daich * doc: how to build a wasm image (#3806) * docs for building wasm images Signed-off-by: Huabing Zhao * address comments Signed-off-by: Huabing Zhao * minor change Signed-off-by: Huabing Zhao --------- Signed-off-by: Huabing Zhao * Use Wasm instead of WASM (#3812) * Use Wasm instead of WASM Signed-off-by: Takeshi Yoneda * part2 Signed-off-by: Takeshi Yoneda --------- Signed-off-by: Takeshi Yoneda * docs: generate v1.1.0-rc.1 release note (#3794) * chore: release-notes-docs be part of generate (#3815) * fix: enable client timeout test (#3811) * enable client timeout test Signed-off-by: Guy Daich * fix target of policy Signed-off-by: Guy Daich --------- Signed-off-by: Guy Daich * chore: add benchmark report into release artifacts (#3756) * add benchmark report save dir Signed-off-by: shawnh2 * add benchmark report to latest release Signed-off-by: shawnh2 * separate benchmark-test push and pull_request event Signed-off-by: shawnh2 * add benchmark report to release workflow Signed-off-by: shawnh2 * fix lint and update doc Signed-off-by: shawnh2 * move out resource limit unit Signed-off-by: shawnh2 --------- Signed-off-by: shawnh2 Co-authored-by: Xunzhuo * docs: fix grafana link (#3818) Signed-off-by: zirain * e2e: make sure ALS server is ready (#3816) Signed-off-by: zirain * Revert "docs: fix grafana link" (#3822) Revert "docs: fix grafana link (#3818)" This reverts commit 0af2f9f50df631f140db016a5846603548911a32. Signed-off-by: zirain * feat: support target selectors on Envoy Gateway Extension Server policies (#3800) * Support target selectors on Envoy Gateway Extension Server policies Signed-off-by: Lior Okman * Fixed the linter errors Signed-off-by: Lior Okman --------- Signed-off-by: Lior Okman * docs: updating the documentation for Extension Servers and adding an example extension server (#3788) * Updating the documentation for Extension Servers and adding an example extension server. Signed-off-by: Lior Okman * Make the docs linter happy Signed-off-by: Lior Okman * Add license headers to every new source file, and make the yaml linter ignore the extension-server chart Signed-off-by: Lior Okman * Add the boilerplate license for generated files. Signed-off-by: Lior Okman --------- Signed-off-by: Lior Okman * docs for ip allowlist/denylist (#3784) * docs for ip whitelisting/blacklisting Signed-off-by: Huabing Zhao * replace terms based on CNCF INI doc Signed-off-by: Huabing Zhao * address comments Signed-off-by: Huabing Zhao --------- Signed-off-by: Huabing Zhao Co-authored-by: Guy Daich * docs: gRPC Access Log Service (ALS) sink (#3768) * docs: gRPC Access Log Service (ALS) sink Signed-off-by: zirain * ignore githubusercontent.com Signed-off-by: zirain * update Signed-off-by: zirain --------- Signed-off-by: zirain Co-authored-by: Guy Daich * docs: update v1.1.0-rc.1 release notes (#3821) update v1.1.0-rc.1 release notes Signed-off-by: Guy Daich * docs: add task for wasm extensions (#3796) * docs for wasm extensions Signed-off-by: Huabing Zhao * fix lint Signed-off-by: Huabing Zhao * address comments Signed-off-by: Huabing Zhao * minor change Signed-off-by: Huabing Zhao --------- Signed-off-by: Huabing Zhao Co-authored-by: Guy Daich * community: promote shawnh2 to maintainer and move qicz to emeritus (#3760) Signed-off-by: bitliu * chore: report a translate error to errChan to make it observed correctly (#3827) Signed-off-by: Kensei Nakada * chore: upgrade to golang v1.22.5 (#3829) * chore: golang v1.22.5 Signed-off-by: Kensei Nakada * chore: update golang version in example manifest Signed-off-by: Kensei Nakada --------- Signed-off-by: Kensei Nakada Co-authored-by: zirain * chore: add `make lint.fix-golint` to address auto fixable lint issues (#3828) * chore: add make lint.fix to address auto fixable lint issues Signed-off-by: Kensei Nakada * chore: rename to lint.fix-golint Signed-off-by: Kensei Nakada * chore: golang v1.22.5 Signed-off-by: Kensei Nakada * fix: correct a mistake on the name Signed-off-by: Kensei Nakada --------- Signed-off-by: Kensei Nakada * docs: patch field within EnvoyService (#3820) * add docs for patching field within EnvoyService Signed-off-by: shawnh2 * update path service example Signed-off-by: shawnh2 --------- Signed-off-by: shawnh2 * accesslog: remove ALS gRPC initialMetadata (#3751) remove ALS gRPC initialMetadata Signed-off-by: zirain * docs: add fixed links to the current version of eg docs (#3819) * rename v1.0.2 to docs Signed-off-by: Huabing Zhao * retain v1.0.2 directory to avoid dead links Signed-off-by: Huabing Zhao * fix link Signed-off-by: Huabing Zhao * fix link Signed-off-by: Huabing Zhao * copy v1.0.2 to docs in make file Signed-off-by: Huabing Zhao * test auto copy Signed-off-by: Huabing Zhao * test auto copy Signed-off-by: Huabing Zhao * test auto copy Signed-off-by: Huabing Zhao * test auto copy Signed-off-by: Huabing Zhao * test auto copy Signed-off-by: Huabing Zhao * copy latest version docs to docs directory Signed-off-by: Huabing Zhao --------- Signed-off-by: Huabing Zhao Co-authored-by: zirain * fix: backendtls minversion (#3835) fix backendtls Signed-off-by: Guy Daich * fix: enable use-client-protocol test (#3825) * enable use-client-protocol test Signed-off-by: Guy Daich * add retries to basic auth tests Signed-off-by: Guy Daich --------- Signed-off-by: Guy Daich * fix: backendtls client cert (#3839) fix backendtls client cert Signed-off-by: Guy Daich * fix: prevent xdsIR updates from overwriting RateLimit configs from other xdsIR (#3771) * fix: prevent xdsIR updates from overwriting RateLimit configs from other xdsIR Signed-off-by: Kensei Nakada * fix: handle deletion events appropriately Signed-off-by: Kensei Nakada * test: add a unit test for subscribeAndTranslate Signed-off-by: Kensei Nakada * chore: sort import order Signed-off-by: Kensei Nakada --------- Signed-off-by: Kensei Nakada Co-authored-by: zirain * docs: use v[x.y] instead of v[x.y.z] (#3836) * docs: use vx.y instead of vx.y.z Signed-off-by: zirain * fix deadlink Signed-off-by: zirain --------- Signed-off-by: zirain * e2e: fix basic auth flaky (#3833) Signed-off-by: zirain Co-authored-by: Guy Daich * design: add wasm extension supports OCI image code source (#3313) * desing docs for wasm oci support Signed-off-by: huabing zhao * fix lint Signed-off-by: huabing zhao * Update site/content/en/contributions/design/wasm-extension.md Co-authored-by: Arko Dasgupta Signed-off-by: Huabing Zhao * minor wording Signed-off-by: huabing zhao * authn consideration Signed-off-by: huabing zhao * address comments Signed-off-by: huabing zhao * minor wording Signed-off-by: huabing zhao * minor wording Signed-off-by: huabing zhao * minor wording Signed-off-by: huabing zhao * minor wording Signed-off-by: huabing zhao * restrict access to priave images Signed-off-by: huabing zhao * minor change Signed-off-by: Huabing Zhao * move image to /img Signed-off-by: Huabing Zhao --------- Signed-off-by: huabing zhao Signed-off-by: Huabing Zhao Co-authored-by: Arko Dasgupta Co-authored-by: zirain Co-authored-by: Guy Daich * fix: enable upgrade test (#3764) adapt upgrade test to v1.1 Signed-off-by: Guy Daich Co-authored-by: zirain * chore: go mod tidy (#3842) Signed-off-by: zirain * fix flaky authorization tests (#3844) Signed-off-by: Huabing Zhao * build(deps): bump golang.org/x/net from 0.26.0 to 0.27.0 (#3849) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.26.0 to 0.27.0. - [Commits](https://github.com/golang/net/compare/v0.26.0...v0.27.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump fortio.org/fortio from 1.65.0 to 1.66.0 (#3848) Bumps [fortio.org/fortio](https://github.com/fortio/fortio) from 1.65.0 to 1.66.0. - [Release notes](https://github.com/fortio/fortio/releases) - [Commits](https://github.com/fortio/fortio/compare/v1.65.0...v1.66.0) --- updated-dependencies: - dependency-name: fortio.org/fortio dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump helm.sh/helm/v3 from 3.15.2 to 3.15.3 (#3850) Bumps [helm.sh/helm/v3](https://github.com/helm/helm) from 3.15.2 to 3.15.3. - [Release notes](https://github.com/helm/helm/releases) - [Commits](https://github.com/helm/helm/compare/v3.15.2...v3.15.3) --- updated-dependencies: - dependency-name: helm.sh/helm/v3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore: move UDP test resources out of the base (#3857) delay the creation for non-shared udp test resources Signed-off-by: Huabing Zhao * chore: replace targetRef with targetRefs in e2e (#3858) * docs: Remove the older versions from linkinator ignore list (#3846) * upgrade hugo and postcss-cli Signed-off-by: zirain * fix deadlink Signed-off-by: zirain * remove linkinator timeout Signed-off-by: zirain --------- Signed-off-by: zirain * build(deps): bump aquasecurity/trivy-action from 0.23.0 to 0.24.0 (#3854) Bumps [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action) from 0.23.0 to 0.24.0. - [Release notes](https://github.com/aquasecurity/trivy-action/releases) - [Commits](https://github.com/aquasecurity/trivy-action/compare/7c2007bcb556501da015201bcba5aa14069b74e2...6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8) --- updated-dependencies: - dependency-name: aquasecurity/trivy-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump github.com/norwoodj/helm-docs from 1.13.0 to 1.14.2 in /tools/src/helm-docs (#3847) build(deps): bump github.com/norwoodj/helm-docs in /tools/src/helm-docs Bumps [github.com/norwoodj/helm-docs](https://github.com/norwoodj/helm-docs) from 1.13.0 to 1.14.2. - [Release notes](https://github.com/norwoodj/helm-docs/releases) - [Changelog](https://github.com/norwoodj/helm-docs/blob/master/CHANGELOG.md) - [Commits](https://github.com/norwoodj/helm-docs/compare/v1.13.0...v1.14.2) --- updated-dependencies: - dependency-name: github.com/norwoodj/helm-docs dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore: move connection limit test resources out of the base (#3859) * delay the creation for non-shared test resources Signed-off-by: Huabing Zhao * fix lint Signed-off-by: Huabing Zhao --------- Signed-off-by: Huabing Zhao * build(deps): bump actions/setup-node from 4.0.2 to 4.0.3 (#3853) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.0.2 to 4.0.3. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/60edb5dd545a775178f52524783378180af0d1f8...1e60f620b9541d16bece96c5465dc8ee9832be0b) --- updated-dependencies: - dependency-name: actions/setup-node dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump google/osv-scanner-action from 1.8.1 to 1.8.2 (#3851) Bumps [google/osv-scanner-action](https://github.com/google/osv-scanner-action) from 1.8.1 to 1.8.2. - [Release notes](https://github.com/google/osv-scanner-action/releases) - [Commits](https://github.com/google/osv-scanner-action/compare/3c399db9dd6dd8106a27d280d53c55077d3f7cea...7ac94f9d40028db4cacf8d53adec6626f5d3d2f7) --- updated-dependencies: - dependency-name: google/osv-scanner-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: zirain * build(deps): bump actions/setup-go from 5.0.1 to 5.0.2 in /tools/github-actions/setup-deps (#3855) build(deps): bump actions/setup-go in /tools/github-actions/setup-deps Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5.0.1 to 5.0.2. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/cdcb36043654635271a94b9a6d1392de5bb323a7...0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32) --- updated-dependencies: - dependency-name: actions/setup-go dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump github/codeql-action from 3.25.11 to 3.25.12 (#3852) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.25.11 to 3.25.12. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/b611370bb5703a7efb587f9d136a52ea24c5c38c...4fa2a7953630fd2f3fb380f21be14ede0169dd4f) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: zirain * docs: add backend tls docs (#3843) * add backend tls docs Signed-off-by: Guy Daich * fix links Signed-off-by: Guy Daich * add gateway paramsref Signed-off-by: Guy Daich * nit Signed-off-by: Guy Daich --------- Signed-off-by: Guy Daich * chore: move zipkin test resources out of the base (#3864) move zipkin test resources out of the base Signed-off-by: Huabing Zhao * chore: move tcp test resources out of the base (#3863) move tcp test resources out of the base Signed-off-by: Huabing Zhao * docs: create concepts docs page and diagram (#3808) * Adding concept page with visual to docs site Signed-off-by: Erica Hughberg * Fix EnvoyGatewayPatchPolicy to EnvoyPatchPolicy Signed-off-by: Erica Hughberg * Fix ordering of columns and adding links. Signed-off-by: Erica Hughberg * Adding to v1.0.2 as well Signed-off-by: Erica Hughberg * Fixing links Signed-off-by: Erica Hughberg * Added Backend resource to concept overview Signed-off-by: Erica Hughberg * Tidy up and update docs for 1.0.2 Signed-off-by: Erica Hughberg * Update arrow from Route to route targets Signed-off-by: Erica Hughberg * fix: wrong path & title and add diagram Signed-off-by: bitliu --------- Signed-off-by: Erica Hughberg Signed-off-by: bitliu Co-authored-by: Xunzhuo * benchmark: enable prometheus to scrape metrics from (#3772) * chore: move backend tls test resources out of the base (#3862) * move backend tls test resources out of the base Signed-off-by: Huabing Zhao * fix lint Signed-off-by: Huabing Zhao * fix lint Signed-off-by: Huabing Zhao * add notice Signed-off-by: Huabing Zhao * fix test Signed-off-by: Huabing Zhao * fix test Signed-off-by: Huabing Zhao * print response body for debugging Signed-off-by: Huabing Zhao * print policy for debugging Signed-off-by: Huabing Zhao * increase timeout Signed-off-by: Huabing Zhao --------- Signed-off-by: Huabing Zhao * chore: remove cherrypicker action (#3831) Signed-off-by: zirain * chore: update linkinator comment (#3870) Signed-off-by: zirain * chore: make format as part of gen-check (#3877) Signed-off-by: zirain * chore: update LINKINATOR_IGNORE (#3879) * chore: update LINKINATOR_IGNORE Signed-off-by: zirain * remove example.com Signed-off-by: zirain * Revert "remove example.com" This reverts commit 0c6e44cdfb70bbd32efe469ad748580895d400b1. Signed-off-by: zirain --------- Signed-off-by: zirain * return 500 error for failed SecurityPolicies to avoid unauthorized access to xRoutes (#3869) * return 500 error to avoid unauthorized access to xRoutes Signed-off-by: Huabing Zhao * address comments Signed-off-by: Huabing Zhao * address comments Signed-off-by: Huabing Zhao * add e2e test for failed SecurityPolicy Signed-off-by: Huabing Zhao * add e2e test for failed SecurityPolicy Signed-off-by: Huabing Zhao * add e2e test for failed SecurityPolicy Signed-off-by: Huabing Zhao * add e2e test for failed SecurityPolicy Signed-off-by: Huabing Zhao * fix lint Signed-off-by: Huabing Zhao * fix lint Signed-off-by: Huabing Zhao * rename Signed-off-by: Huabing Zhao * rename Signed-off-by: Huabing Zhao --------- Signed-off-by: Huabing Zhao Co-authored-by: Xunzhuo * lint: update yamllint and codespell skip (#3882) * lint: update yamllint and codespell skip Signed-off-by: zirain * end with newline Signed-off-by: zirain --------- Signed-off-by: zirain * e2e: increase test timeout (#3883) Signed-off-by: zirain * chore: client mtls test (#3874) client mtls test Signed-off-by: Guy Daich * fix: nil pointer err during hash load balancing build (#3886) fix nil pointer err in buildHashPolicy Signed-off-by: shawnh2 * fix override issue for EEP (#3881) * add test for empty policies Signed-off-by: Huabing Zhao * add test for eep Signed-off-by: Huabing Zhao * add teset for eep Signed-off-by: Huabing Zhao * fix eep override issue Signed-off-by: Huabing Zhao --------- Signed-off-by: Huabing Zhao * accesslog: fix different CelMatches on AccessLog (#3885) * accesslog: fix different CelMatches on AccessLog Signed-off-by: zirain * lint Signed-off-by: zirain * text Signed-off-by: zirain * lint Signed-off-by: zirain --------- Signed-off-by: zirain Co-authored-by: Guy Daich * rm gateway-api translation error message from direct response (#3878) * Responding back with an error message around translation errors may leak info to internet facing external clients around ingress internals Signed-off-by: Arko Dasgupta Co-authored-by: zirain Co-authored-by: Xunzhuo * GetParentReferences should use namespace from RouteContext (#3876) * GetParentReferences should use namespace from RouteContext Signed-off-by: zirain * lint Signed-off-by: zirain * add test Signed-off-by: zirain * fix test Signed-off-by: zirain * update Signed-off-by: zirain * add negative case Signed-off-by: zirain * address review comment Signed-off-by: zirain --------- Signed-off-by: zirain * Add e2e test for load balancing (#3868) * update api doc and e2e test env for lb Signed-off-by: shawnh2 * add e2e test for round robin lb Signed-off-by: shawnh2 * fix gen-check Signed-off-by: shawnh2 * add e2e test for source ip consistent hash lb Signed-off-by: shawnh2 * add e2e test for header consistent hash lb Signed-off-by: shawnh2 * add e2e test for cookie consistent hash lb Signed-off-by: shawnh2 * add deployment only for lb test case Signed-off-by: shawnh2 * rename deployment for different test setup Signed-off-by: shawnh2 * wait deployment to have available replicas Signed-off-by: shawnh2 --------- Signed-off-by: shawnh2 Co-authored-by: zirain * egctl: introduce `egctl x collect` (#3775) * e2e: add e2e test for cookie based consistent hash load balancing (#3890) * add e2e test for cookie based consistent hash load balancing Signed-off-by: shawnh2 * fix lint Signed-off-by: shawnh2 * lower the round robin lb test boundary Signed-off-by: shawnh2 * add case for generated cookie Signed-off-by: shawnh2 * fix lint Signed-off-by: shawnh2 * remove response dump Signed-off-by: shawnh2 --------- Signed-off-by: shawnh2 * enable HTTPRouteBackendRequestHeaderModifier test (#3891) * enable HTTPRouteBackendRequestHeaderModifier test already supported with https://github.com/envoyproxy/gateway/pull/3246 Signed-off-by: Arko Dasgupta * make testdata Signed-off-by: Arko Dasgupta --------- Signed-off-by: Arko Dasgupta * disable writing into GatewayClass.Status.SupportedFeatures disable until the field moves from experiemental to stable so status writes for a GatewayClass dont fail when the datatypes differ Signed-off-by: Arko Dasgupta * comment out test snippet Signed-off-by: Arko Dasgupta * validate for reconcile should check reference from EnvoyProxy (#3895) validateEndpointSliceForReconcile should check reference from EnvoyProxy Signed-off-by: zirain * chore: add grafonnet dashboards support (#3785) * add grafonnet lib and support for resources monitor dashboard Signed-off-by: shawnh2 * update helm-generate to support grafonnet generate dashboards Signed-off-by: shawnh2 * update doc Signed-off-by: shawnh2 * fix doc-lint and osv-scan Signed-off-by: shawnh2 * fix tools path Signed-off-by: shawnh2 * resolve conflicts Signed-off-by: shawnh2 * fix gen-check Signed-off-by: shawnh2 * fix doc-lint Signed-off-by: shawnh2 --------- Signed-off-by: shawnh2 Co-authored-by: Xunzhuo * add startupProbe to all provisioned containers (#3893) * This ensures the readinessProbe kicks in only after the container has started * max startup time is 300s - 30 (failureThreshold) x 10 (periodSeconds). After this the container is killed and the `restartPolicy` kicks in https://kubernetes.io/docs/concepts/configuration/liveness-readiness-startup-probes/#startup-probe Fixes: https://github.com/envoyproxy/gateway/issues/3511 Signed-off-by: Arko Dasgupta * e2e: move als test resources out of the base (#3884) Signed-off-by: zirain * e2e: fix ZipkinTracing flaky (#3899) * e2e: make sure OTel-collector is ready Signed-off-by: zirain * fix gen Signed-off-by: zirain * fix retry Signed-off-by: zirain * remove infrastructure.parametersRef from all-namespace Signed-off-by: zirain * update Signed-off-by: zirain * update Signed-off-by: zirain * lint Signed-off-by: zirain * fix bad request Signed-off-by: zirain * increase time of one cycle Signed-off-by: zirain --------- Signed-off-by: zirain * doc: add load balancing usage (#3903) add load balancing usage Signed-off-by: shawnh2 Co-authored-by: Xunzhuo * fix: typos in release notes (#3909) Signed-off-by: bitliu * fix: fix the CEL definitions to allow policies that use target selectors without explicit targetRefs (#3904) Fix the CEL definitions to allow policies that use target selectors without explicit targetRefs Signed-off-by: Lior Okman * feat(logger): Add tlog for better test logging (#3913) Add tlog.Logf() logger Signed-off-by: Manoramsharma * e2e: add hook to debug OIDC fail (#3914) * e2e: refactor and improve lb test (#3912) * e2e: refactor and improve lb test Signed-off-by: zirain * tlog Signed-off-by: zirain * update Signed-off-by: zirain * lint Signed-off-by: zirain * nit Signed-off-by: zirain --------- Signed-off-by: zirain * tools: remove sphinx (#3927) Signed-off-by: zirain * release v1.1.0 (#3932) * release v1.1.0 Signed-off-by: Guy Daich * update release notes with delta from v1.1.0-rc.1 Signed-off-by: Guy Daich * fix lint Signed-off-by: Guy Daich --------- Signed-off-by: Guy Daich --------- Signed-off-by: Huabing Zhao Signed-off-by: dependabot[bot] Signed-off-by: zirain Signed-off-by: Guy Daich Signed-off-by: Takeshi Yoneda Signed-off-by: shawnh2 Signed-off-by: Lior Okman Signed-off-by: bitliu Signed-off-by: Kensei Nakada Signed-off-by: huabing zhao Signed-off-by: Erica Hughberg Signed-off-by: Arko Dasgupta Signed-off-by: Manoramsharma Co-authored-by: Huabing Zhao Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: zirain Co-authored-by: Xunzhuo Co-authored-by: Takeshi Yoneda Co-authored-by: sh2 Co-authored-by: Lior Okman Co-authored-by: Kensei Nakada Co-authored-by: Arko Dasgupta Co-authored-by: Erica Hughberg Co-authored-by: Arko Dasgupta Co-authored-by: Manoramsharma <84619980+Manoramsharma@users.noreply.github.com> --- .github/workflows/build_and_test.yaml | 11 +- .github/workflows/cherrypick.yaml | 34 - .github/workflows/codeql.yml | 6 +- .github/workflows/docs.yaml | 2 +- .github/workflows/latest_release.yaml | 39 + .github/workflows/license-scan.yml | 2 +- .github/workflows/osv-scanner.yml | 19 +- .github/workflows/release.yaml | 39 + .github/workflows/scorecard.yml | 2 +- .github/workflows/trivy.yml | 2 +- .gitignore | 5 + OWNERS | 12 +- VERSION | 2 +- api/v1alpha1/accesslogging_types.go | 2 +- api/v1alpha1/backendtrafficpolicy_types.go | 2 +- api/v1alpha1/clienttrafficpolicy_types.go | 2 +- api/v1alpha1/envoyextensionypolicy_types.go | 2 +- api/v1alpha1/loadbalancer_types.go | 5 +- api/v1alpha1/securitypolicy_types.go | 2 +- charts/gateway-addons-helm/.helmignore | 5 + charts/gateway-addons-helm/README.md | 1 + .../dashboards/envoy-gateway-resource.json | 262 -- .../dashboards/envoy-pod-resource.json | 255 -- ...oy-global.json => envoy-proxy-global.json} | 0 .../dashboards/jsonnetfile.json | 15 + .../dashboards/jsonnetfile.lock.json | 46 + .../dashboards/lib/g.libsonnet | 1 + .../dashboards/lib/panels.libsonnet | 83 + .../dashboards/lib/queries.libsonnet | 73 + .../dashboards/lib/variables.libsonnet | 7 + .../dashboards/resources-monitor.gen.json | 249 ++ .../dashboards/resources-monitor.libsonnet | 31 + charts/gateway-addons-helm/values.yaml | 2 + ....envoyproxy.io_backendtrafficpolicies.yaml | 12 +- ...y.envoyproxy.io_clienttrafficpolicies.yaml | 5 +- ....envoyproxy.io_envoyextensionpolicies.yaml | 5 +- ...ateway.envoyproxy.io_securitypolicies.yaml | 5 +- charts/gateway-helm/templates/NOTES.txt | 2 +- examples/extension-server/.gitignore | 1 + examples/extension-server/Makefile | 30 + examples/extension-server/api/doc.go | 6 + examples/extension-server/api/v1alpha1/doc.go | 8 + .../api/v1alpha1/groupversion_info.go | 25 + .../api/v1alpha1/listenercontext_types.go | 42 + .../api/v1alpha1/zz_generated.deepcopy.go | 95 + .../charts/extension-server/.helmignore | 23 + .../charts/extension-server/Chart.yaml | 24 + ...extensions.io_listenercontextexamples.yaml | 112 + .../extension-server/templates/NOTES.txt | 1 + .../extension-server/templates/_helpers.tpl | 62 + .../templates/deployment.yaml | 51 + .../extension-server/templates/service.yaml | 15 + .../templates/serviceaccount.yaml | 12 + .../charts/extension-server/values.yaml | 59 + .../cmd/extension-server/main.go | 96 + examples/extension-server/go.mod | 45 + examples/extension-server/go.sum | 138 + .../internal/extensionserver/htpasswd.go | 38 + .../internal/extensionserver/server.go | 150 + .../tools/boilerplate.generatego.txt | 5 + .../tools/docker/extension-server/Dockerfile | 6 + .../tools/src/controller-gen/go.mod | 38 + .../tools/src/controller-gen/go.sum | 136 + .../tools/src/controller-gen/pin.go | 11 + examples/kubernetes/envoy-als.yaml | 231 + .../kubernetes/ext-proc-grpc-service.yaml | 2 +- go.mod | 113 +- go.sum | 1025 ++++- internal/cmd/egctl/collect.go | 103 + internal/cmd/egctl/experimental.go | 1 + .../translate/out/default-resources.all.yaml | 23 - .../out/echo-gateway-api.cluster.yaml | 23 - .../translate/out/echo-gateway-api.route.json | 24 - .../translate/out/invalid-envoyproxy.all.yaml | 23 - .../out/rejected-http-route.route.yaml | 23 - .../translate/out/valid-envoyproxy.all.yaml | 23 - internal/cmd/egctl/translate_test.go | 8 +- internal/gatewayapi/backendtlspolicy.go | 2 +- internal/gatewayapi/conformance/suite.go | 3 +- internal/gatewayapi/envoyextensionpolicy.go | 21 +- internal/gatewayapi/ext_service.go | 2 +- internal/gatewayapi/extensionserverpolicy.go | 48 +- .../gatewayapi/extensionserverpolicy_test.go | 54 +- internal/gatewayapi/filters.go | 3 - internal/gatewayapi/helpers.go | 18 +- internal/gatewayapi/helpers_test.go | 69 + internal/gatewayapi/listener.go | 47 +- internal/gatewayapi/route.go | 4 +- internal/gatewayapi/securitypolicy.go | 15 +- internal/gatewayapi/status/gatewayclass.go | 5 +- .../backend-invalid-feature-disabled.out.yaml | 1 + ...kendtrafficpolicy-override-replace.in.yaml | 29 + ...endtrafficpolicy-override-replace.out.yaml | 89 +- .../testdata/custom-filter-order.out.yaml | 47 +- ...oyextensionpolicy-override-replace.in.yaml | 29 + ...yextensionpolicy-override-replace.out.yaml | 123 +- ...extensionpolicy-status-conditions.out.yaml | 2 + ...ith-extproc-with-backendtlspolicy.out.yaml | 80 +- ...extproc-with-multiple-backendrefs.out.yaml | 75 +- ...ensionpolicy-with-wasm-targetrefs.out.yaml | 94 +- .../envoyextensionpolicy-with-wasm.out.yaml | 94 +- .../testdata/envoyproxy-accesslog-cel.in.yaml | 41 + .../envoyproxy-accesslog-cel.out.yaml | 27 +- ...voyproxy-accesslog-without-format.out.yaml | 3 +- ...route-with-multi-gateways-notmatch.in.yaml | 45 + ...oute-with-multi-gateways-notmatch.out.yaml | 151 + ...with-multi-gateways-with-same-name.in.yaml | 45 + ...ith-multi-gateways-with-same-name.out.yaml | 205 + .../securitypolicy-override-replace.in.yaml | 29 + .../securitypolicy-override-replace.out.yaml | 89 +- ...-extauth-invalid-no-matching-port.out.yaml | 2 + ...licy-with-extauth-invalid-no-port.out.yaml | 2 + ...xtauth-invalid-no-reference-grant.out.yaml | 2 + ...y-with-extauth-invalid-no-service.out.yaml | 2 + ...ypolicy-with-jwt-and-invalid-oidc.out.yaml | 4 + internal/globalratelimit/runner/runner.go | 26 +- .../globalratelimit/runner/runner_test.go | 244 ++ .../kubernetes/proxy/resource.go | 26 + .../testdata/daemonsets/component-level.yaml | 18 + .../proxy/testdata/daemonsets/custom.yaml | 18 + .../testdata/daemonsets/default-env.yaml | 18 + .../proxy/testdata/daemonsets/default.yaml | 18 + .../daemonsets/disable-prometheus.yaml | 18 + .../testdata/daemonsets/extension-env.yaml | 18 + .../override-labels-and-annotations.yaml | 18 + .../testdata/daemonsets/patch-daemonset.yaml | 18 + .../testdata/daemonsets/shutdown-manager.yaml | 18 + .../proxy/testdata/daemonsets/volumes.yaml | 18 + .../testdata/daemonsets/with-annotations.yaml | 18 + .../testdata/daemonsets/with-concurrency.yaml | 18 + .../testdata/daemonsets/with-extra-args.yaml | 18 + .../daemonsets/with-image-pull-secrets.yaml | 18 + .../proxy/testdata/daemonsets/with-name.yaml | 18 + .../daemonsets/with-node-selector.yaml | 18 + .../with-topology-spread-constraints.yaml | 18 + .../proxy/testdata/deployments/bootstrap.yaml | 18 + .../testdata/deployments/component-level.yaml | 18 + .../proxy/testdata/deployments/custom.yaml | 18 + .../custom_with_initcontainers.yaml | 18 + .../testdata/deployments/default-env.yaml | 18 + .../proxy/testdata/deployments/default.yaml | 18 + .../deployments/disable-prometheus.yaml | 18 + .../testdata/deployments/extension-env.yaml | 18 + .../override-labels-and-annotations.yaml | 18 + .../deployments/patch-deployment.yaml | 18 + .../deployments/shutdown-manager.yaml | 18 + .../proxy/testdata/deployments/volumes.yaml | 18 + .../deployments/with-annotations.yaml | 18 + .../deployments/with-concurrency.yaml | 18 + .../deployments/with-empty-memory-limits.yaml | 18 + .../testdata/deployments/with-extra-args.yaml | 18 + .../deployments/with-image-pull-secrets.yaml | 18 + .../proxy/testdata/deployments/with-name.yaml | 18 + .../deployments/with-node-selector.yaml | 18 + .../with-topology-spread-constraints.yaml | 18 + .../kubernetes/ratelimit/resource.go | 13 + .../testdata/deployments/custom.yaml | 9 + .../testdata/deployments/default-env.yaml | 9 + .../testdata/deployments/default.yaml | 9 + .../deployments/disable-prometheus.yaml | 9 + .../deployments/enable-tracing-custom.yaml | 9 + .../testdata/deployments/enable-tracing.yaml | 9 + .../testdata/deployments/extension-env.yaml | 9 + .../testdata/deployments/override-env.yaml | 9 + .../deployments/patch-deployment.yaml | 9 + .../deployments/redis-tls-settings.yaml | 9 + .../testdata/deployments/tolerations.yaml | 9 + .../testdata/deployments/volumes.yaml | 9 + .../deployments/with-node-selector.yaml | 9 + .../with-topology-spread-constraints.yaml | 9 + internal/ir/xds.go | 32 +- internal/ir/xds_test.go | 2 - internal/ir/zz_generated.deepcopy.go | 80 +- internal/kubernetes/client.go | 17 +- internal/provider/kubernetes/predicates.go | 8 + internal/troubleshoot/collect.go | 92 + internal/troubleshoot/collect/config_dump.go | 100 + .../troubleshoot/collect/custom_resource.go | 93 + .../collect/envoy_gateway_resource.go | 103 + .../collect/prometheus_metrics.go | 154 + .../collect/troubleshoot_helper.go | 611 +++ internal/utils/helm/package.go | 5 + internal/utils/helm/package_test.go | 106 + internal/xds/translator/accesslog.go | 61 +- internal/xds/translator/extproc.go | 13 +- internal/xds/translator/route.go | 19 +- .../testdata/in/xds-ir/accesslog-cel.yaml | 8 +- .../in/xds-ir/accesslog-multi-cel.yaml | 13 +- .../in/xds-ir/custom-filter-order.yaml | 43 +- .../testdata/in/xds-ir/ext-proc.yaml | 84 +- .../in/xds-ir/http-route-with-clientcert.yaml | 40 + .../testdata/in/xds-ir/load-balancer.yaml | 15 + .../translator/testdata/in/xds-ir/wasm.yaml | 94 +- .../out/xds-ir/accesslog-cel.routes.yaml | 2 - .../accesslog-endpoint-stats.routes.yaml | 2 - .../xds-ir/accesslog-formatters.routes.yaml | 2 - .../xds-ir/accesslog-multi-cel.routes.yaml | 2 - .../accesslog-without-format.routes.yaml | 2 - .../out/xds-ir/accesslog.listeners.yaml | 17 - .../testdata/out/xds-ir/accesslog.routes.yaml | 2 - .../http-route-direct-response.routes.yaml | 2 - .../http-route-with-clientcert.clusters.yaml | 43 + .../http-route-with-clientcert.endpoints.yaml | 16 + .../http-route-with-clientcert.listeners.yaml | 35 + .../http-route-with-clientcert.routes.yaml | 14 + .../http-route-with-clientcert.secrets.yaml | 10 + .../out/xds-ir/load-balancer.clusters.yaml | 17 + .../out/xds-ir/load-balancer.endpoints.yaml | 12 + .../out/xds-ir/load-balancer.routes.yaml | 13 + .../xds-ir/tracing-endpoint-stats.routes.yaml | 2 - .../out/xds-ir/tracing-zipkin.routes.yaml | 2 - .../testdata/out/xds-ir/tracing.routes.yaml | 2 - internal/xds/translator/translator.go | 21 +- internal/xds/translator/wasm.go | 9 +- release-notes/v0.2.0-rc1.yaml | 6 +- release-notes/v0.2.0-rc2.yaml | 6 +- release-notes/v0.2.0.yaml | 2 +- release-notes/v0.3.0-rc.1.yaml | 2 +- release-notes/v0.3.0.yaml | 2 +- release-notes/v0.4.0-rc.1.yaml | 2 +- release-notes/v0.4.0.yaml | 2 +- release-notes/v0.5.0-rc.1.yaml | 2 +- release-notes/v0.5.0.yaml | 2 +- release-notes/v0.6.0-rc.1.yaml | 2 +- release-notes/v1.0.0.yaml | 2 +- release-notes/v1.1.0-rc.1.yaml | 16 +- release-notes/v1.1.0.yaml | 252 ++ site/content/en/_index.md | 2 +- site/content/en/contributions/DEVELOP.md | 15 +- site/content/en/contributions/RELEASING.md | 32 - .../en/contributions/design/config-api.md | 2 +- .../design/envoy-extension-policy.md | 8 +- .../design/extending-envoy-gateway.md | 37 +- .../en/contributions/design/wasm-extension.md | 84 + site/content/en/{v1.0.2 => docs}/_index.md | 0 .../content/en/{v0.3.0 => docs}/api/_index.md | 0 site/content/en/docs/api/extension_types.md | 3861 +++++++++++++++++ site/content/en/docs/boilerplates/index.md | 5 + .../docs/boilerplates/o11y_prerequisites.md | 14 + site/content/en/docs/concepts/_index.md | 5 + .../en/docs/concepts/concepts_overview.md | 54 + .../en/{v0.5.0 => docs}/install/_index.md | 0 site/content/en/docs/install/custom-cert.md | 146 + .../docs/install/gateway-addons-helm-api.md | 123 + .../en/docs/install/gateway-helm-api.md | 66 + .../{v1.0.2 => docs}/install/install-egctl.md | 0 site/content/en/docs/install/install-helm.md | 144 + site/content/en/docs/install/install-yaml.md | 39 + .../en/{v1.0.2 => docs}/tasks/_index.md | 0 .../en/docs/tasks/extensibility/_index.md | 5 + .../tasks/extensibility/build-wasm-image.md | 71 + .../tasks/extensibility/envoy-patch-policy.md | 359 ++ .../en/docs/tasks/extensibility/ext-proc.md | 290 ++ .../tasks/extensibility/extension-server.md | 209 + .../en/docs/tasks/extensibility/wasm.md | 194 + .../en/docs/tasks/observability/_index.md | 5 + .../observability/gateway-api-metrics.md | 0 .../observability/gateway-exported-metrics.md | 97 + .../observability/gateway-observability.md | 176 + .../observability/grafana-integration.md | 87 + .../tasks/observability/proxy-accesslog.md | 251 ++ .../docs/tasks/observability/proxy-metric.md | 47 + .../docs/tasks/observability/proxy-trace.md | 233 + .../observability/rate-limit-observability.md | 98 + .../en/docs/tasks/operations/_index.md | 5 + .../tasks/operations/customize-envoyproxy.md | 955 ++++ .../docs/tasks/operations/deployment-mode.md | 1072 +++++ .../content/en/docs/tasks/operations/egctl.md | 908 ++++ site/content/en/docs/tasks/quickstart.md | 156 + site/content/en/docs/tasks/security/_index.md | 5 + .../en/docs/tasks/security/backend-mtls.md | 200 + .../en/docs/tasks/security/backend-tls.md | 390 ++ .../en/docs/tasks/security/basic-auth.md | 221 + site/content/en/docs/tasks/security/cors.md | 177 + .../en/docs/tasks/security/ext-auth.md | 460 ++ .../docs/tasks/security/jwt-authentication.md | 170 + .../en/docs/tasks/security/mutual-tls.md | 186 + site/content/en/docs/tasks/security/oidc.md | 322 ++ .../tasks/security/private-key-provider.md | 621 +++ .../docs/tasks/security/restrict-ip-access.md | 197 + .../en/docs/tasks/security/secure-gateways.md | 520 +++ .../en/docs/tasks/security/threat-model.md | 665 +++ .../docs/tasks/security/tls-cert-manager.md | 436 ++ .../en/docs/tasks/security/tls-passthrough.md | 124 + .../en/docs/tasks/security/tls-termination.md | 91 + .../{v1.0.2 => docs}/tasks/traffic/_index.md | 0 site/content/en/docs/tasks/traffic/backend.md | 211 + .../en/docs/tasks/traffic/circuit-breaker.md | 150 + .../tasks/traffic/client-traffic-policy.md | 688 +++ .../en/docs/tasks/traffic/connection-limit.md | 137 + .../en/docs/tasks/traffic/fault-injection.md | 381 ++ .../tasks/traffic/gateway-address.md | 0 .../docs/tasks/traffic/gatewayapi-support.md | 120 + .../docs/tasks/traffic/global-rate-limit.md | 1312 ++++++ .../en/docs/tasks/traffic/grpc-routing.md | 272 ++ .../tasks/traffic/http-redirect.md | 0 .../tasks/traffic/http-request-headers.md | 449 ++ .../tasks/traffic/http-request-mirroring.md | 447 ++ .../tasks/traffic/http-response-headers.md | 446 ++ .../en/docs/tasks/traffic/http-routing.md | 302 ++ .../tasks/traffic/http-timeouts.md | 0 .../tasks/traffic/http-traffic-splitting.md | 527 +++ .../en/docs/tasks/traffic/http-urlrewrite.md | 405 ++ site/content/en/docs/tasks/traffic/http3.md | 136 + .../en/docs/tasks/traffic/load-balancing.md | 922 ++++ .../en/docs/tasks/traffic/local-rate-limit.md | 414 ++ .../tasks/traffic/multicluster-service.md | 86 + site/content/en/docs/tasks/traffic/retry.md | 147 + .../traffic/routing-outside-kubernetes.md | 168 + .../en/docs/tasks/traffic/tcp-routing.md | 483 +++ .../en/docs/tasks/traffic/udp-routing.md | 170 + site/content/en/latest/api/extension_types.md | 2 +- site/content/en/latest/concepts/_index.md | 5 + .../en/latest/concepts/concepts_overview.md | 54 + .../latest/install/gateway-addons-helm-api.md | 1 + site/content/en/latest/releases/_index.md | 5 - site/content/en/latest/releases/v0.2.0-rc1.md | 37 - site/content/en/latest/releases/v0.2.0-rc2.md | 34 - .../content/en/latest/releases/v0.3.0-rc.1.md | 64 - site/content/en/latest/releases/v0.4.0.md | 59 - site/content/en/latest/releases/v0.5.0.md | 71 - site/content/en/latest/releases/v0.6.0.md | 70 - .../tasks/extensibility/build-wasm-image.md | 71 + .../tasks/extensibility/extension-server.md | 209 + .../en/latest/tasks/extensibility/wasm.md | 194 + .../observability/grafana-integration.md | 43 +- .../tasks/observability/proxy-accesslog.md | 53 +- .../tasks/operations/customize-envoyproxy.md | 65 +- site/content/en/latest/tasks/quickstart.md | 2 + .../en/latest/tasks/security/backend-mtls.md | 200 + .../en/latest/tasks/security/backend-tls.md | 118 +- .../tasks/security/restrict-ip-access.md | 197 + .../en/latest/tasks/security/threat-model.md | 2 +- .../en/latest/tasks/traffic/load-balancing.md | 922 ++++ .../en/news/blogs/1.0-release/1.0-release.md | 16 +- site/content/en/news/releases/matrix.md | 1 + site/content/en/news/releases/notes/_index.md | 5 + .../releases/notes}/v0.1.0.md | 1 + .../releases/notes}/v0.2.0-rc1.md | 9 +- .../releases/notes}/v0.2.0-rc2.md | 9 +- .../releases/notes}/v0.2.0.md | 13 +- .../releases/notes}/v0.3.0-rc.1.md | 3 +- .../releases/notes}/v0.3.0.md | 3 +- .../releases/notes}/v0.4.0-rc.1.md | 3 +- .../releases/notes}/v0.4.0.md | 3 +- .../releases/notes}/v0.5.0-rc.1.md | 6 +- .../releases/notes/v0.5.0.md} | 8 +- .../releases/notes}/v0.6.0-rc.1.md | 14 +- .../releases/notes}/v0.6.0.md | 16 +- .../releases/notes/v1.0.0-rc.1.md} | 55 +- .../releases/notes}/v1.0.0.md | 0 .../releases/notes}/v1.0.1.md | 0 .../releases/notes}/v1.0.2.md | 0 .../en/news/releases/notes/v1.1.0-rc.1.md | 225 + site/content/en/news/releases/notes/v1.1.0.md | 238 + site/content/en/news/releases/v0.2.md | 4 +- site/content/en/news/releases/v0.3.md | 4 +- site/content/en/news/releases/v0.4.md | 4 +- site/content/en/news/releases/v0.5.md | 4 +- site/content/en/news/releases/v0.6.md | 4 +- site/content/en/news/releases/v1.0.md | 4 +- site/content/en/news/releases/v1.1.md | 270 ++ site/content/en/{v0.2.0 => v0.2}/_index.md | 0 .../contributions/CODEOWNERS.md | 0 .../contributions/CODE_OF_CONDUCT.md | 0 .../contributions/CONTRIBUTING.md | 4 +- site/content/en/v0.2/contributions/DEVELOP.md | 162 + .../en/{v0.2.0 => v0.2}/contributions/DOCS.md | 0 .../contributions/RELEASING.md | 2 +- .../{v0.2.0 => v0.2}/contributions/_index.md | 0 .../{v0.2.0 => v0.2}/contributions/roadmap.md | 0 .../en/{v0.2.0 => v0.2}/design/_index.md | 0 .../en/{v0.3.0 => v0.2}/design/config-api.md | 4 +- .../design/gatewayapi-translator.md | 0 .../en/{v0.2.0 => v0.2}/design/goals.md | 0 .../{v0.3.0 => v0.2}/design/system-design.md | 10 +- .../en/{v0.2.0 => v0.2}/design/watching.md | 0 .../en/{v0.2.0 => v0.2}/user/_index.md | 0 .../en/{v0.2.0 => v0.2}/user/http-redirect.md | 0 .../user/http-request-headers.md | 0 .../en/{v0.2.0 => v0.2}/user/http-routing.md | 0 .../user/http-traffic-splitting.md | 0 .../en/{v0.2.0 => v0.2}/user/quickstart.md | 2 +- .../{v0.2.0 => v0.2}/user/secure-gateways.md | 0 .../{v0.2.0 => v0.2}/user/tls-passthrough.md | 0 site/content/en/{v0.3.0 => v0.3}/_index.md | 0 .../content/en/{v0.4.0 => v0.3}/api/_index.md | 0 .../en/{v0.3.0 => v0.3}/api/config_types.md | 2 +- .../{v0.3.0 => v0.3}/api/extension_types.md | 0 .../contributions/CODEOWNERS.md | 0 .../contributions/CODE_OF_CONDUCT.md | 0 .../contributions/CONTRIBUTING.md | 4 +- .../{v0.3.0 => v0.3}/contributions/DEVELOP.md | 2 +- .../en/{v0.3.0 => v0.3}/contributions/DOCS.md | 0 .../contributions/RELEASING.md | 2 +- .../{v0.3.0 => v0.3}/contributions/_index.md | 0 .../{v0.3.0 => v0.3}/contributions/roadmap.md | 0 .../en/{v0.3.0 => v0.3}/design/_index.md | 0 .../en/{v0.2.0 => v0.3}/design/config-api.md | 4 +- .../en/{v0.3.0 => v0.3}/design/egctl.md | 0 .../design/gatewayapi-support.md | 8 +- .../design/gatewayapi-translator.md | 0 .../en/{v0.3.0 => v0.3}/design/goals.md | 0 .../en/{v0.3.0 => v0.3}/design/ratelimit.md | 0 .../design/request-authentication.md | 0 .../{v0.2.0 => v0.3}/design/system-design.md | 8 +- .../{v0.3.0 => v0.3}/design/tcp-udp-design.md | 0 .../en/{v0.3.0 => v0.3}/design/watching.md | 0 .../en/{v0.3.0 => v0.3}/user/_index.md | 0 .../content/en/{v0.3.0 => v0.3}/user/authn.md | 2 +- .../en/{v0.3.0 => v0.3}/user/grpc-routing.md | 0 .../en/{v0.3.0 => v0.3}/user/http-redirect.md | 0 .../user/http-request-headers.md | 0 .../user/http-response-headers.md | 0 .../en/{v0.3.0 => v0.3}/user/http-routing.md | 0 .../user/http-traffic-splitting.md | 0 .../{v0.3.0 => v0.3}/user/http-urlrewrite.md | 0 .../en/{v0.3.0 => v0.3}/user/quickstart.md | 2 +- .../en/{v0.3.0 => v0.3}/user/rate-limit.md | 4 +- .../{v0.3.0 => v0.3}/user/secure-gateways.md | 0 .../en/{v0.3.0 => v0.3}/user/tcp-routing.md | 0 .../{v0.3.0 => v0.3}/user/tls-passthrough.md | 0 .../en/{v0.3.0 => v0.3}/user/udp-routing.md | 2 +- site/content/en/{v0.4.0 => v0.4}/_index.md | 0 .../content/en/{v0.5.0 => v0.4}/api/_index.md | 0 .../en/{v0.4.0 => v0.4}/api/config_types.md | 2 +- .../{v0.4.0 => v0.4}/api/extension_types.md | 0 .../contributions/CODEOWNERS.md | 0 .../contributions/CODE_OF_CONDUCT.md | 0 .../contributions/CONTRIBUTING.md | 4 +- .../{v0.2.0 => v0.4}/contributions/DEVELOP.md | 2 +- .../en/{v0.4.0 => v0.4}/contributions/DOCS.md | 0 .../contributions/RELEASING.md | 2 +- .../{v0.4.0 => v0.4}/contributions/_index.md | 0 .../{v0.4.0 => v0.4}/contributions/roadmap.md | 0 .../en/{v0.4.0 => v0.4}/design/_index.md | 0 .../en/{v0.4.0 => v0.4}/design/bootstrap.md | 2 +- .../en/{v0.4.0 => v0.4}/design/config-api.md | 4 +- .../en/{v0.4.0 => v0.4}/design/egctl.md | 0 .../design/extending-envoy-gateway.md | 6 +- .../design/gatewayapi-translator.md | 0 .../en/{v0.4.0 => v0.4}/design/goals.md | 0 .../en/{v0.4.0 => v0.4}/design/rate-limit.md | 0 .../design/request-authentication.md | 0 .../{v0.4.0 => v0.4}/design/system-design.md | 8 +- .../{v0.4.0 => v0.4}/design/tcp-udp-design.md | 0 .../en/{v0.4.0 => v0.4}/design/watching.md | 0 .../en/{v0.4.0 => v0.4}/user/_index.md | 0 .../content/en/{v0.4.0 => v0.4}/user/authn.md | 2 +- .../user/customize-envoyproxy.md | 6 +- .../{v0.4.0 => v0.4}/user/deployment-mode.md | 0 .../content/en/{v0.4.0 => v0.4}/user/egctl.md | 2 +- .../user/gatewayapi-support.md | 8 +- .../en/{v0.4.0 => v0.4}/user/grpc-routing.md | 0 .../en/{v0.4.0 => v0.4}/user/http-redirect.md | 0 .../user/http-request-headers.md | 0 .../user/http-response-headers.md | 0 .../en/{v0.4.0 => v0.4}/user/http-routing.md | 0 .../user/http-traffic-splitting.md | 0 .../{v0.4.0 => v0.4}/user/http-urlrewrite.md | 0 .../en/{v0.4.0 => v0.4}/user/quickstart.md | 2 +- .../en/{v0.4.0 => v0.4}/user/rate-limit.md | 6 +- .../{v0.4.0 => v0.4}/user/secure-gateways.md | 0 .../en/{v0.4.0 => v0.4}/user/tcp-routing.md | 0 .../{v0.4.0 => v0.4}/user/tls-passthrough.md | 0 .../en/{v0.4.0 => v0.4}/user/udp-routing.md | 2 +- site/content/en/{v0.5.0 => v0.5}/_index.md | 0 .../content/en/{v0.6.0 => v0.5}/api/_index.md | 0 .../en/{v0.5.0 => v0.5}/api/config_types.md | 2 +- .../{v0.5.0 => v0.5}/api/extension_types.md | 0 .../contributions/CODEOWNERS.md | 0 .../contributions/CODE_OF_CONDUCT.md | 0 .../contributions/CONTRIBUTING.md | 4 +- .../{v0.5.0 => v0.5}/contributions/DEVELOP.md | 2 +- .../en/{v0.5.0 => v0.5}/contributions/DOCS.md | 0 .../contributions/RELEASING.md | 2 +- .../{v0.5.0 => v0.5}/contributions/_index.md | 0 .../{v0.5.0 => v0.5}/contributions/roadmap.md | 0 .../en/{v0.5.0 => v0.5}/design/_index.md | 0 .../en/{v0.5.0 => v0.5}/design/accesslog.md | 0 .../en/{v0.5.0 => v0.5}/design/bootstrap.md | 2 +- .../en/{v0.5.0 => v0.5}/design/config-api.md | 4 +- .../en/{v0.5.0 => v0.5}/design/egctl.md | 0 .../design/envoy-patch-policy.md | 12 +- .../design/extending-envoy-gateway.md | 6 +- .../design/gatewayapi-translator.md | 0 .../en/{v0.5.0 => v0.5}/design/goals.md | 0 .../design/local-envoy-gateway.md | 0 .../en/{v0.5.0 => v0.5}/design/metrics.md | 0 .../en/{v0.5.0 => v0.5}/design/pprof.md | 0 .../en/{v0.5.0 => v0.5}/design/rate-limit.md | 0 .../design/request-authentication.md | 0 .../{v0.5.0 => v0.5}/design/system-design.md | 11 +- .../{v0.5.0 => v0.5}/design/tcp-udp-design.md | 0 .../en/{v0.5.0 => v0.5}/design/tracing.md | 0 .../en/{v0.5.0 => v0.5}/design/watching.md | 0 .../en/{v0.6.0 => v0.5}/install/_index.md | 0 .../en/{v0.5.0 => v0.5}/install/api.md | 0 .../{v0.5.0 => v0.5}/install/install-egctl.md | 2 +- .../{v0.5.0 => v0.5}/install/install-helm.md | 4 +- .../{v0.5.0 => v0.5}/install/install-yaml.md | 4 +- .../en/{v0.5.0 => v0.5}/user/_index.md | 0 .../content/en/{v0.5.0 => v0.5}/user/authn.md | 2 +- .../user/customize-envoyproxy.md | 6 +- .../{v0.5.0 => v0.5}/user/deployment-mode.md | 0 .../content/en/{v0.5.0 => v0.5}/user/egctl.md | 2 +- .../user/envoy-patch-policy.md | 10 +- .../{v0.5.0 => v0.5}/user/gateway-address.md | 0 .../user/gatewayapi-support.md | 8 +- .../en/{v0.5.0 => v0.5}/user/grpc-routing.md | 0 .../en/{v0.5.0 => v0.5}/user/http-redirect.md | 0 .../user/http-request-headers.md | 0 .../user/http-request-mirroring.md | 0 .../user/http-response-headers.md | 0 .../en/{v0.5.0 => v0.5}/user/http-routing.md | 0 .../user/http-traffic-splitting.md | 0 .../{v0.5.0 => v0.5}/user/http-urlrewrite.md | 0 .../user/proxy-observability.md | 0 .../en/{v0.5.0 => v0.5}/user/quickstart.md | 2 +- .../en/{v0.5.0 => v0.5}/user/rate-limit.md | 6 +- .../{v0.5.0 => v0.5}/user/secure-gateways.md | 0 .../en/{v0.5.0 => v0.5}/user/tcp-routing.md | 0 .../{v0.5.0 => v0.5}/user/tls-cert-manager.md | 0 .../{v0.5.0 => v0.5}/user/tls-passthrough.md | 0 .../{v0.5.0 => v0.5}/user/tls-termination.md | 0 .../en/{v0.5.0 => v0.5}/user/udp-routing.md | 2 +- .../en/v0.6.0/contributions/CONTRIBUTING.md | 190 - .../en/v0.6.0/contributions/DEVELOP.md | 163 - site/content/en/{v0.6.0 => v0.6}/_index.md | 0 .../content/en/{v1.0.2 => v0.6}/api/_index.md | 0 .../{v0.6.0 => v0.6}/api/extension_types.md | 2 +- .../contributions/CODEOWNERS.md | 0 .../contributions/CODE_OF_CONDUCT.md | 0 .../en/v0.6/contributions/CONTRIBUTING.md | 190 + .../{v0.4.0 => v0.6}/contributions/DEVELOP.md | 2 +- .../en/{v0.6.0 => v0.6}/contributions/DOCS.md | 0 .../contributions/RELEASING.md | 5 +- .../{v0.6.0 => v0.6}/contributions/_index.md | 0 .../{v0.6.0 => v0.6}/contributions/roadmap.md | 0 .../en/{v0.6.0 => v0.6}/design/_index.md | 0 .../en/{v0.6.0 => v0.6}/design/accesslog.md | 0 .../design/backend-traffic-policy.md | 0 .../en/{v0.6.0 => v0.6}/design/bootstrap.md | 2 +- .../design/client-traffic-policy.md | 0 .../en/{v0.6.0 => v0.6}/design/config-api.md | 2 +- .../en/{v0.6.0 => v0.6}/design/eg-metrics.md | 0 .../en/{v0.6.0 => v0.6}/design/egctl.md | 0 .../design/envoy-patch-policy.md | 10 +- .../design/extending-envoy-gateway.md | 6 +- .../design/gatewayapi-translator.md | 0 .../en/{v0.6.0 => v0.6}/design/goals.md | 0 .../design/local-envoy-gateway.md | 0 .../en/{v0.6.0 => v0.6}/design/metrics.md | 0 .../en/{v0.6.0 => v0.6}/design/pprof.md | 0 .../en/{v0.6.0 => v0.6}/design/rate-limit.md | 0 .../design/security-policy.md | 0 .../{v0.6.0 => v0.6}/design/system-design.md | 4 +- .../{v0.6.0 => v0.6}/design/tcp-udp-design.md | 0 .../en/{v0.6.0 => v0.6}/design/tracing.md | 0 .../en/{v0.6.0 => v0.6}/design/watching.md | 0 .../en/{v1.0.2 => v0.6}/install/_index.md | 0 .../en/{v0.6.0 => v0.6}/install/api.md | 0 .../{v0.6.0 => v0.6}/install/install-egctl.md | 0 .../{v0.6.0 => v0.6}/install/install-helm.md | 4 +- .../{v0.6.0 => v0.6}/install/install-yaml.md | 4 +- .../en/{v0.6.0 => v0.6}/user/_index.md | 0 site/content/en/{v0.6.0 => v0.6}/user/cors.md | 6 +- .../user/customize-envoyproxy.md | 6 +- .../{v0.6.0 => v0.6}/user/deployment-mode.md | 0 .../content/en/{v0.6.0 => v0.6}/user/egctl.md | 2 +- .../user/envoy-patch-policy.md | 12 +- .../{v0.6.0 => v0.6}/user/gateway-address.md | 0 .../user/gateway-api-metrics.md | 6 +- .../user/gatewayapi-support.md | 6 +- .../user/grafana-integration.md | 16 +- .../en/{v0.6.0 => v0.6}/user/grpc-routing.md | 0 .../en/{v0.6.0 => v0.6}/user/http-redirect.md | 2 +- .../user/http-request-headers.md | 2 +- .../user/http-request-mirroring.md | 2 +- .../user/http-response-headers.md | 2 +- .../en/{v0.6.0 => v0.6}/user/http-routing.md | 0 .../user/http-traffic-splitting.md | 2 +- .../{v0.6.0 => v0.6}/user/http-urlrewrite.md | 2 +- .../user/jwt-authentication.md | 6 +- .../user/multicluster-service.md | 0 .../user/proxy-observability.md | 4 +- .../en/{v0.6.0 => v0.6}/user/quickstart.md | 2 +- .../en/{v0.6.0 => v0.6}/user/rate-limit.md | 8 +- .../{v0.6.0 => v0.6}/user/secure-gateways.md | 20 +- .../en/{v0.6.0 => v0.6}/user/tcp-routing.md | 0 .../{v0.6.0 => v0.6}/user/tls-cert-manager.md | 4 +- .../{v0.6.0 => v0.6}/user/tls-passthrough.md | 4 +- .../{v0.6.0 => v0.6}/user/tls-termination.md | 2 +- .../en/{v0.6.0 => v0.6}/user/udp-routing.md | 4 +- site/content/en/v1.0.2/releases/_index.md | 5 - site/content/en/v1.0.2/releases/v0.1.0.md | 9 - site/content/en/v1.0.2/releases/v0.2.0.md | 53 - site/content/en/v1.0.2/releases/v0.3.0.md | 77 - .../content/en/v1.0.2/releases/v0.4.0-rc.1.md | 56 - site/content/en/v1.0.2/releases/v0.5.0.md | 71 - .../content/en/v1.0.2/releases/v0.6.0-rc.1.md | 64 - site/content/en/v1.0.2/releases/v1.0.1.md | 27 - site/content/en/v1.0.2/releases/v1.0.2.md | 29 - site/content/en/v1.0/_index.md | 15 + site/content/en/v1.0/api/_index.md | 5 + .../{v1.0.2 => v1.0}/api/extension_types.md | 0 site/content/en/v1.0/install/_index.md | 5 + .../en/{v1.0.2 => v1.0}/install/api.md | 0 .../{v1.0.2 => v1.0}/install/custom-cert.md | 0 site/content/en/v1.0/install/install-egctl.md | 72 + .../{v1.0.2 => v1.0}/install/install-helm.md | 0 .../{v1.0.2 => v1.0}/install/install-yaml.md | 0 site/content/en/v1.0/tasks/_index.md | 5 + .../tasks/extensibility/_index.md | 0 .../tasks/extensibility/envoy-patch-policy.md | 0 .../tasks/observability/_index.md | 0 .../observability/gateway-api-metrics.md | 59 + .../observability/grafana-integration.md | 10 +- .../observability/proxy-observability.md | 0 .../tasks/operations/_index.md | 0 .../tasks/operations/customize-envoyproxy.md | 0 .../tasks/operations/deployment-mode.md | 0 .../tasks/operations/egctl.md | 0 .../en/{v1.0.2 => v1.0}/tasks/quickstart.md | 0 .../{v1.0.2 => v1.0}/tasks/security/_index.md | 0 .../tasks/security/backend-tls.md | 5 +- .../tasks/security/basic-auth.md | 0 .../{v1.0.2 => v1.0}/tasks/security/cors.md | 0 .../tasks/security/ext-auth.md | 0 .../tasks/security/jwt-authentication.md | 0 .../tasks/security/mutual-tls.md | 0 .../{v1.0.2 => v1.0}/tasks/security/oidc.md | 0 .../tasks/security/secure-gateways.md | 0 .../tasks/security/threat-model.md | 4 +- .../tasks/security/tls-cert-manager.md | 0 .../tasks/security/tls-passthrough.md | 0 .../tasks/security/tls-termination.md | 0 site/content/en/v1.0/tasks/traffic/_index.md | 5 + .../tasks/traffic/circuit-breaker.md | 0 .../tasks/traffic/client-traffic-policy.md | 0 .../tasks/traffic/fault-injection.md | 0 .../en/v1.0/tasks/traffic/gateway-address.md | 68 + .../tasks/traffic/gatewayapi-support.md | 0 .../tasks/traffic/global-rate-limit.md | 0 .../tasks/traffic/grpc-routing.md | 0 .../en/v1.0/tasks/traffic/http-redirect.md | 399 ++ .../tasks/traffic/http-request-headers.md | 0 .../tasks/traffic/http-request-mirroring.md | 0 .../tasks/traffic/http-response-headers.md | 0 .../tasks/traffic/http-routing.md | 0 .../en/v1.0/tasks/traffic/http-timeouts.md | 199 + .../tasks/traffic/http-traffic-splitting.md | 0 .../tasks/traffic/http-urlrewrite.md | 0 .../{v1.0.2 => v1.0}/tasks/traffic/http3.md | 0 .../tasks/traffic/local-rate-limit.md | 0 .../tasks/traffic/multicluster-service.md | 0 .../{v1.0.2 => v1.0}/tasks/traffic/retry.md | 0 .../traffic/routing-outside-kubernetes.md | 0 .../tasks/traffic/tcp-routing.md | 0 .../tasks/traffic/udp-routing.md | 0 site/content/en/v1.1/_index.md | 15 + site/content/en/v1.1/api/_index.md | 5 + site/content/en/v1.1/api/extension_types.md | 3861 +++++++++++++++++ site/content/en/v1.1/boilerplates/index.md | 5 + .../v1.1/boilerplates/o11y_prerequisites.md | 14 + site/content/en/v1.1/concepts/_index.md | 5 + .../en/v1.1/concepts/concepts_overview.md | 54 + site/content/en/v1.1/install/_index.md | 5 + site/content/en/v1.1/install/custom-cert.md | 146 + .../v1.1/install/gateway-addons-helm-api.md | 123 + .../en/v1.1/install/gateway-helm-api.md | 66 + site/content/en/v1.1/install/install-egctl.md | 72 + site/content/en/v1.1/install/install-helm.md | 144 + site/content/en/v1.1/install/install-yaml.md | 39 + site/content/en/v1.1/tasks/_index.md | 5 + .../en/v1.1/tasks/extensibility/_index.md | 5 + .../tasks/extensibility/build-wasm-image.md | 71 + .../tasks/extensibility/envoy-patch-policy.md | 359 ++ .../en/v1.1/tasks/extensibility/ext-proc.md | 290 ++ .../tasks/extensibility/extension-server.md | 209 + .../en/v1.1/tasks/extensibility/wasm.md | 194 + .../en/v1.1/tasks/observability/_index.md | 5 + .../observability/gateway-api-metrics.md | 59 + .../observability/gateway-exported-metrics.md | 97 + .../observability/gateway-observability.md | 176 + .../observability/grafana-integration.md | 87 + .../tasks/observability/proxy-accesslog.md | 251 ++ .../v1.1/tasks/observability/proxy-metric.md | 47 + .../v1.1/tasks/observability/proxy-trace.md | 233 + .../observability/rate-limit-observability.md | 98 + .../en/v1.1/tasks/operations/_index.md | 5 + .../tasks/operations/customize-envoyproxy.md | 955 ++++ .../v1.1/tasks/operations/deployment-mode.md | 1072 +++++ .../content/en/v1.1/tasks/operations/egctl.md | 908 ++++ site/content/en/v1.1/tasks/quickstart.md | 156 + site/content/en/v1.1/tasks/security/_index.md | 5 + .../en/v1.1/tasks/security/backend-mtls.md | 200 + .../en/v1.1/tasks/security/backend-tls.md | 390 ++ .../en/v1.1/tasks/security/basic-auth.md | 221 + site/content/en/v1.1/tasks/security/cors.md | 177 + .../en/v1.1/tasks/security/ext-auth.md | 460 ++ .../v1.1/tasks/security/jwt-authentication.md | 170 + .../en/v1.1/tasks/security/mutual-tls.md | 186 + site/content/en/v1.1/tasks/security/oidc.md | 322 ++ .../tasks/security/private-key-provider.md | 621 +++ .../v1.1/tasks/security/restrict-ip-access.md | 197 + .../en/v1.1/tasks/security/secure-gateways.md | 520 +++ .../en/v1.1/tasks/security/threat-model.md | 665 +++ .../v1.1/tasks/security/tls-cert-manager.md | 436 ++ .../en/v1.1/tasks/security/tls-passthrough.md | 124 + .../en/v1.1/tasks/security/tls-termination.md | 91 + site/content/en/v1.1/tasks/traffic/_index.md | 5 + site/content/en/v1.1/tasks/traffic/backend.md | 211 + .../en/v1.1/tasks/traffic/circuit-breaker.md | 150 + .../tasks/traffic/client-traffic-policy.md | 688 +++ .../en/v1.1/tasks/traffic/connection-limit.md | 137 + .../en/v1.1/tasks/traffic/fault-injection.md | 381 ++ .../en/v1.1/tasks/traffic/gateway-address.md | 68 + .../v1.1/tasks/traffic/gatewayapi-support.md | 120 + .../v1.1/tasks/traffic/global-rate-limit.md | 1312 ++++++ .../en/v1.1/tasks/traffic/grpc-routing.md | 272 ++ .../en/v1.1/tasks/traffic/http-redirect.md | 399 ++ .../tasks/traffic/http-request-headers.md | 449 ++ .../tasks/traffic/http-request-mirroring.md | 447 ++ .../tasks/traffic/http-response-headers.md | 446 ++ .../en/v1.1/tasks/traffic/http-routing.md | 302 ++ .../en/v1.1/tasks/traffic/http-timeouts.md | 199 + .../tasks/traffic/http-traffic-splitting.md | 527 +++ .../en/v1.1/tasks/traffic/http-urlrewrite.md | 405 ++ site/content/en/v1.1/tasks/traffic/http3.md | 136 + .../en/v1.1/tasks/traffic/load-balancing.md | 922 ++++ .../en/v1.1/tasks/traffic/local-rate-limit.md | 414 ++ .../tasks/traffic/multicluster-service.md | 86 + site/content/en/v1.1/tasks/traffic/retry.md | 147 + .../traffic/routing-outside-kubernetes.md | 168 + .../en/v1.1/tasks/traffic/tcp-routing.md | 483 +++ .../en/v1.1/tasks/traffic/udp-routing.md | 170 + site/content/zh/_index.md | 2 +- site/content/zh/latest/api/extension_types.md | 2 +- .../latest/install/gateway-addons-helm-api.md | 1 + site/hugo.toml | 30 +- site/layouts/shortcodes/boilerplate.html | 2 +- site/package.json | 4 +- .../img/envoy-gateway-resources-dashboard.png | Bin 130568 -> 0 bytes .../img/envoy-gateway-resources-overview.png | Bin 0 -> 563199 bytes .../img/envoy-pod-resources-dashboard.png | Bin 74690 -> 0 bytes ...d.png => envoy-proxy-global-dashboard.png} | Bin .../img/resources-monitor-dashboard.png | Bin 0 -> 261700 bytes site/static/img/wasm-extension.png | Bin 0 -> 55654 bytes test/benchmark/benchmark_report.md | 992 ----- test/benchmark/benchmark_test.go | 16 +- test/benchmark/config/nighthawk-client.yaml | 2 - test/benchmark/suite/flags.go | 10 +- test/benchmark/suite/{test.go => options.go} | 8 - test/benchmark/suite/render.go | 301 +- test/benchmark/suite/report.go | 179 +- test/benchmark/suite/suite.go | 84 +- .../backendtrafficpolicy_test.go | 19 + .../clienttrafficpolicy_test.go | 19 + .../envoyextensionpolicy_test.go | 19 + test/cel-validation/securitypolicy_test.go | 19 + test/e2e/base/manifests.yaml | 509 +-- test/e2e/e2e_test.go | 29 +- test/e2e/embed.go | 23 +- .../e2e/merge_gateways/merge_gateways_test.go | 21 +- test/e2e/multiple_gc/multiple_gc_test.go | 24 +- test/e2e/testdata/accesslog-als.yaml | 216 + .../e2e/testdata/authorization-client-ip.yaml | 12 +- .../authorization-default-action.yaml | 8 +- .../backend-health-check-active-http.yaml | 3 - test/e2e/testdata/backend-tls-settings.yaml | 27 + test/e2e/testdata/backend-upgrade.yaml | 5 +- test/e2e/testdata/basic-auth.yaml | 9 +- test/e2e/testdata/circuitbreaker.yaml | 5 +- test/e2e/testdata/client-mtls.yaml | 71 + test/e2e/testdata/client-timeout.yaml | 25 +- test/e2e/testdata/connection-limit.yaml | 24 +- test/e2e/testdata/cors.yaml | 5 +- test/e2e/testdata/envoy-patch-policy.yaml | 1 - .../ext-auth-grpc-securitypolicy.yaml | 4 +- .../ext-auth-http-securitypolicy.yaml | 4 +- .../ext-proc-envoyextensionpolicy.yaml | 12 +- test/e2e/testdata/fault-injection.yaml | 18 +- test/e2e/testdata/jwt-optional.yaml | 8 +- test/e2e/testdata/listener-health-check.yaml | 9 +- ...load_balancing_consistent_hash_cookie.yaml | 85 + ...load_balancing_consistent_hash_header.yaml | 82 + ...d_balancing_consistent_hash_source_ip.yaml | 80 + .../testdata/load_balancing_round_robin.yaml | 78 + test/e2e/testdata/local-ratelimit.yaml | 10 +- test/e2e/testdata/oidc-securitypolicy.yaml | 4 +- test/e2e/testdata/preserve-case.yaml | 5 +- .../testdata/ratelimit-based-jwt-claims.yaml | 10 +- test/e2e/testdata/ratelimit-cidr-match.yaml | 9 +- test/e2e/testdata/ratelimit-header-match.yaml | 9 +- .../testdata/ratelimit-headers-disabled.yaml | 17 +- .../ratelimit-multiple-listeners.yaml | 9 +- test/e2e/testdata/retry.yaml | 9 +- .../securitypolicy-translation-failed.yaml | 39 + test/e2e/testdata/tcp-route.yaml | 21 + test/e2e/testdata/tracing-zipkin.yaml | 64 + test/e2e/testdata/udproute.yaml | 86 + test/e2e/testdata/use-client-protocol.yaml | 5 +- test/e2e/testdata/wasm-http.yaml | 4 +- test/e2e/tests/accesslog.go | 30 +- ...lient-ip.go => authorization_client_ip.go} | 50 +- ...ion.go => authorization_default_action.go} | 20 +- test/e2e/tests/backend_health_check.go | 15 +- test/e2e/tests/basic_auth.go | 50 +- test/e2e/tests/client_mtls.go | 117 + test/e2e/tests/client_timeout.go | 21 +- test/e2e/tests/connection_limit.go | 37 +- test/e2e/tests/controlplane.go | 3 +- test/e2e/tests/eg_upgrade.go | 314 +- test/e2e/tests/envoy_shutdown.go | 4 - test/e2e/tests/ext_auth_grpc_service.go | 4 +- ...path.go => httproute_rewrite_full_path.go} | 0 test/e2e/tests/load_balancing.go | 360 ++ test/e2e/tests/metric.go | 5 +- .../tests/{multiple-gc.go => multiple_gc.go} | 0 test/e2e/tests/oidc.go | 39 +- test/e2e/tests/oidc_testclient.go | 2 +- test/e2e/tests/retry.go | 19 +- .../securitypolicy_transaltion_failed.go | 62 + test/e2e/tests/tcp_route.go | 5 +- test/e2e/tests/tracing.go | 113 +- test/e2e/tests/udproute.go | 32 +- test/e2e/tests/use_client_protocol.go | 4 + test/e2e/tests/utils.go | 158 +- test/e2e/tests/wasm_oci.go | 5 +- test/e2e/tests/weighted_backend.go | 26 +- test/e2e/upgrade/eg_upgrade_test.go | 38 +- test/e2e/upgrade/manifests.yaml | 125 + .../helm/gateway-addons-helm/default.out.yaml | 749 +--- test/helm/gateway-addons-helm/e2e.out.yaml | 749 +--- .../suite => utils/kubernetes}/client.go | 20 +- test/{e2e => }/utils/prometheus/prometheus.go | 66 +- tools/docker/envoy-gateway/Dockerfile | 2 +- tools/github-actions/setup-deps/action.yaml | 4 +- tools/hack/create-cluster.sh | 6 + tools/linter/codespell/.codespell.skip | 4 +- tools/linter/yamllint/.yamllint | 2 + tools/make/common.mk | 12 +- tools/make/docs.mk | 42 +- tools/make/golang.mk | 10 +- tools/make/helm.mk | 14 +- tools/make/kube.mk | 62 +- tools/make/lint.mk | 11 +- tools/make/tools.mk | 3 + tools/osv-scanner/config.toml | 3 + tools/src/gci/go.mod | 22 + tools/src/gci/go.sum | 49 + tools/src/gci/pin.go | 11 + tools/src/helm-docs/go.mod | 28 +- tools/src/helm-docs/go.sum | 81 +- tools/src/jb/go.mod | 16 + tools/src/jb/go.sum | 54 + tools/src/jb/pin.go | 11 + tools/src/jsonnet/go.mod | 14 + tools/src/jsonnet/go.sum | 20 + tools/src/jsonnet/pin.go | 11 + tools/src/release-notes-docs/yml2md.py | 4 + tools/src/sphinx-build/requirements.txt | 2 - 863 files changed, 62105 insertions(+), 6561 deletions(-) delete mode 100644 .github/workflows/cherrypick.yaml delete mode 100644 charts/gateway-addons-helm/dashboards/envoy-gateway-resource.json delete mode 100644 charts/gateway-addons-helm/dashboards/envoy-pod-resource.json rename charts/gateway-addons-helm/dashboards/{envoy-global.json => envoy-proxy-global.json} (100%) create mode 100644 charts/gateway-addons-helm/dashboards/jsonnetfile.json create mode 100644 charts/gateway-addons-helm/dashboards/jsonnetfile.lock.json create mode 100644 charts/gateway-addons-helm/dashboards/lib/g.libsonnet create mode 100644 charts/gateway-addons-helm/dashboards/lib/panels.libsonnet create mode 100644 charts/gateway-addons-helm/dashboards/lib/queries.libsonnet create mode 100644 charts/gateway-addons-helm/dashboards/lib/variables.libsonnet create mode 100644 charts/gateway-addons-helm/dashboards/resources-monitor.gen.json create mode 100644 charts/gateway-addons-helm/dashboards/resources-monitor.libsonnet create mode 100644 examples/extension-server/.gitignore create mode 100644 examples/extension-server/Makefile create mode 100644 examples/extension-server/api/doc.go create mode 100644 examples/extension-server/api/v1alpha1/doc.go create mode 100644 examples/extension-server/api/v1alpha1/groupversion_info.go create mode 100644 examples/extension-server/api/v1alpha1/listenercontext_types.go create mode 100644 examples/extension-server/api/v1alpha1/zz_generated.deepcopy.go create mode 100644 examples/extension-server/charts/extension-server/.helmignore create mode 100644 examples/extension-server/charts/extension-server/Chart.yaml create mode 100644 examples/extension-server/charts/extension-server/crds/generated/example.extensions.io_listenercontextexamples.yaml create mode 100644 examples/extension-server/charts/extension-server/templates/NOTES.txt create mode 100644 examples/extension-server/charts/extension-server/templates/_helpers.tpl create mode 100644 examples/extension-server/charts/extension-server/templates/deployment.yaml create mode 100644 examples/extension-server/charts/extension-server/templates/service.yaml create mode 100644 examples/extension-server/charts/extension-server/templates/serviceaccount.yaml create mode 100644 examples/extension-server/charts/extension-server/values.yaml create mode 100644 examples/extension-server/cmd/extension-server/main.go create mode 100644 examples/extension-server/go.mod create mode 100644 examples/extension-server/go.sum create mode 100644 examples/extension-server/internal/extensionserver/htpasswd.go create mode 100644 examples/extension-server/internal/extensionserver/server.go create mode 100644 examples/extension-server/tools/boilerplate.generatego.txt create mode 100644 examples/extension-server/tools/docker/extension-server/Dockerfile create mode 100644 examples/extension-server/tools/src/controller-gen/go.mod create mode 100644 examples/extension-server/tools/src/controller-gen/go.sum create mode 100644 examples/extension-server/tools/src/controller-gen/pin.go create mode 100644 examples/kubernetes/envoy-als.yaml create mode 100644 internal/cmd/egctl/collect.go create mode 100644 internal/gatewayapi/testdata/httproute-with-multi-gateways-notmatch.in.yaml create mode 100644 internal/gatewayapi/testdata/httproute-with-multi-gateways-notmatch.out.yaml create mode 100644 internal/gatewayapi/testdata/httproute-with-multi-gateways-with-same-name.in.yaml create mode 100644 internal/gatewayapi/testdata/httproute-with-multi-gateways-with-same-name.out.yaml create mode 100644 internal/globalratelimit/runner/runner_test.go create mode 100644 internal/troubleshoot/collect.go create mode 100644 internal/troubleshoot/collect/config_dump.go create mode 100644 internal/troubleshoot/collect/custom_resource.go create mode 100644 internal/troubleshoot/collect/envoy_gateway_resource.go create mode 100644 internal/troubleshoot/collect/prometheus_metrics.go create mode 100644 internal/troubleshoot/collect/troubleshoot_helper.go create mode 100644 internal/utils/helm/package_test.go create mode 100644 internal/xds/translator/testdata/in/xds-ir/http-route-with-clientcert.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/http-route-with-clientcert.clusters.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/http-route-with-clientcert.endpoints.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/http-route-with-clientcert.listeners.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/http-route-with-clientcert.routes.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/http-route-with-clientcert.secrets.yaml create mode 100644 release-notes/v1.1.0.yaml create mode 100644 site/content/en/contributions/design/wasm-extension.md rename site/content/en/{v1.0.2 => docs}/_index.md (100%) rename site/content/en/{v0.3.0 => docs}/api/_index.md (100%) create mode 100644 site/content/en/docs/api/extension_types.md create mode 100644 site/content/en/docs/boilerplates/index.md create mode 100644 site/content/en/docs/boilerplates/o11y_prerequisites.md create mode 100644 site/content/en/docs/concepts/_index.md create mode 100644 site/content/en/docs/concepts/concepts_overview.md rename site/content/en/{v0.5.0 => docs}/install/_index.md (100%) create mode 100644 site/content/en/docs/install/custom-cert.md create mode 100644 site/content/en/docs/install/gateway-addons-helm-api.md create mode 100644 site/content/en/docs/install/gateway-helm-api.md rename site/content/en/{v1.0.2 => docs}/install/install-egctl.md (100%) create mode 100644 site/content/en/docs/install/install-helm.md create mode 100644 site/content/en/docs/install/install-yaml.md rename site/content/en/{v1.0.2 => docs}/tasks/_index.md (100%) create mode 100644 site/content/en/docs/tasks/extensibility/_index.md create mode 100644 site/content/en/docs/tasks/extensibility/build-wasm-image.md create mode 100644 site/content/en/docs/tasks/extensibility/envoy-patch-policy.md create mode 100644 site/content/en/docs/tasks/extensibility/ext-proc.md create mode 100644 site/content/en/docs/tasks/extensibility/extension-server.md create mode 100644 site/content/en/docs/tasks/extensibility/wasm.md create mode 100644 site/content/en/docs/tasks/observability/_index.md rename site/content/en/{v1.0.2 => docs}/tasks/observability/gateway-api-metrics.md (100%) create mode 100644 site/content/en/docs/tasks/observability/gateway-exported-metrics.md create mode 100644 site/content/en/docs/tasks/observability/gateway-observability.md create mode 100644 site/content/en/docs/tasks/observability/grafana-integration.md create mode 100644 site/content/en/docs/tasks/observability/proxy-accesslog.md create mode 100644 site/content/en/docs/tasks/observability/proxy-metric.md create mode 100644 site/content/en/docs/tasks/observability/proxy-trace.md create mode 100644 site/content/en/docs/tasks/observability/rate-limit-observability.md create mode 100644 site/content/en/docs/tasks/operations/_index.md create mode 100644 site/content/en/docs/tasks/operations/customize-envoyproxy.md create mode 100644 site/content/en/docs/tasks/operations/deployment-mode.md create mode 100644 site/content/en/docs/tasks/operations/egctl.md create mode 100644 site/content/en/docs/tasks/quickstart.md create mode 100644 site/content/en/docs/tasks/security/_index.md create mode 100644 site/content/en/docs/tasks/security/backend-mtls.md create mode 100644 site/content/en/docs/tasks/security/backend-tls.md create mode 100644 site/content/en/docs/tasks/security/basic-auth.md create mode 100644 site/content/en/docs/tasks/security/cors.md create mode 100644 site/content/en/docs/tasks/security/ext-auth.md create mode 100644 site/content/en/docs/tasks/security/jwt-authentication.md create mode 100644 site/content/en/docs/tasks/security/mutual-tls.md create mode 100644 site/content/en/docs/tasks/security/oidc.md create mode 100644 site/content/en/docs/tasks/security/private-key-provider.md create mode 100644 site/content/en/docs/tasks/security/restrict-ip-access.md create mode 100644 site/content/en/docs/tasks/security/secure-gateways.md create mode 100644 site/content/en/docs/tasks/security/threat-model.md create mode 100644 site/content/en/docs/tasks/security/tls-cert-manager.md create mode 100644 site/content/en/docs/tasks/security/tls-passthrough.md create mode 100644 site/content/en/docs/tasks/security/tls-termination.md rename site/content/en/{v1.0.2 => docs}/tasks/traffic/_index.md (100%) create mode 100644 site/content/en/docs/tasks/traffic/backend.md create mode 100644 site/content/en/docs/tasks/traffic/circuit-breaker.md create mode 100644 site/content/en/docs/tasks/traffic/client-traffic-policy.md create mode 100644 site/content/en/docs/tasks/traffic/connection-limit.md create mode 100644 site/content/en/docs/tasks/traffic/fault-injection.md rename site/content/en/{v1.0.2 => docs}/tasks/traffic/gateway-address.md (100%) create mode 100644 site/content/en/docs/tasks/traffic/gatewayapi-support.md create mode 100644 site/content/en/docs/tasks/traffic/global-rate-limit.md create mode 100644 site/content/en/docs/tasks/traffic/grpc-routing.md rename site/content/en/{v1.0.2 => docs}/tasks/traffic/http-redirect.md (100%) create mode 100644 site/content/en/docs/tasks/traffic/http-request-headers.md create mode 100644 site/content/en/docs/tasks/traffic/http-request-mirroring.md create mode 100644 site/content/en/docs/tasks/traffic/http-response-headers.md create mode 100644 site/content/en/docs/tasks/traffic/http-routing.md rename site/content/en/{v1.0.2 => docs}/tasks/traffic/http-timeouts.md (100%) create mode 100644 site/content/en/docs/tasks/traffic/http-traffic-splitting.md create mode 100644 site/content/en/docs/tasks/traffic/http-urlrewrite.md create mode 100644 site/content/en/docs/tasks/traffic/http3.md create mode 100644 site/content/en/docs/tasks/traffic/load-balancing.md create mode 100644 site/content/en/docs/tasks/traffic/local-rate-limit.md create mode 100644 site/content/en/docs/tasks/traffic/multicluster-service.md create mode 100644 site/content/en/docs/tasks/traffic/retry.md create mode 100644 site/content/en/docs/tasks/traffic/routing-outside-kubernetes.md create mode 100644 site/content/en/docs/tasks/traffic/tcp-routing.md create mode 100644 site/content/en/docs/tasks/traffic/udp-routing.md create mode 100644 site/content/en/latest/concepts/_index.md create mode 100644 site/content/en/latest/concepts/concepts_overview.md delete mode 100644 site/content/en/latest/releases/_index.md delete mode 100644 site/content/en/latest/releases/v0.2.0-rc1.md delete mode 100644 site/content/en/latest/releases/v0.2.0-rc2.md delete mode 100644 site/content/en/latest/releases/v0.3.0-rc.1.md delete mode 100644 site/content/en/latest/releases/v0.4.0.md delete mode 100644 site/content/en/latest/releases/v0.5.0.md delete mode 100644 site/content/en/latest/releases/v0.6.0.md create mode 100644 site/content/en/latest/tasks/extensibility/build-wasm-image.md create mode 100644 site/content/en/latest/tasks/extensibility/extension-server.md create mode 100644 site/content/en/latest/tasks/extensibility/wasm.md create mode 100644 site/content/en/latest/tasks/security/backend-mtls.md create mode 100644 site/content/en/latest/tasks/security/restrict-ip-access.md create mode 100644 site/content/en/latest/tasks/traffic/load-balancing.md create mode 100644 site/content/en/news/releases/notes/_index.md rename site/content/en/{latest/releases => news/releases/notes}/v0.1.0.md (99%) rename site/content/en/{v1.0.2/releases => news/releases/notes}/v0.2.0-rc1.md (93%) rename site/content/en/{v1.0.2/releases => news/releases/notes}/v0.2.0-rc2.md (93%) rename site/content/en/{latest/releases => news/releases/notes}/v0.2.0.md (91%) rename site/content/en/{v1.0.2/releases => news/releases/notes}/v0.3.0-rc.1.md (98%) rename site/content/en/{latest/releases => news/releases/notes}/v0.3.0.md (99%) rename site/content/en/{latest/releases => news/releases/notes}/v0.4.0-rc.1.md (98%) rename site/content/en/{v1.0.2/releases => news/releases/notes}/v0.4.0.md (98%) rename site/content/en/{latest/releases => news/releases/notes}/v0.5.0-rc.1.md (98%) rename site/content/en/{v1.0.2/releases/v0.5.0-rc.1.md => news/releases/notes/v0.5.0.md} (97%) rename site/content/en/{latest/releases => news/releases/notes}/v0.6.0-rc.1.md (95%) rename site/content/en/{v1.0.2/releases => news/releases/notes}/v0.6.0.md (89%) rename site/content/en/{v1.0.2/releases/v1.0.0.md => news/releases/notes/v1.0.0-rc.1.md} (75%) rename site/content/en/{latest/releases => news/releases/notes}/v1.0.0.md (100%) rename site/content/en/{latest/releases => news/releases/notes}/v1.0.1.md (100%) rename site/content/en/{latest/releases => news/releases/notes}/v1.0.2.md (100%) create mode 100644 site/content/en/news/releases/notes/v1.1.0-rc.1.md create mode 100644 site/content/en/news/releases/notes/v1.1.0.md create mode 100644 site/content/en/news/releases/v1.1.md rename site/content/en/{v0.2.0 => v0.2}/_index.md (100%) rename site/content/en/{v0.2.0 => v0.2}/contributions/CODEOWNERS.md (100%) rename site/content/en/{v0.2.0 => v0.2}/contributions/CODE_OF_CONDUCT.md (100%) rename site/content/en/{v0.2.0 => v0.2}/contributions/CONTRIBUTING.md (97%) create mode 100644 site/content/en/v0.2/contributions/DEVELOP.md rename site/content/en/{v0.2.0 => v0.2}/contributions/DOCS.md (100%) rename site/content/en/{v0.3.0 => v0.2}/contributions/RELEASING.md (98%) rename site/content/en/{v0.2.0 => v0.2}/contributions/_index.md (100%) rename site/content/en/{v0.2.0 => v0.2}/contributions/roadmap.md (100%) rename site/content/en/{v0.2.0 => v0.2}/design/_index.md (100%) rename site/content/en/{v0.3.0 => v0.2}/design/config-api.md (98%) rename site/content/en/{v0.2.0 => v0.2}/design/gatewayapi-translator.md (100%) rename site/content/en/{v0.2.0 => v0.2}/design/goals.md (100%) rename site/content/en/{v0.3.0 => v0.2}/design/system-design.md (96%) rename site/content/en/{v0.2.0 => v0.2}/design/watching.md (100%) rename site/content/en/{v0.2.0 => v0.2}/user/_index.md (100%) rename site/content/en/{v0.2.0 => v0.2}/user/http-redirect.md (100%) rename site/content/en/{v0.2.0 => v0.2}/user/http-request-headers.md (100%) rename site/content/en/{v0.2.0 => v0.2}/user/http-routing.md (100%) rename site/content/en/{v0.2.0 => v0.2}/user/http-traffic-splitting.md (100%) rename site/content/en/{v0.2.0 => v0.2}/user/quickstart.md (95%) rename site/content/en/{v0.2.0 => v0.2}/user/secure-gateways.md (100%) rename site/content/en/{v0.2.0 => v0.2}/user/tls-passthrough.md (100%) rename site/content/en/{v0.3.0 => v0.3}/_index.md (100%) rename site/content/en/{v0.4.0 => v0.3}/api/_index.md (100%) rename site/content/en/{v0.3.0 => v0.3}/api/config_types.md (98%) rename site/content/en/{v0.3.0 => v0.3}/api/extension_types.md (100%) rename site/content/en/{v0.3.0 => v0.3}/contributions/CODEOWNERS.md (100%) rename site/content/en/{v0.3.0 => v0.3}/contributions/CODE_OF_CONDUCT.md (100%) rename site/content/en/{v0.5.0 => v0.3}/contributions/CONTRIBUTING.md (97%) rename site/content/en/{v0.3.0 => v0.3}/contributions/DEVELOP.md (98%) rename site/content/en/{v0.3.0 => v0.3}/contributions/DOCS.md (100%) rename site/content/en/{v0.4.0 => v0.3}/contributions/RELEASING.md (98%) rename site/content/en/{v0.3.0 => v0.3}/contributions/_index.md (100%) rename site/content/en/{v0.3.0 => v0.3}/contributions/roadmap.md (100%) rename site/content/en/{v0.3.0 => v0.3}/design/_index.md (100%) rename site/content/en/{v0.2.0 => v0.3}/design/config-api.md (98%) rename site/content/en/{v0.3.0 => v0.3}/design/egctl.md (100%) rename site/content/en/{v0.3.0 => v0.3}/design/gatewayapi-support.md (96%) rename site/content/en/{v0.3.0 => v0.3}/design/gatewayapi-translator.md (100%) rename site/content/en/{v0.3.0 => v0.3}/design/goals.md (100%) rename site/content/en/{v0.3.0 => v0.3}/design/ratelimit.md (100%) rename site/content/en/{v0.3.0 => v0.3}/design/request-authentication.md (100%) rename site/content/en/{v0.2.0 => v0.3}/design/system-design.md (96%) rename site/content/en/{v0.3.0 => v0.3}/design/tcp-udp-design.md (100%) rename site/content/en/{v0.3.0 => v0.3}/design/watching.md (100%) rename site/content/en/{v0.3.0 => v0.3}/user/_index.md (100%) rename site/content/en/{v0.3.0 => v0.3}/user/authn.md (96%) rename site/content/en/{v0.3.0 => v0.3}/user/grpc-routing.md (100%) rename site/content/en/{v0.3.0 => v0.3}/user/http-redirect.md (100%) rename site/content/en/{v0.3.0 => v0.3}/user/http-request-headers.md (100%) rename site/content/en/{v0.3.0 => v0.3}/user/http-response-headers.md (100%) rename site/content/en/{v0.3.0 => v0.3}/user/http-routing.md (100%) rename site/content/en/{v0.3.0 => v0.3}/user/http-traffic-splitting.md (100%) rename site/content/en/{v0.3.0 => v0.3}/user/http-urlrewrite.md (100%) rename site/content/en/{v0.3.0 => v0.3}/user/quickstart.md (96%) rename site/content/en/{v0.3.0 => v0.3}/user/rate-limit.md (98%) rename site/content/en/{v0.3.0 => v0.3}/user/secure-gateways.md (100%) rename site/content/en/{v0.3.0 => v0.3}/user/tcp-routing.md (100%) rename site/content/en/{v0.3.0 => v0.3}/user/tls-passthrough.md (100%) rename site/content/en/{v0.3.0 => v0.3}/user/udp-routing.md (98%) rename site/content/en/{v0.4.0 => v0.4}/_index.md (100%) rename site/content/en/{v0.5.0 => v0.4}/api/_index.md (100%) rename site/content/en/{v0.4.0 => v0.4}/api/config_types.md (99%) rename site/content/en/{v0.4.0 => v0.4}/api/extension_types.md (100%) rename site/content/en/{v0.4.0 => v0.4}/contributions/CODEOWNERS.md (100%) rename site/content/en/{v0.4.0 => v0.4}/contributions/CODE_OF_CONDUCT.md (100%) rename site/content/en/{v0.4.0 => v0.4}/contributions/CONTRIBUTING.md (97%) rename site/content/en/{v0.2.0 => v0.4}/contributions/DEVELOP.md (98%) rename site/content/en/{v0.4.0 => v0.4}/contributions/DOCS.md (100%) rename site/content/en/{v0.2.0 => v0.4}/contributions/RELEASING.md (98%) rename site/content/en/{v0.4.0 => v0.4}/contributions/_index.md (100%) rename site/content/en/{v0.4.0 => v0.4}/contributions/roadmap.md (100%) rename site/content/en/{v0.4.0 => v0.4}/design/_index.md (100%) rename site/content/en/{v0.4.0 => v0.4}/design/bootstrap.md (99%) rename site/content/en/{v0.4.0 => v0.4}/design/config-api.md (98%) rename site/content/en/{v0.4.0 => v0.4}/design/egctl.md (100%) rename site/content/en/{v0.4.0 => v0.4}/design/extending-envoy-gateway.md (98%) rename site/content/en/{v0.4.0 => v0.4}/design/gatewayapi-translator.md (100%) rename site/content/en/{v0.4.0 => v0.4}/design/goals.md (100%) rename site/content/en/{v0.4.0 => v0.4}/design/rate-limit.md (100%) rename site/content/en/{v0.4.0 => v0.4}/design/request-authentication.md (100%) rename site/content/en/{v0.4.0 => v0.4}/design/system-design.md (96%) rename site/content/en/{v0.4.0 => v0.4}/design/tcp-udp-design.md (100%) rename site/content/en/{v0.4.0 => v0.4}/design/watching.md (100%) rename site/content/en/{v0.4.0 => v0.4}/user/_index.md (100%) rename site/content/en/{v0.4.0 => v0.4}/user/authn.md (96%) rename site/content/en/{v0.4.0 => v0.4}/user/customize-envoyproxy.md (95%) rename site/content/en/{v0.4.0 => v0.4}/user/deployment-mode.md (100%) rename site/content/en/{v0.4.0 => v0.4}/user/egctl.md (99%) rename site/content/en/{v0.4.0 => v0.4}/user/gatewayapi-support.md (96%) rename site/content/en/{v0.4.0 => v0.4}/user/grpc-routing.md (100%) rename site/content/en/{v0.4.0 => v0.4}/user/http-redirect.md (100%) rename site/content/en/{v0.4.0 => v0.4}/user/http-request-headers.md (100%) rename site/content/en/{v0.4.0 => v0.4}/user/http-response-headers.md (100%) rename site/content/en/{v0.4.0 => v0.4}/user/http-routing.md (100%) rename site/content/en/{v0.4.0 => v0.4}/user/http-traffic-splitting.md (100%) rename site/content/en/{v0.4.0 => v0.4}/user/http-urlrewrite.md (100%) rename site/content/en/{v0.4.0 => v0.4}/user/quickstart.md (96%) rename site/content/en/{v0.4.0 => v0.4}/user/rate-limit.md (98%) rename site/content/en/{v0.4.0 => v0.4}/user/secure-gateways.md (100%) rename site/content/en/{v0.4.0 => v0.4}/user/tcp-routing.md (100%) rename site/content/en/{v0.4.0 => v0.4}/user/tls-passthrough.md (100%) rename site/content/en/{v0.4.0 => v0.4}/user/udp-routing.md (98%) rename site/content/en/{v0.5.0 => v0.5}/_index.md (100%) rename site/content/en/{v0.6.0 => v0.5}/api/_index.md (100%) rename site/content/en/{v0.5.0 => v0.5}/api/config_types.md (99%) rename site/content/en/{v0.5.0 => v0.5}/api/extension_types.md (100%) rename site/content/en/{v0.5.0 => v0.5}/contributions/CODEOWNERS.md (100%) rename site/content/en/{v0.5.0 => v0.5}/contributions/CODE_OF_CONDUCT.md (100%) rename site/content/en/{v0.3.0 => v0.5}/contributions/CONTRIBUTING.md (97%) rename site/content/en/{v0.5.0 => v0.5}/contributions/DEVELOP.md (98%) rename site/content/en/{v0.5.0 => v0.5}/contributions/DOCS.md (100%) rename site/content/en/{v0.5.0 => v0.5}/contributions/RELEASING.md (98%) rename site/content/en/{v0.5.0 => v0.5}/contributions/_index.md (100%) rename site/content/en/{v0.5.0 => v0.5}/contributions/roadmap.md (100%) rename site/content/en/{v0.5.0 => v0.5}/design/_index.md (100%) rename site/content/en/{v0.5.0 => v0.5}/design/accesslog.md (100%) rename site/content/en/{v0.5.0 => v0.5}/design/bootstrap.md (99%) rename site/content/en/{v0.5.0 => v0.5}/design/config-api.md (98%) rename site/content/en/{v0.5.0 => v0.5}/design/egctl.md (100%) rename site/content/en/{v0.5.0 => v0.5}/design/envoy-patch-policy.md (91%) rename site/content/en/{v0.5.0 => v0.5}/design/extending-envoy-gateway.md (98%) rename site/content/en/{v0.5.0 => v0.5}/design/gatewayapi-translator.md (100%) rename site/content/en/{v0.5.0 => v0.5}/design/goals.md (100%) rename site/content/en/{v0.5.0 => v0.5}/design/local-envoy-gateway.md (100%) rename site/content/en/{v0.5.0 => v0.5}/design/metrics.md (100%) rename site/content/en/{v0.5.0 => v0.5}/design/pprof.md (100%) rename site/content/en/{v0.5.0 => v0.5}/design/rate-limit.md (100%) rename site/content/en/{v0.5.0 => v0.5}/design/request-authentication.md (100%) rename site/content/en/{v0.5.0 => v0.5}/design/system-design.md (96%) rename site/content/en/{v0.5.0 => v0.5}/design/tcp-udp-design.md (100%) rename site/content/en/{v0.5.0 => v0.5}/design/tracing.md (100%) rename site/content/en/{v0.5.0 => v0.5}/design/watching.md (100%) rename site/content/en/{v0.6.0 => v0.5}/install/_index.md (100%) rename site/content/en/{v0.5.0 => v0.5}/install/api.md (100%) rename site/content/en/{v0.5.0 => v0.5}/install/install-egctl.md (96%) rename site/content/en/{v0.5.0 => v0.5}/install/install-helm.md (97%) rename site/content/en/{v0.5.0 => v0.5}/install/install-yaml.md (90%) rename site/content/en/{v0.5.0 => v0.5}/user/_index.md (100%) rename site/content/en/{v0.5.0 => v0.5}/user/authn.md (96%) rename site/content/en/{v0.5.0 => v0.5}/user/customize-envoyproxy.md (96%) rename site/content/en/{v0.5.0 => v0.5}/user/deployment-mode.md (100%) rename site/content/en/{v0.5.0 => v0.5}/user/egctl.md (99%) rename site/content/en/{v0.5.0 => v0.5}/user/envoy-patch-policy.md (94%) rename site/content/en/{v0.5.0 => v0.5}/user/gateway-address.md (100%) rename site/content/en/{v0.5.0 => v0.5}/user/gatewayapi-support.md (96%) rename site/content/en/{v0.5.0 => v0.5}/user/grpc-routing.md (100%) rename site/content/en/{v0.5.0 => v0.5}/user/http-redirect.md (100%) rename site/content/en/{v0.5.0 => v0.5}/user/http-request-headers.md (100%) rename site/content/en/{v0.5.0 => v0.5}/user/http-request-mirroring.md (100%) rename site/content/en/{v0.5.0 => v0.5}/user/http-response-headers.md (100%) rename site/content/en/{v0.5.0 => v0.5}/user/http-routing.md (100%) rename site/content/en/{v0.5.0 => v0.5}/user/http-traffic-splitting.md (100%) rename site/content/en/{v0.5.0 => v0.5}/user/http-urlrewrite.md (100%) rename site/content/en/{v0.5.0 => v0.5}/user/proxy-observability.md (100%) rename site/content/en/{v0.5.0 => v0.5}/user/quickstart.md (96%) rename site/content/en/{v0.5.0 => v0.5}/user/rate-limit.md (98%) rename site/content/en/{v0.5.0 => v0.5}/user/secure-gateways.md (100%) rename site/content/en/{v0.5.0 => v0.5}/user/tcp-routing.md (100%) rename site/content/en/{v0.5.0 => v0.5}/user/tls-cert-manager.md (100%) rename site/content/en/{v0.5.0 => v0.5}/user/tls-passthrough.md (100%) rename site/content/en/{v0.5.0 => v0.5}/user/tls-termination.md (100%) rename site/content/en/{v0.5.0 => v0.5}/user/udp-routing.md (98%) delete mode 100644 site/content/en/v0.6.0/contributions/CONTRIBUTING.md delete mode 100644 site/content/en/v0.6.0/contributions/DEVELOP.md rename site/content/en/{v0.6.0 => v0.6}/_index.md (100%) rename site/content/en/{v1.0.2 => v0.6}/api/_index.md (100%) rename site/content/en/{v0.6.0 => v0.6}/api/extension_types.md (99%) rename site/content/en/{v0.6.0 => v0.6}/contributions/CODEOWNERS.md (100%) rename site/content/en/{v0.6.0 => v0.6}/contributions/CODE_OF_CONDUCT.md (100%) create mode 100644 site/content/en/v0.6/contributions/CONTRIBUTING.md rename site/content/en/{v0.4.0 => v0.6}/contributions/DEVELOP.md (98%) rename site/content/en/{v0.6.0 => v0.6}/contributions/DOCS.md (100%) rename site/content/en/{v0.6.0 => v0.6}/contributions/RELEASING.md (97%) rename site/content/en/{v0.6.0 => v0.6}/contributions/_index.md (100%) rename site/content/en/{v0.6.0 => v0.6}/contributions/roadmap.md (100%) rename site/content/en/{v0.6.0 => v0.6}/design/_index.md (100%) rename site/content/en/{v0.6.0 => v0.6}/design/accesslog.md (100%) rename site/content/en/{v0.6.0 => v0.6}/design/backend-traffic-policy.md (100%) rename site/content/en/{v0.6.0 => v0.6}/design/bootstrap.md (99%) rename site/content/en/{v0.6.0 => v0.6}/design/client-traffic-policy.md (100%) rename site/content/en/{v0.6.0 => v0.6}/design/config-api.md (99%) rename site/content/en/{v0.6.0 => v0.6}/design/eg-metrics.md (100%) rename site/content/en/{v0.6.0 => v0.6}/design/egctl.md (100%) rename site/content/en/{v0.6.0 => v0.6}/design/envoy-patch-policy.md (95%) rename site/content/en/{v0.6.0 => v0.6}/design/extending-envoy-gateway.md (99%) rename site/content/en/{v0.6.0 => v0.6}/design/gatewayapi-translator.md (100%) rename site/content/en/{v0.6.0 => v0.6}/design/goals.md (100%) rename site/content/en/{v0.6.0 => v0.6}/design/local-envoy-gateway.md (100%) rename site/content/en/{v0.6.0 => v0.6}/design/metrics.md (100%) rename site/content/en/{v0.6.0 => v0.6}/design/pprof.md (100%) rename site/content/en/{v0.6.0 => v0.6}/design/rate-limit.md (100%) rename site/content/en/{v0.6.0 => v0.6}/design/security-policy.md (100%) rename site/content/en/{v0.6.0 => v0.6}/design/system-design.md (97%) rename site/content/en/{v0.6.0 => v0.6}/design/tcp-udp-design.md (100%) rename site/content/en/{v0.6.0 => v0.6}/design/tracing.md (100%) rename site/content/en/{v0.6.0 => v0.6}/design/watching.md (100%) rename site/content/en/{v1.0.2 => v0.6}/install/_index.md (100%) rename site/content/en/{v0.6.0 => v0.6}/install/api.md (100%) rename site/content/en/{v0.6.0 => v0.6}/install/install-egctl.md (100%) rename site/content/en/{v0.6.0 => v0.6}/install/install-helm.md (97%) rename site/content/en/{v0.6.0 => v0.6}/install/install-yaml.md (90%) rename site/content/en/{v0.6.0 => v0.6}/user/_index.md (100%) rename site/content/en/{v0.6.0 => v0.6}/user/cors.md (92%) rename site/content/en/{v0.6.0 => v0.6}/user/customize-envoyproxy.md (97%) rename site/content/en/{v0.6.0 => v0.6}/user/deployment-mode.md (100%) rename site/content/en/{v0.6.0 => v0.6}/user/egctl.md (99%) rename site/content/en/{v0.6.0 => v0.6}/user/envoy-patch-policy.md (93%) rename site/content/en/{v0.6.0 => v0.6}/user/gateway-address.md (100%) rename site/content/en/{v0.6.0 => v0.6}/user/gateway-api-metrics.md (91%) rename site/content/en/{v0.6.0 => v0.6}/user/gatewayapi-support.md (98%) rename site/content/en/{v0.6.0 => v0.6}/user/grafana-integration.md (71%) rename site/content/en/{v0.6.0 => v0.6}/user/grpc-routing.md (100%) rename site/content/en/{v0.6.0 => v0.6}/user/http-redirect.md (98%) rename site/content/en/{v0.6.0 => v0.6}/user/http-request-headers.md (98%) rename site/content/en/{v0.6.0 => v0.6}/user/http-request-mirroring.md (99%) rename site/content/en/{v0.6.0 => v0.6}/user/http-response-headers.md (98%) rename site/content/en/{v0.6.0 => v0.6}/user/http-routing.md (100%) rename site/content/en/{v0.6.0 => v0.6}/user/http-traffic-splitting.md (98%) rename site/content/en/{v0.6.0 => v0.6}/user/http-urlrewrite.md (98%) rename site/content/en/{v0.6.0 => v0.6}/user/jwt-authentication.md (94%) rename site/content/en/{v0.6.0 => v0.6}/user/multicluster-service.md (100%) rename site/content/en/{v0.6.0 => v0.6}/user/proxy-observability.md (96%) rename site/content/en/{v0.6.0 => v0.6}/user/quickstart.md (96%) rename site/content/en/{v0.6.0 => v0.6}/user/rate-limit.md (98%) rename site/content/en/{v0.6.0 => v0.6}/user/secure-gateways.md (89%) rename site/content/en/{v0.6.0 => v0.6}/user/tcp-routing.md (100%) rename site/content/en/{v0.6.0 => v0.6}/user/tls-cert-manager.md (99%) rename site/content/en/{v0.6.0 => v0.6}/user/tls-passthrough.md (94%) rename site/content/en/{v0.6.0 => v0.6}/user/tls-termination.md (96%) rename site/content/en/{v0.6.0 => v0.6}/user/udp-routing.md (96%) delete mode 100644 site/content/en/v1.0.2/releases/_index.md delete mode 100644 site/content/en/v1.0.2/releases/v0.1.0.md delete mode 100644 site/content/en/v1.0.2/releases/v0.2.0.md delete mode 100644 site/content/en/v1.0.2/releases/v0.3.0.md delete mode 100644 site/content/en/v1.0.2/releases/v0.4.0-rc.1.md delete mode 100644 site/content/en/v1.0.2/releases/v0.5.0.md delete mode 100644 site/content/en/v1.0.2/releases/v0.6.0-rc.1.md delete mode 100644 site/content/en/v1.0.2/releases/v1.0.1.md delete mode 100644 site/content/en/v1.0.2/releases/v1.0.2.md create mode 100644 site/content/en/v1.0/_index.md create mode 100644 site/content/en/v1.0/api/_index.md rename site/content/en/{v1.0.2 => v1.0}/api/extension_types.md (100%) create mode 100644 site/content/en/v1.0/install/_index.md rename site/content/en/{v1.0.2 => v1.0}/install/api.md (100%) rename site/content/en/{v1.0.2 => v1.0}/install/custom-cert.md (100%) create mode 100644 site/content/en/v1.0/install/install-egctl.md rename site/content/en/{v1.0.2 => v1.0}/install/install-helm.md (100%) rename site/content/en/{v1.0.2 => v1.0}/install/install-yaml.md (100%) create mode 100644 site/content/en/v1.0/tasks/_index.md rename site/content/en/{v1.0.2 => v1.0}/tasks/extensibility/_index.md (100%) rename site/content/en/{v1.0.2 => v1.0}/tasks/extensibility/envoy-patch-policy.md (100%) rename site/content/en/{v1.0.2 => v1.0}/tasks/observability/_index.md (100%) create mode 100644 site/content/en/v1.0/tasks/observability/gateway-api-metrics.md rename site/content/en/{v1.0.2 => v1.0}/tasks/observability/grafana-integration.md (81%) rename site/content/en/{v1.0.2 => v1.0}/tasks/observability/proxy-observability.md (100%) rename site/content/en/{v1.0.2 => v1.0}/tasks/operations/_index.md (100%) rename site/content/en/{v1.0.2 => v1.0}/tasks/operations/customize-envoyproxy.md (100%) rename site/content/en/{v1.0.2 => v1.0}/tasks/operations/deployment-mode.md (100%) rename site/content/en/{v1.0.2 => v1.0}/tasks/operations/egctl.md (100%) rename site/content/en/{v1.0.2 => v1.0}/tasks/quickstart.md (100%) rename site/content/en/{v1.0.2 => v1.0}/tasks/security/_index.md (100%) rename site/content/en/{v1.0.2 => v1.0}/tasks/security/backend-tls.md (96%) rename site/content/en/{v1.0.2 => v1.0}/tasks/security/basic-auth.md (100%) rename site/content/en/{v1.0.2 => v1.0}/tasks/security/cors.md (100%) rename site/content/en/{v1.0.2 => v1.0}/tasks/security/ext-auth.md (100%) rename site/content/en/{v1.0.2 => v1.0}/tasks/security/jwt-authentication.md (100%) rename site/content/en/{v1.0.2 => v1.0}/tasks/security/mutual-tls.md (100%) rename site/content/en/{v1.0.2 => v1.0}/tasks/security/oidc.md (100%) rename site/content/en/{v1.0.2 => v1.0}/tasks/security/secure-gateways.md (100%) rename site/content/en/{v1.0.2 => v1.0}/tasks/security/threat-model.md (98%) rename site/content/en/{v1.0.2 => v1.0}/tasks/security/tls-cert-manager.md (100%) rename site/content/en/{v1.0.2 => v1.0}/tasks/security/tls-passthrough.md (100%) rename site/content/en/{v1.0.2 => v1.0}/tasks/security/tls-termination.md (100%) create mode 100644 site/content/en/v1.0/tasks/traffic/_index.md rename site/content/en/{v1.0.2 => v1.0}/tasks/traffic/circuit-breaker.md (100%) rename site/content/en/{v1.0.2 => v1.0}/tasks/traffic/client-traffic-policy.md (100%) rename site/content/en/{v1.0.2 => v1.0}/tasks/traffic/fault-injection.md (100%) create mode 100644 site/content/en/v1.0/tasks/traffic/gateway-address.md rename site/content/en/{v1.0.2 => v1.0}/tasks/traffic/gatewayapi-support.md (100%) rename site/content/en/{v1.0.2 => v1.0}/tasks/traffic/global-rate-limit.md (100%) rename site/content/en/{v1.0.2 => v1.0}/tasks/traffic/grpc-routing.md (100%) create mode 100644 site/content/en/v1.0/tasks/traffic/http-redirect.md rename site/content/en/{v1.0.2 => v1.0}/tasks/traffic/http-request-headers.md (100%) rename site/content/en/{v1.0.2 => v1.0}/tasks/traffic/http-request-mirroring.md (100%) rename site/content/en/{v1.0.2 => v1.0}/tasks/traffic/http-response-headers.md (100%) rename site/content/en/{v1.0.2 => v1.0}/tasks/traffic/http-routing.md (100%) create mode 100644 site/content/en/v1.0/tasks/traffic/http-timeouts.md rename site/content/en/{v1.0.2 => v1.0}/tasks/traffic/http-traffic-splitting.md (100%) rename site/content/en/{v1.0.2 => v1.0}/tasks/traffic/http-urlrewrite.md (100%) rename site/content/en/{v1.0.2 => v1.0}/tasks/traffic/http3.md (100%) rename site/content/en/{v1.0.2 => v1.0}/tasks/traffic/local-rate-limit.md (100%) rename site/content/en/{v1.0.2 => v1.0}/tasks/traffic/multicluster-service.md (100%) rename site/content/en/{v1.0.2 => v1.0}/tasks/traffic/retry.md (100%) rename site/content/en/{v1.0.2 => v1.0}/tasks/traffic/routing-outside-kubernetes.md (100%) rename site/content/en/{v1.0.2 => v1.0}/tasks/traffic/tcp-routing.md (100%) rename site/content/en/{v1.0.2 => v1.0}/tasks/traffic/udp-routing.md (100%) create mode 100644 site/content/en/v1.1/_index.md create mode 100644 site/content/en/v1.1/api/_index.md create mode 100644 site/content/en/v1.1/api/extension_types.md create mode 100644 site/content/en/v1.1/boilerplates/index.md create mode 100644 site/content/en/v1.1/boilerplates/o11y_prerequisites.md create mode 100644 site/content/en/v1.1/concepts/_index.md create mode 100644 site/content/en/v1.1/concepts/concepts_overview.md create mode 100644 site/content/en/v1.1/install/_index.md create mode 100644 site/content/en/v1.1/install/custom-cert.md create mode 100644 site/content/en/v1.1/install/gateway-addons-helm-api.md create mode 100644 site/content/en/v1.1/install/gateway-helm-api.md create mode 100644 site/content/en/v1.1/install/install-egctl.md create mode 100644 site/content/en/v1.1/install/install-helm.md create mode 100644 site/content/en/v1.1/install/install-yaml.md create mode 100644 site/content/en/v1.1/tasks/_index.md create mode 100644 site/content/en/v1.1/tasks/extensibility/_index.md create mode 100644 site/content/en/v1.1/tasks/extensibility/build-wasm-image.md create mode 100644 site/content/en/v1.1/tasks/extensibility/envoy-patch-policy.md create mode 100644 site/content/en/v1.1/tasks/extensibility/ext-proc.md create mode 100644 site/content/en/v1.1/tasks/extensibility/extension-server.md create mode 100644 site/content/en/v1.1/tasks/extensibility/wasm.md create mode 100644 site/content/en/v1.1/tasks/observability/_index.md create mode 100644 site/content/en/v1.1/tasks/observability/gateway-api-metrics.md create mode 100644 site/content/en/v1.1/tasks/observability/gateway-exported-metrics.md create mode 100644 site/content/en/v1.1/tasks/observability/gateway-observability.md create mode 100644 site/content/en/v1.1/tasks/observability/grafana-integration.md create mode 100644 site/content/en/v1.1/tasks/observability/proxy-accesslog.md create mode 100644 site/content/en/v1.1/tasks/observability/proxy-metric.md create mode 100644 site/content/en/v1.1/tasks/observability/proxy-trace.md create mode 100644 site/content/en/v1.1/tasks/observability/rate-limit-observability.md create mode 100644 site/content/en/v1.1/tasks/operations/_index.md create mode 100644 site/content/en/v1.1/tasks/operations/customize-envoyproxy.md create mode 100644 site/content/en/v1.1/tasks/operations/deployment-mode.md create mode 100644 site/content/en/v1.1/tasks/operations/egctl.md create mode 100644 site/content/en/v1.1/tasks/quickstart.md create mode 100644 site/content/en/v1.1/tasks/security/_index.md create mode 100644 site/content/en/v1.1/tasks/security/backend-mtls.md create mode 100644 site/content/en/v1.1/tasks/security/backend-tls.md create mode 100644 site/content/en/v1.1/tasks/security/basic-auth.md create mode 100644 site/content/en/v1.1/tasks/security/cors.md create mode 100644 site/content/en/v1.1/tasks/security/ext-auth.md create mode 100644 site/content/en/v1.1/tasks/security/jwt-authentication.md create mode 100644 site/content/en/v1.1/tasks/security/mutual-tls.md create mode 100644 site/content/en/v1.1/tasks/security/oidc.md create mode 100644 site/content/en/v1.1/tasks/security/private-key-provider.md create mode 100644 site/content/en/v1.1/tasks/security/restrict-ip-access.md create mode 100644 site/content/en/v1.1/tasks/security/secure-gateways.md create mode 100644 site/content/en/v1.1/tasks/security/threat-model.md create mode 100644 site/content/en/v1.1/tasks/security/tls-cert-manager.md create mode 100644 site/content/en/v1.1/tasks/security/tls-passthrough.md create mode 100644 site/content/en/v1.1/tasks/security/tls-termination.md create mode 100644 site/content/en/v1.1/tasks/traffic/_index.md create mode 100644 site/content/en/v1.1/tasks/traffic/backend.md create mode 100644 site/content/en/v1.1/tasks/traffic/circuit-breaker.md create mode 100644 site/content/en/v1.1/tasks/traffic/client-traffic-policy.md create mode 100644 site/content/en/v1.1/tasks/traffic/connection-limit.md create mode 100644 site/content/en/v1.1/tasks/traffic/fault-injection.md create mode 100644 site/content/en/v1.1/tasks/traffic/gateway-address.md create mode 100644 site/content/en/v1.1/tasks/traffic/gatewayapi-support.md create mode 100644 site/content/en/v1.1/tasks/traffic/global-rate-limit.md create mode 100644 site/content/en/v1.1/tasks/traffic/grpc-routing.md create mode 100644 site/content/en/v1.1/tasks/traffic/http-redirect.md create mode 100644 site/content/en/v1.1/tasks/traffic/http-request-headers.md create mode 100644 site/content/en/v1.1/tasks/traffic/http-request-mirroring.md create mode 100644 site/content/en/v1.1/tasks/traffic/http-response-headers.md create mode 100644 site/content/en/v1.1/tasks/traffic/http-routing.md create mode 100644 site/content/en/v1.1/tasks/traffic/http-timeouts.md create mode 100644 site/content/en/v1.1/tasks/traffic/http-traffic-splitting.md create mode 100644 site/content/en/v1.1/tasks/traffic/http-urlrewrite.md create mode 100644 site/content/en/v1.1/tasks/traffic/http3.md create mode 100644 site/content/en/v1.1/tasks/traffic/load-balancing.md create mode 100644 site/content/en/v1.1/tasks/traffic/local-rate-limit.md create mode 100644 site/content/en/v1.1/tasks/traffic/multicluster-service.md create mode 100644 site/content/en/v1.1/tasks/traffic/retry.md create mode 100644 site/content/en/v1.1/tasks/traffic/routing-outside-kubernetes.md create mode 100644 site/content/en/v1.1/tasks/traffic/tcp-routing.md create mode 100644 site/content/en/v1.1/tasks/traffic/udp-routing.md delete mode 100644 site/static/img/envoy-gateway-resources-dashboard.png create mode 100644 site/static/img/envoy-gateway-resources-overview.png delete mode 100644 site/static/img/envoy-pod-resources-dashboard.png rename site/static/img/{envoy-global-dashboard.png => envoy-proxy-global-dashboard.png} (100%) create mode 100644 site/static/img/resources-monitor-dashboard.png create mode 100644 site/static/img/wasm-extension.png delete mode 100644 test/benchmark/benchmark_report.md rename test/benchmark/suite/{test.go => options.go} (79%) create mode 100644 test/e2e/testdata/client-mtls.yaml create mode 100644 test/e2e/testdata/load_balancing_consistent_hash_cookie.yaml create mode 100644 test/e2e/testdata/load_balancing_consistent_hash_header.yaml create mode 100644 test/e2e/testdata/load_balancing_consistent_hash_source_ip.yaml create mode 100644 test/e2e/testdata/load_balancing_round_robin.yaml create mode 100644 test/e2e/testdata/securitypolicy-translation-failed.yaml rename test/e2e/tests/{authorization-client-ip.go => authorization_client_ip.go} (70%) rename test/e2e/tests/{authorization-default-action.go => authorization_default_action.go} (77%) create mode 100644 test/e2e/tests/client_mtls.go rename test/e2e/tests/{httproute-rewrite-full-path.go => httproute_rewrite_full_path.go} (100%) create mode 100644 test/e2e/tests/load_balancing.go rename test/e2e/tests/{multiple-gc.go => multiple_gc.go} (100%) create mode 100644 test/e2e/tests/securitypolicy_transaltion_failed.go create mode 100644 test/e2e/upgrade/manifests.yaml rename test/{benchmark/suite => utils/kubernetes}/client.go (71%) rename test/{e2e => }/utils/prometheus/prometheus.go (50%) create mode 100644 tools/osv-scanner/config.toml create mode 100644 tools/src/gci/go.mod create mode 100644 tools/src/gci/go.sum create mode 100644 tools/src/gci/pin.go create mode 100644 tools/src/jb/go.mod create mode 100644 tools/src/jb/go.sum create mode 100644 tools/src/jb/pin.go create mode 100644 tools/src/jsonnet/go.mod create mode 100644 tools/src/jsonnet/go.sum create mode 100644 tools/src/jsonnet/pin.go delete mode 100644 tools/src/sphinx-build/requirements.txt diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml index 8ab5cc2c264..0a367a06d65 100644 --- a/.github/workflows/build_and_test.yaml +++ b/.github/workflows/build_and_test.yaml @@ -18,7 +18,7 @@ permissions: jobs: lint: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - uses: ./tools/github-actions/setup-deps @@ -137,6 +137,8 @@ jobs: benchmark-test: runs-on: ubuntu-latest + # There's a different workflow for benchmark-test on push. + if: ${{ ! startsWith(github.event_name, 'push') }} needs: [build] steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 @@ -151,12 +153,13 @@ jobs: BENCHMARK_RPS: 10000 BENCHMARK_CONNECTIONS: 100 BENCHMARK_DURATION: 30 - BENCHMARK_CPU_LIMITS: 1000 - BENCHMARK_MEMORY_LIMITS: 2000 + BENCHMARK_CPU_LIMITS: 1000m + BENCHMARK_MEMORY_LIMITS: 2000Mi + BENCHMARK_REPORT_DIR: benchmark_report run: make benchmark - name: Read Benchmark report - run: cat test/benchmark/benchmark_report.md + run: cat test/benchmark/benchmark_report/benchmark_report.md publish: runs-on: ubuntu-latest diff --git a/.github/workflows/cherrypick.yaml b/.github/workflows/cherrypick.yaml deleted file mode 100644 index 4de9cbbdf5d..00000000000 --- a/.github/workflows/cherrypick.yaml +++ /dev/null @@ -1,34 +0,0 @@ -name: Cherry pick -on: - pull_request: - branches: - - main - types: ["closed"] - -permissions: - contents: read - -jobs: - cherry_pick_release_v1_0: - permissions: - pull-requests: write - contents: write - runs-on: ubuntu-22.04 - name: Cherry pick into release-v1.0 - if: ${{ contains(github.event.pull_request.labels.*.name, 'cherrypick/release-v1.0') && github.event.pull_request.merged == true }} - steps: - - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - with: - fetch-depth: 0 - - name: Cherry pick into release/v1.0 - uses: carloscastrojumo/github-cherry-pick-action@503773289f4a459069c832dc628826685b75b4b3 # v1.0.10 - with: - branch: release/v1.0 - title: "[release/v1.0] {old_title}" - body: "Cherry picking #{old_pull_request_id} onto release/v1.0" - labels: | - cherrypick/release-v1.0 - # put release manager here - reviewers: | - Xunzhuo diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index da76ee7aea0..ec8557b71cd 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -36,14 +36,14 @@ jobs: - uses: ./tools/github-actions/setup-deps - name: Initialize CodeQL - uses: github/codeql-action/init@b611370bb5703a7efb587f9d136a52ea24c5c38c # v3.25.11 + uses: github/codeql-action/init@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12 with: languages: ${{ matrix.language }} - name: Autobuild - uses: github/codeql-action/autobuild@b611370bb5703a7efb587f9d136a52ea24c5c38c # v3.25.11 + uses: github/codeql-action/autobuild@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@b611370bb5703a7efb587f9d136a52ea24c5c38c # v3.25.11 + uses: github/codeql-action/analyze@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index efb18d77b56..0b4cb36ecf6 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -62,7 +62,7 @@ jobs: extended: true - name: Setup Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.1.0 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.1.0 with: node-version: '18' diff --git a/.github/workflows/latest_release.yaml b/.github/workflows/latest_release.yaml index 66ebc5def70..b08ea0d69d9 100644 --- a/.github/workflows/latest_release.yaml +++ b/.github/workflows/latest_release.yaml @@ -17,8 +17,40 @@ concurrency: cancel-in-progress: true jobs: + # For push event, we run benchmark test here because we need to + # include benchmark report in the latest release. + benchmark-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: ./tools/github-actions/setup-deps + + # Benchmark + - name: Run Benchmark tests + env: + KIND_NODE_TAG: v1.28.9 + IMAGE_PULL_POLICY: IfNotPresent + # Args for benchmark test + BENCHMARK_RPS: 10000 + BENCHMARK_CONNECTIONS: 100 + BENCHMARK_DURATION: 30 + BENCHMARK_CPU_LIMITS: 1000m + BENCHMARK_MEMORY_LIMITS: 2000Mi + BENCHMARK_REPORT_DIR: benchmark_report + run: make benchmark + + - name: Package benchmark report + run: cd test/benchmark && zip -r benchmark_report.zip benchmark_report + + - name: Upload Benchmark Report + uses: actions/upload-artifact@v4 # version is better be consistent with actions/download-artifact + with: + name: benchmark_report + path: test/benchmark/benchmark_report.zip + latest-release: runs-on: ubuntu-22.04 + needs: [benchmark-test] permissions: contents: write steps: @@ -29,6 +61,12 @@ jobs: # Use `Always` image pull policy for latest version. run: IMAGE_PULL_POLICY=Always make generate-manifests IMAGE=envoyproxy/gateway-dev TAG=latest OUTPUT_DIR=release-artifacts + - name: Download Benchmark Report + uses: actions/download-artifact@v4 + with: + name: benchmark_report + path: release-artifacts + - name: Build egctl latest multiarch binaries run: | make build-multiarch BINS="egctl" @@ -70,6 +108,7 @@ jobs: files: | release-artifacts/install.yaml release-artifacts/quickstart.yaml + release-artifacts/benchmark_report.zip egctl_latest_linux_amd64.tar.gz egctl_latest_linux_arm64.tar.gz egctl_latest_darwin_amd64.tar.gz diff --git a/.github/workflows/license-scan.yml b/.github/workflows/license-scan.yml index d4ecceec165..31014adf8a5 100644 --- a/.github/workflows/license-scan.yml +++ b/.github/workflows/license-scan.yml @@ -18,7 +18,7 @@ jobs: - name: Checkout code uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Run scanner - uses: google/osv-scanner-action/osv-scanner-action@3c399db9dd6dd8106a27d280d53c55077d3f7cea # v1.8.1 + uses: google/osv-scanner-action/osv-scanner-action@7ac94f9d40028db4cacf8d53adec6626f5d3d2f7 # v1.8.2 with: scan-args: |- --skip-git diff --git a/.github/workflows/osv-scanner.yml b/.github/workflows/osv-scanner.yml index 63f546be8c2..424ff41a189 100644 --- a/.github/workflows/osv-scanner.yml +++ b/.github/workflows/osv-scanner.yml @@ -16,16 +16,31 @@ on: jobs: scan-scheduled: if: ${{ github.event_name == 'push' || github.event_name == 'schedule' }} - uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@3c399db9dd6dd8106a27d280d53c55077d3f7cea" # v1.8.1 + uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@7ac94f9d40028db4cacf8d53adec6626f5d3d2f7" # v1.8.2 permissions: actions: read contents: read # Require writing security events to upload SARIF file to security tab security-events: write + with: + scan-args: |- + --skip-git + --recursive + ./ + --config + tools/osv-scanner/config.toml + scan-pr: if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }} - uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@3c399db9dd6dd8106a27d280d53c55077d3f7cea" # v1.8.1 + uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@7ac94f9d40028db4cacf8d53adec6626f5d3d2f7" # v1.8.2 permissions: actions: read contents: read security-events: write + with: + scan-args: |- + --skip-git + --recursive + ./ + --config + tools/osv-scanner/config.toml diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 679c6184e39..2243a4acc78 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -10,8 +10,40 @@ on: - "v*.*.*" jobs: + # For push event, we run benchmark test here because we need to + # include benchmark report in the release. + benchmark-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: ./tools/github-actions/setup-deps + + # Benchmark + - name: Run Benchmark tests + env: + KIND_NODE_TAG: v1.28.9 + IMAGE_PULL_POLICY: IfNotPresent + # Args for benchmark test + BENCHMARK_RPS: 10000 + BENCHMARK_CONNECTIONS: 100 + BENCHMARK_DURATION: 30 + BENCHMARK_CPU_LIMITS: 1000m + BENCHMARK_MEMORY_LIMITS: 2000Mi + BENCHMARK_REPORT_DIR: benchmark_report + run: make benchmark + + - name: Package benchmark report + run: cd test/benchmark && zip -r benchmark_report.zip benchmark_report + + - name: Upload Benchmark Report + uses: actions/upload-artifact@v4 # version is better be consistent with actions/download-artifact + with: + name: benchmark_report + path: test/benchmark/benchmark_report.zip + release: runs-on: ubuntu-22.04 + needs: [benchmark-test] permissions: contents: write steps: @@ -39,6 +71,12 @@ jobs: - name: Build and Push EG Release Helm Chart run: IMAGE_PULL_POLICY=IfNotPresent OCI_REGISTRY=oci://docker.io/envoyproxy CHART_VERSION=${{ env.release_tag }} IMAGE=docker.io/envoyproxy/gateway TAG=${{ env.release_tag }} make helm-package helm-push + - name: Download Benchmark Report + uses: actions/download-artifact@v4 + with: + name: benchmark_report + path: release-artifacts + - name: Upload Release Manifests uses: softprops/action-gh-release@a74c6b72af54cfa997e81df42d94703d6313a2d0 # v0.1.15 with: @@ -46,6 +84,7 @@ jobs: release-artifacts/install.yaml release-artifacts/quickstart.yaml release-artifacts/release-notes.yaml + release-artifacts/benchmark_report.zip release-artifacts/egctl_${{ env.release_tag }}_linux_amd64.tar.gz release-artifacts/egctl_${{ env.release_tag }}_linux_arm64.tar.gz release-artifacts/egctl_${{ env.release_tag }}_darwin_amd64.tar.gz diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index a7b7847bf66..c7e9b20cdad 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -40,6 +40,6 @@ jobs: retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@b611370bb5703a7efb587f9d136a52ea24c5c38c # v3.25.11 + uses: github/codeql-action/upload-sarif@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12 with: sarif_file: results.sarif diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index e86341238ac..24570e7f064 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -25,7 +25,7 @@ jobs: IMAGE=envoy-proxy/gateway-dev TAG=${{ github.sha }} make image - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@7c2007bcb556501da015201bcba5aa14069b74e2 # v0.23.0 + uses: aquasecurity/trivy-action@6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8 # v0.24.0 with: image-ref: envoy-proxy/gateway-dev:${{ github.sha }} exit-code: '1' diff --git a/.gitignore b/.gitignore index 5bf0184010e..22f749c8c38 100644 --- a/.gitignore +++ b/.gitignore @@ -35,5 +35,10 @@ charts/gateway-helm/values.yaml # dependency charts generated by addons helm. charts/gateway-addons-helm/charts/ +# vendor for grafonnet +charts/gateway-addons-helm/dashboards/vendor/ + # VIM .*.swp + +*.tar.gz diff --git a/OWNERS b/OWNERS index a8b84e2cc3d..9237b007189 100644 --- a/OWNERS +++ b/OWNERS @@ -13,9 +13,18 @@ maintainers: - arkodg - Xunzhuo - zirain -- qicz - zhaohuabing - guydc +- shawnh2 + +emeritus-maintainers: + +- alexgervais +- danehans +- LukeShu +- skriss +- youngnick +- qicz reviewers: @@ -24,5 +33,4 @@ reviewers: - tmsnan - tanujd11 - cnvergence -- shawnh2 - liorokman diff --git a/VERSION b/VERSION index 564e7810ec8..795460fcec8 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v1.1.0-rc.1 +v1.1.0 diff --git a/api/v1alpha1/accesslogging_types.go b/api/v1alpha1/accesslogging_types.go index 765fa4de7bc..24272564488 100644 --- a/api/v1alpha1/accesslogging_types.go +++ b/api/v1alpha1/accesslogging_types.go @@ -20,7 +20,7 @@ type ProxyAccessLogSetting struct { // Format defines the format of accesslog. // This will be ignored if sink type is ALS. // +optional - Format *ProxyAccessLogFormat `json:"format"` + Format *ProxyAccessLogFormat `json:"format,omitempty"` // Matches defines the match conditions for accesslog in CEL expression. // An accesslog will be emitted only when one or more match conditions are evaluated to true. // Invalid [CEL](https://www.envoyproxy.io/docs/envoy/latest/xds/type/v3/cel.proto.html#common-expression-language-cel-proto) expressions will be ignored. diff --git a/api/v1alpha1/backendtrafficpolicy_types.go b/api/v1alpha1/backendtrafficpolicy_types.go index 8c23b133231..f484f44b409 100644 --- a/api/v1alpha1/backendtrafficpolicy_types.go +++ b/api/v1alpha1/backendtrafficpolicy_types.go @@ -34,7 +34,7 @@ type BackendTrafficPolicy struct { Status gwapiv1a2.PolicyStatus `json:"status,omitempty"` } -// +kubebuilder:validation:XValidation:rule="(has(self.targetRef) && !has(self.targetRefs)) || (!has(self.targetRef) && has(self.targetRefs))", message="either targetRef or targetRefs must be used" +// +kubebuilder:validation:XValidation:rule="(has(self.targetRef) && !has(self.targetRefs)) || (!has(self.targetRef) && has(self.targetRefs)) || (has(self.targetSelectors) && self.targetSelectors.size() > 0) ", message="either targetRef or targetRefs must be used" // // +kubebuilder:validation:XValidation:rule="has(self.targetRef) ? self.targetRef.group == 'gateway.networking.k8s.io' : true ", message="this policy can only have a targetRef.group of gateway.networking.k8s.io" // +kubebuilder:validation:XValidation:rule="has(self.targetRef) ? self.targetRef.kind in ['Gateway', 'HTTPRoute', 'GRPCRoute', 'UDPRoute', 'TCPRoute', 'TLSRoute'] : true", message="this policy can only have a targetRef.kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute/UDPRoute/TLSRoute" diff --git a/api/v1alpha1/clienttrafficpolicy_types.go b/api/v1alpha1/clienttrafficpolicy_types.go index 20953b1960b..397535ebf43 100644 --- a/api/v1alpha1/clienttrafficpolicy_types.go +++ b/api/v1alpha1/clienttrafficpolicy_types.go @@ -34,7 +34,7 @@ type ClientTrafficPolicy struct { Status gwapiv1a2.PolicyStatus `json:"status,omitempty"` } -// +kubebuilder:validation:XValidation:rule="(has(self.targetRef) && !has(self.targetRefs)) || (!has(self.targetRef) && has(self.targetRefs))", message="either targetRef or targetRefs must be used" +// +kubebuilder:validation:XValidation:rule="(has(self.targetRef) && !has(self.targetRefs)) || (!has(self.targetRef) && has(self.targetRefs)) || (has(self.targetSelectors) && self.targetSelectors.size() > 0) ", message="either targetRef or targetRefs must be used" // // +kubebuilder:validation:XValidation:rule="has(self.targetRef) ? self.targetRef.group == 'gateway.networking.k8s.io' : true", message="this policy can only have a targetRef.group of gateway.networking.k8s.io" // +kubebuilder:validation:XValidation:rule="has(self.targetRef) ? self.targetRef.kind == 'Gateway' : true", message="this policy can only have a targetRef.kind of Gateway" diff --git a/api/v1alpha1/envoyextensionypolicy_types.go b/api/v1alpha1/envoyextensionypolicy_types.go index b88cdcf0d32..cbab194b24e 100644 --- a/api/v1alpha1/envoyextensionypolicy_types.go +++ b/api/v1alpha1/envoyextensionypolicy_types.go @@ -32,7 +32,7 @@ type EnvoyExtensionPolicy struct { Status gwapiv1a2.PolicyStatus `json:"status,omitempty"` } -// +kubebuilder:validation:XValidation:rule="(has(self.targetRef) && !has(self.targetRefs)) || (!has(self.targetRef) && has(self.targetRefs))", message="either targetRef or targetRefs must be used" +// +kubebuilder:validation:XValidation:rule="(has(self.targetRef) && !has(self.targetRefs)) || (!has(self.targetRef) && has(self.targetRefs)) || (has(self.targetSelectors) && self.targetSelectors.size() > 0) ", message="either targetRef or targetRefs must be used" // // +kubebuilder:validation:XValidation:rule="has(self.targetRef) ? self.targetRef.group == 'gateway.networking.k8s.io' : true", message="this policy can only have a targetRef.group of gateway.networking.k8s.io" // +kubebuilder:validation:XValidation:rule="has(self.targetRef) ? self.targetRef.kind in ['Gateway', 'HTTPRoute', 'GRPCRoute', 'UDPRoute', 'TCPRoute', 'TLSRoute'] : true", message="this policy can only have a targetRef.kind of Gateway/HTTPRoute/GRPCRoute/TCPRoute/UDPRoute/TLSRoute" diff --git a/api/v1alpha1/loadbalancer_types.go b/api/v1alpha1/loadbalancer_types.go index 3fd144bdfa1..d4d0925d619 100644 --- a/api/v1alpha1/loadbalancer_types.go +++ b/api/v1alpha1/loadbalancer_types.go @@ -58,7 +58,10 @@ const ( // +kubebuilder:validation:XValidation:rule="self.type == 'Header' ? has(self.header) : !has(self.header)",message="If consistent hash type is header, the header field must be set." // +kubebuilder:validation:XValidation:rule="self.type == 'Cookie' ? has(self.cookie) : !has(self.cookie)",message="If consistent hash type is cookie, the cookie field must be set." type ConsistentHash struct { - // ConsistentHashType defines the type of input to hash on. Valid Type values are "SourceIP" or "Header". + // ConsistentHashType defines the type of input to hash on. Valid Type values are + // "SourceIP", + // "Header", + // "Cookie". // // +unionDiscriminator Type ConsistentHashType `json:"type"` diff --git a/api/v1alpha1/securitypolicy_types.go b/api/v1alpha1/securitypolicy_types.go index c0cb7543a45..61277ba3734 100644 --- a/api/v1alpha1/securitypolicy_types.go +++ b/api/v1alpha1/securitypolicy_types.go @@ -33,7 +33,7 @@ type SecurityPolicy struct { Status gwapiv1a2.PolicyStatus `json:"status,omitempty"` } -// +kubebuilder:validation:XValidation:rule="(has(self.targetRef) && !has(self.targetRefs)) || (!has(self.targetRef) && has(self.targetRefs))", message="either targetRef or targetRefs must be used" +// +kubebuilder:validation:XValidation:rule="(has(self.targetRef) && !has(self.targetRefs)) || (!has(self.targetRef) && has(self.targetRefs)) || (has(self.targetSelectors) && self.targetSelectors.size() > 0) ", message="either targetRef or targetRefs must be used" // // +kubebuilder:validation:XValidation:rule="has(self.targetRef) ? self.targetRef.group == 'gateway.networking.k8s.io' : true", message="this policy can only have a targetRef.group of gateway.networking.k8s.io" // +kubebuilder:validation:XValidation:rule="has(self.targetRef) ? self.targetRef.kind in ['Gateway', 'HTTPRoute', 'GRPCRoute'] : true", message="this policy can only have a targetRef.kind of Gateway/HTTPRoute/GRPCRoute" diff --git a/charts/gateway-addons-helm/.helmignore b/charts/gateway-addons-helm/.helmignore index 0e8a0eb36f4..09a09c71149 100644 --- a/charts/gateway-addons-helm/.helmignore +++ b/charts/gateway-addons-helm/.helmignore @@ -21,3 +21,8 @@ .idea/ *.tmproj .vscode/ +# Vendor and configs for Jsonnet and Grafonnet +dashboards/lib/ +dashboards/vendor/ +dashboards/jsonnetfile.json +dashboards/jsonnetfile.lock.json diff --git a/charts/gateway-addons-helm/README.md b/charts/gateway-addons-helm/README.md index dc364b5d1a9..43082172180 100644 --- a/charts/gateway-addons-helm/README.md +++ b/charts/gateway-addons-helm/README.md @@ -61,6 +61,7 @@ To uninstall the chart: | fluent-bit.config.service | string | `"[SERVICE]\n Daemon Off\n Flush {{ .Values.flush }}\n Log_Level {{ .Values.logLevel }}\n Parsers_File parsers.conf\n Parsers_File custom_parsers.conf\n HTTP_Server On\n HTTP_Listen 0.0.0.0\n HTTP_Port {{ .Values.metricsPort }}\n Health_Check On\n"` | | | fluent-bit.enabled | bool | `true` | | | fluent-bit.fullnameOverride | string | `"fluent-bit"` | | +| fluent-bit.image.repository | string | `"fluent/fluent-bit"` | | | fluent-bit.podAnnotations."fluentbit.io/exclude" | string | `"true"` | | | fluent-bit.podAnnotations."prometheus.io/path" | string | `"/api/v1/metrics/prometheus"` | | | fluent-bit.podAnnotations."prometheus.io/port" | string | `"2020"` | | diff --git a/charts/gateway-addons-helm/dashboards/envoy-gateway-resource.json b/charts/gateway-addons-helm/dashboards/envoy-gateway-resource.json deleted file mode 100644 index ff984728c8d..00000000000 --- a/charts/gateway-addons-helm/dashboards/envoy-gateway-resource.json +++ /dev/null @@ -1,262 +0,0 @@ -{ - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": { - "type": "grafana", - "uid": "-- Grafana --" - }, - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - } - ] - }, - "description": "Envoy Gateway Memory and CPU Usage", - "editable": true, - "fiscalYearStartMonth": 0, - "graphTooltip": 0, - "links": [], - "panels": [ - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 0 - }, - "id": 5, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "maxHeight": 600, - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "PBFA97CFB590B2093" - }, - "disableTextWrap": false, - "editorMode": "code", - "expr": "sum by(namespace) (container_memory_working_set_bytes{container=\"envoy-gateway\"}/1024/1024)", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "__auto", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Envoy Gateway Memory Usage (MiB)", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 0 - }, - "id": 6, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "maxHeight": 600, - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "PBFA97CFB590B2093" - }, - "disableTextWrap": false, - "editorMode": "code", - "expr": "sum by(namespace) (rate(container_cpu_usage_seconds_total{container=\"envoy-gateway\"}[5m]) * 1000)", - "fullMetaSearch": false, - "includeNullMetadata": false, - "instant": false, - "legendFormat": "__auto", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Envoy Gateway CPU Time (ms)", - "type": "timeseries" - } - ], - "schemaVersion": 39, - "tags": [ - "Control Plane" - ], - "templating": { - "list": [ - { - "current": { - "selected": false, - "text": "Prometheus", - "value": "PBFA97CFB590B2093" - }, - "hide": 0, - "includeAll": false, - "multi": false, - "name": "datasource", - "options": [], - "query": "prometheus", - "queryValue": "", - "refresh": 1, - "regex": "", - "skipUrlSync": false, - "type": "datasource" - } - ] - }, - "time": { - "from": "now-15m", - "to": "now" - }, - "timeRangeUpdatedDuringEditOrView": false, - "timepicker": {}, - "timezone": "", - "title": "Envoy Gateway Resources", - "uid": "edq1b2tldspa8d", - "version": 1, - "weekStart": "" -} \ No newline at end of file diff --git a/charts/gateway-addons-helm/dashboards/envoy-pod-resource.json b/charts/gateway-addons-helm/dashboards/envoy-pod-resource.json deleted file mode 100644 index b01caf31100..00000000000 --- a/charts/gateway-addons-helm/dashboards/envoy-pod-resource.json +++ /dev/null @@ -1,255 +0,0 @@ -{ - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": { - "type": "grafana", - "uid": "-- Grafana --" - }, - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - } - ] - }, - "description": "Envoy Pod Memory and CPU Usage", - "editable": true, - "fiscalYearStartMonth": 0, - "graphTooltip": 0, - "id": 4, - "links": [], - "liveNow": false, - "panels": [ - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 0 - }, - "id": 2, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "maxHeight": 600, - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "editorMode": "code", - "exemplar": false, - "expr": "sum by(pod) (container_memory_working_set_bytes{container=~\"envoy\"}/1000000)", - "instant": false, - "range": true, - "refId": "A" - } - ], - "title": "Memory Working Set Envoy Pods(mb)", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 0 - }, - "id": 1, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "maxHeight": 600, - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "PBFA97CFB590B2093" - }, - "editorMode": "builder", - "expr": "sum by(pod) (rate(container_cpu_usage_seconds_total{container=\"envoy\"}[5m]))", - "instant": false, - "range": true, - "refId": "A" - } - ], - "title": "CPU Usage Envoy Pods", - "type": "timeseries" - } - ], - "refresh": "", - "schemaVersion": 39, - "tags": [ - "Data Plane" - ], - "templating": { - "list": [ - { - "current": { - "selected": false, - "text": "Prometheus", - "value": "PBFA97CFB590B2093" - }, - "hide": 0, - "includeAll": false, - "multi": false, - "name": "datasource", - "options": [], - "query": "prometheus", - "refresh": 1, - "regex": "", - "skipUrlSync": false, - "type": "datasource" - } - ] - }, - "time": { - "from": "now-5m", - "to": "now" - }, - "timeRangeUpdatedDuringEditOrView": false, - "timepicker": {}, - "timezone": "", - "title": "Envoy Pod Resources", - "uid": "f2279235-80b7-4c85-84f4-f25a3bf3eac0", - "version": 1, - "weekStart": "" -} \ No newline at end of file diff --git a/charts/gateway-addons-helm/dashboards/envoy-global.json b/charts/gateway-addons-helm/dashboards/envoy-proxy-global.json similarity index 100% rename from charts/gateway-addons-helm/dashboards/envoy-global.json rename to charts/gateway-addons-helm/dashboards/envoy-proxy-global.json diff --git a/charts/gateway-addons-helm/dashboards/jsonnetfile.json b/charts/gateway-addons-helm/dashboards/jsonnetfile.json new file mode 100644 index 00000000000..2414c867194 --- /dev/null +++ b/charts/gateway-addons-helm/dashboards/jsonnetfile.json @@ -0,0 +1,15 @@ +{ + "version": 1, + "dependencies": [ + { + "source": { + "git": { + "remote": "https://github.com/grafana/grafonnet.git", + "subdir": "gen/grafonnet-latest" + } + }, + "version": "main" + } + ], + "legacyImports": true +} diff --git a/charts/gateway-addons-helm/dashboards/jsonnetfile.lock.json b/charts/gateway-addons-helm/dashboards/jsonnetfile.lock.json new file mode 100644 index 00000000000..2700d2b1e6b --- /dev/null +++ b/charts/gateway-addons-helm/dashboards/jsonnetfile.lock.json @@ -0,0 +1,46 @@ +{ + "version": 1, + "dependencies": [ + { + "source": { + "git": { + "remote": "https://github.com/grafana/grafonnet.git", + "subdir": "gen/grafonnet-latest" + } + }, + "version": "119d65363dff84a1976bba609f2ac3a8f450e760", + "sum": "eyuJ0jOXeA4MrobbNgU4/v5a7ASDHslHZ0eS6hDdWoI=" + }, + { + "source": { + "git": { + "remote": "https://github.com/grafana/grafonnet.git", + "subdir": "gen/grafonnet-v11.0.0" + } + }, + "version": "119d65363dff84a1976bba609f2ac3a8f450e760", + "sum": "Fuo+qTZZzF+sHDBWX/8fkPsUmwW6qhH8hRVz45HznfI=" + }, + { + "source": { + "git": { + "remote": "https://github.com/jsonnet-libs/docsonnet.git", + "subdir": "doc-util" + } + }, + "version": "6ac6c69685b8c29c54515448eaca583da2d88150", + "sum": "BrAL/k23jq+xy9oA7TWIhUx07dsA/QLm3g7ktCwe//U=" + }, + { + "source": { + "git": { + "remote": "https://github.com/jsonnet-libs/xtd.git", + "subdir": "" + } + }, + "version": "63d430b69a95741061c2f7fc9d84b1a778511d9c", + "sum": "qiZi3axUSXCVzKUF83zSAxklwrnitMmrDK4XAfjPMdE=" + } + ], + "legacyImports": false +} diff --git a/charts/gateway-addons-helm/dashboards/lib/g.libsonnet b/charts/gateway-addons-helm/dashboards/lib/g.libsonnet new file mode 100644 index 00000000000..69aac830033 --- /dev/null +++ b/charts/gateway-addons-helm/dashboards/lib/g.libsonnet @@ -0,0 +1 @@ +import 'github.com/grafana/grafonnet/gen/grafonnet-latest/main.libsonnet' diff --git a/charts/gateway-addons-helm/dashboards/lib/panels.libsonnet b/charts/gateway-addons-helm/dashboards/lib/panels.libsonnet new file mode 100644 index 00000000000..1173efa70d4 --- /dev/null +++ b/charts/gateway-addons-helm/dashboards/lib/panels.libsonnet @@ -0,0 +1,83 @@ +local g = import 'g.libsonnet'; + +{ + timeSeries: { + local timeSeries = g.panel.timeSeries, + local fieldOverride = g.panel.timeSeries.fieldOverride, + local custom = timeSeries.fieldConfig.defaults.custom, + local options = timeSeries.options, + + base(title, targets): + timeSeries.new(title) + + timeSeries.queryOptions.withTargets(targets) + + timeSeries.queryOptions.withInterval('1m') + + options.legend.withDisplayMode('table') + + options.legend.withCalcs([ + 'lastNotNull', + 'max', + ]) + + custom.withFillOpacity(10) + + custom.withShowPoints('never'), + + short(title, targets): + self.base(title, targets) + + timeSeries.standardOptions.withUnit('short') + + timeSeries.standardOptions.withDecimals(0), + + seconds(title, targets): + self.base(title, targets) + + timeSeries.standardOptions.withUnit('s') + + custom.scaleDistribution.withType('log') + + custom.scaleDistribution.withLog(10), + + cpuUsage: self.seconds, + + bytes(title, targets): + self.base(title, targets) + + timeSeries.standardOptions.withUnit('bytes') + + custom.scaleDistribution.withType('log') + + custom.scaleDistribution.withLog(2), + + memoryUsage: self.bytes, + + durationQuantile(title, targets): + self.base(title, targets) + + timeSeries.standardOptions.withUnit('s') + + custom.withDrawStyle('bars') + + timeSeries.standardOptions.withOverrides([ + fieldOverride.byRegexp.new('/mean/i') + + fieldOverride.byRegexp.withProperty( + 'custom.fillOpacity', + 0 + ) + + fieldOverride.byRegexp.withProperty( + 'custom.lineStyle', + { + dash: [8, 10], + fill: 'dash', + } + ), + ]), + }, + + heatmap: { + local heatmap = g.panel.heatmap, + local options = heatmap.options, + + base(title, targets): + heatmap.new(title) + + heatmap.queryOptions.withTargets(targets) + + heatmap.queryOptions.withInterval('1m') + + options.withCalculate() + + options.calculation.xBuckets.withMode('size') + + options.calculation.xBuckets.withValue('1min') + + options.withCellGap(2) + + options.color.withMode('scheme') + + options.color.withScheme('Spectral') + + options.color.withSteps(128) + + options.yAxis.withDecimals(0) + + options.yAxis.withUnit('s'), + }, +} + +// vim: foldmethod=marker foldmarker=local,; diff --git a/charts/gateway-addons-helm/dashboards/lib/queries.libsonnet b/charts/gateway-addons-helm/dashboards/lib/queries.libsonnet new file mode 100644 index 00000000000..ec38b428246 --- /dev/null +++ b/charts/gateway-addons-helm/dashboards/lib/queries.libsonnet @@ -0,0 +1,73 @@ +local g = import './g.libsonnet'; +local prometheusQuery = g.query.prometheus; + +local variables = import './variables.libsonnet'; + +{ + cpuUsageForEnvoyGateway: + prometheusQuery.new( + '$' + variables.datasource.name, + ||| + sum by (namespace) ( + rate( + container_cpu_usage_seconds_total{ + container="envoy-gateway" + } + [$__rate_interval]) + ) + ||| + ) + + prometheusQuery.withIntervalFactor(2) + + prometheusQuery.withLegendFormat(||| + {{namespace}} + |||), + + cpuUsageForEnvoyProxy: + prometheusQuery.new( + '$' + variables.datasource.name, + ||| + sum by (pod) ( + rate( + container_cpu_usage_seconds_total{ + container="envoy" + } + [$__rate_interval]) + ) + ||| + ) + + prometheusQuery.withIntervalFactor(2) + + prometheusQuery.withLegendFormat(||| + {{pod}} + |||), + + memUsageForEnvoyGateway: + prometheusQuery.new( + '$' + variables.datasource.name, + ||| + sum by (namespace) ( + container_memory_working_set_bytes{container="envoy-gateway"} + ) + ||| + ) + + prometheusQuery.withIntervalFactor(2) + + prometheusQuery.withLegendFormat(||| + {{namespace}} + |||), + + memUsageForEnvoyProxy: + prometheusQuery.new( + '$' + variables.datasource.name, + ||| + sum by (pod) ( + container_memory_working_set_bytes{container="envoy"} + ) + ||| + ) + + prometheusQuery.withIntervalFactor(2) + + prometheusQuery.withLegendFormat(||| + {{pod}} + |||), + +} + +// vim: foldmethod=indent shiftwidth=2 foldlevel=1 diff --git a/charts/gateway-addons-helm/dashboards/lib/variables.libsonnet b/charts/gateway-addons-helm/dashboards/lib/variables.libsonnet new file mode 100644 index 00000000000..1beecb77605 --- /dev/null +++ b/charts/gateway-addons-helm/dashboards/lib/variables.libsonnet @@ -0,0 +1,7 @@ +local g = import './g.libsonnet'; +local var = g.dashboard.variable; + +{ + datasource: + var.datasource.new('datasource', 'prometheus'), +} diff --git a/charts/gateway-addons-helm/dashboards/resources-monitor.gen.json b/charts/gateway-addons-helm/dashboards/resources-monitor.gen.json new file mode 100644 index 00000000000..c4f423d5f18 --- /dev/null +++ b/charts/gateway-addons-helm/dashboards/resources-monitor.gen.json @@ -0,0 +1,249 @@ +{ + "description": "Memory and CPU Usage Monitor for Envoy Gateway and Envoy Proxy.\n", + "graphTooltip": 1, + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 1, + "panels": [ ], + "title": "Envoy Gateway", + "type": "row" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Mixed --" + }, + "fieldConfig": { + "defaults": { + "custom": { + "fillOpacity": 10, + "scaleDistribution": { + "log": 10, + "type": "log" + }, + "showPoints": "never" + }, + "unit": "s" + } + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 1 + }, + "id": 2, + "interval": "1m", + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max" + ], + "displayMode": "table" + } + }, + "pluginVersion": "v11.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "expr": "sum by (namespace) (\n rate(\n container_cpu_usage_seconds_total{\n container=\"envoy-gateway\"\n }\n [$__rate_interval])\n)\n", + "intervalFactor": 2, + "legendFormat": "{{namespace}}\n" + } + ], + "title": "CPU Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Mixed --" + }, + "fieldConfig": { + "defaults": { + "custom": { + "fillOpacity": 10, + "scaleDistribution": { + "log": 2, + "type": "log" + }, + "showPoints": "never" + }, + "unit": "bytes" + } + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 1 + }, + "id": 3, + "interval": "1m", + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max" + ], + "displayMode": "table" + } + }, + "pluginVersion": "v11.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "expr": "sum by (namespace) (\n container_memory_working_set_bytes{container=\"envoy-gateway\"}\n)\n", + "intervalFactor": 2, + "legendFormat": "{{namespace}}\n" + } + ], + "title": "Memory Usage", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 9 + }, + "id": 4, + "panels": [ ], + "title": "Envoy Proxy", + "type": "row" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Mixed --" + }, + "fieldConfig": { + "defaults": { + "custom": { + "fillOpacity": 10, + "scaleDistribution": { + "log": 10, + "type": "log" + }, + "showPoints": "never" + }, + "unit": "s" + } + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 10 + }, + "id": 5, + "interval": "1m", + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max" + ], + "displayMode": "table" + } + }, + "pluginVersion": "v11.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "expr": "sum by (pod) (\n rate(\n container_cpu_usage_seconds_total{\n container=\"envoy\"\n }\n [$__rate_interval])\n)\n", + "intervalFactor": 2, + "legendFormat": "{{pod}}\n" + } + ], + "title": "CPU Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Mixed --" + }, + "fieldConfig": { + "defaults": { + "custom": { + "fillOpacity": 10, + "scaleDistribution": { + "log": 2, + "type": "log" + }, + "showPoints": "never" + }, + "unit": "bytes" + } + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 10 + }, + "id": 6, + "interval": "1m", + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max" + ], + "displayMode": "table" + } + }, + "pluginVersion": "v11.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "expr": "sum by (pod) (\n container_memory_working_set_bytes{container=\"envoy\"}\n)\n", + "intervalFactor": 2, + "legendFormat": "{{pod}}\n" + } + ], + "title": "Memory Usage", + "type": "timeseries" + } + ], + "schemaVersion": 39, + "templating": { + "list": [ + { + "name": "datasource", + "query": "prometheus", + "type": "datasource" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timezone": "utc", + "title": "Resources Monitor", + "uid": "f7aeb41676b7865cf31ae49691325f91" +} diff --git a/charts/gateway-addons-helm/dashboards/resources-monitor.libsonnet b/charts/gateway-addons-helm/dashboards/resources-monitor.libsonnet new file mode 100644 index 00000000000..ec2aa5ff0e9 --- /dev/null +++ b/charts/gateway-addons-helm/dashboards/resources-monitor.libsonnet @@ -0,0 +1,31 @@ +local g = import 'lib/g.libsonnet'; + +local row = g.panel.row; + +local panels = import 'lib/panels.libsonnet'; +local variables = import 'lib/variables.libsonnet'; +local queries = import 'lib/queries.libsonnet'; + +g.dashboard.new('Resources Monitor') ++ g.dashboard.withDescription(||| + Memory and CPU Usage Monitor for Envoy Gateway and Envoy Proxy. +|||) ++ g.dashboard.graphTooltip.withSharedCrosshair() ++ g.dashboard.withVariables([ + variables.datasource, +]) ++ g.dashboard.withPanels( + g.util.grid.makeGrid([ + row.new('Envoy Gateway') + + row.withPanels([ + panels.timeSeries.cpuUsage('CPU Usage', queries.cpuUsageForEnvoyGateway), + panels.timeSeries.memoryUsage('Memory Usage', queries.memUsageForEnvoyGateway), + ]), + row.new('Envoy Proxy') + + row.withPanels([ + panels.timeSeries.cpuUsage('CPU Usage', queries.cpuUsageForEnvoyProxy), + panels.timeSeries.memoryUsage('Memory Usage', queries.memUsageForEnvoyProxy), + ]), + ], panelWidth=8) +) ++ g.dashboard.withUid(std.md5('resources-monitor.json')) diff --git a/charts/gateway-addons-helm/values.yaml b/charts/gateway-addons-helm/values.yaml index eddf933c17c..fa98354c5f0 100644 --- a/charts/gateway-addons-helm/values.yaml +++ b/charts/gateway-addons-helm/values.yaml @@ -60,6 +60,8 @@ prometheus: # Values for Fluent-bit dependency fluent-bit: enabled: true + image: + repository: fluent/fluent-bit # use image from dockerhub fullnameOverride: fluent-bit testFramework: enabled: false diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml index d779a2d832f..20ffe833923 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml @@ -476,8 +476,11 @@ spec: minimum: 2 type: integer type: - description: ConsistentHashType defines the type of input - to hash on. Valid Type values are "SourceIP" or "Header". + description: |- + ConsistentHashType defines the type of input to hash on. Valid Type values are + "SourceIP", + "Header", + "Cookie". enum: - SourceIP - Header @@ -1121,8 +1124,9 @@ spec: type: object x-kubernetes-validations: - message: either targetRef or targetRefs must be used - rule: (has(self.targetRef) && !has(self.targetRefs)) || (!has(self.targetRef) - && has(self.targetRefs)) + rule: '(has(self.targetRef) && !has(self.targetRefs)) || (!has(self.targetRef) + && has(self.targetRefs)) || (has(self.targetSelectors) && self.targetSelectors.size() + > 0) ' - message: this policy can only have a targetRef.group of gateway.networking.k8s.io rule: 'has(self.targetRef) ? self.targetRef.group == ''gateway.networking.k8s.io'' : true ' diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml index 2e2224ecca3..48cfb9f3aad 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml @@ -700,8 +700,9 @@ spec: type: object x-kubernetes-validations: - message: either targetRef or targetRefs must be used - rule: (has(self.targetRef) && !has(self.targetRefs)) || (!has(self.targetRef) - && has(self.targetRefs)) + rule: '(has(self.targetRef) && !has(self.targetRefs)) || (!has(self.targetRef) + && has(self.targetRefs)) || (has(self.targetSelectors) && self.targetSelectors.size() + > 0) ' - message: this policy can only have a targetRef.group of gateway.networking.k8s.io rule: 'has(self.targetRef) ? self.targetRef.group == ''gateway.networking.k8s.io'' : true' diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml index 0b257603050..61827ee1205 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml @@ -521,8 +521,9 @@ spec: type: object x-kubernetes-validations: - message: either targetRef or targetRefs must be used - rule: (has(self.targetRef) && !has(self.targetRefs)) || (!has(self.targetRef) - && has(self.targetRefs)) + rule: '(has(self.targetRef) && !has(self.targetRefs)) || (!has(self.targetRef) + && has(self.targetRefs)) || (has(self.targetSelectors) && self.targetSelectors.size() + > 0) ' - message: this policy can only have a targetRef.group of gateway.networking.k8s.io rule: 'has(self.targetRef) ? self.targetRef.group == ''gateway.networking.k8s.io'' : true' diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml index e5c0b7fc95a..3906b325b3d 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml @@ -1129,8 +1129,9 @@ spec: type: object x-kubernetes-validations: - message: either targetRef or targetRefs must be used - rule: (has(self.targetRef) && !has(self.targetRefs)) || (!has(self.targetRef) - && has(self.targetRefs)) + rule: '(has(self.targetRef) && !has(self.targetRefs)) || (!has(self.targetRef) + && has(self.targetRefs)) || (has(self.targetSelectors) && self.targetSelectors.size() + > 0) ' - message: this policy can only have a targetRef.group of gateway.networking.k8s.io rule: 'has(self.targetRef) ? self.targetRef.group == ''gateway.networking.k8s.io'' : true' diff --git a/charts/gateway-helm/templates/NOTES.txt b/charts/gateway-helm/templates/NOTES.txt index e002d40d699..595c49bcd96 100644 --- a/charts/gateway-helm/templates/NOTES.txt +++ b/charts/gateway-helm/templates/NOTES.txt @@ -15,6 +15,6 @@ To learn more about the release, try: $ helm status {{ .Release.Name }} -n {{ .Release.Namespace }} $ helm get all {{ .Release.Name }} -n {{ .Release.Namespace }} -To have a quickstart of Envoy Gateway, please refer to https://gateway.envoyproxy.io/latest/user/quickstart. +To have a quickstart of Envoy Gateway, please refer to https://gateway.envoyproxy.io/latest/tasks/quickstart. To get more details, please visit https://gateway.envoyproxy.io and https://github.com/envoyproxy/gateway. diff --git a/examples/extension-server/.gitignore b/examples/extension-server/.gitignore new file mode 100644 index 00000000000..ba077a4031a --- /dev/null +++ b/examples/extension-server/.gitignore @@ -0,0 +1 @@ +bin diff --git a/examples/extension-server/Makefile b/examples/extension-server/Makefile new file mode 100644 index 00000000000..9131d1b701b --- /dev/null +++ b/examples/extension-server/Makefile @@ -0,0 +1,30 @@ +tools.dir = tools +tools.bindir = tools/bin +tools.srcdir = tools/src + +tools/controller-gen = $(tools.bindir)/controller-gen +$(tools.bindir)/%: $(tools.srcdir)/%/pin.go $(tools.srcdir)/%/go.mod + cd $( ../../ diff --git a/examples/extension-server/go.sum b/examples/extension-server/go.sum new file mode 100644 index 00000000000..78924f55902 --- /dev/null +++ b/examples/extension-server/go.sum @@ -0,0 +1,138 @@ +cel.dev/expr v0.15.0 h1:O1jzfJCQBfL5BFoYktaxwIhuttaQPsVWerH9/EEKx0w= +cel.dev/expr v0.15.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg= +github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= +github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= +github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b h1:ga8SEFjZ60pxLcmhnThWgvH2wg8376yUJmPhEH4H3kw= +github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.12.1-0.20240612043845-c54ec4ce422d h1:RopQsG28t61pLLZRkwzwBsi60yDsOP8RvW47A3eAcGo= +github.com/envoyproxy/go-control-plane v0.12.1-0.20240612043845-c54ec4ce422d/go.mod h1:5Wkq+JduFtdAXihLmeTJf+tRYIT4KBc2vPXDhwVo1pA= +github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A= +github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8= +github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs= +github.com/onsi/gomega v1.32.0 h1:JRYU78fJ1LPxlckP6Txi/EYqJvjtMrDC04/MM5XRHPk= +github.com/onsi/gomega v1.32.0/go.mod h1:a4x4gW6Pz2yK1MAmvluYme5lvYTn61afQ2ETw/8n4Lg= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI= +github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM= +github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= +github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0= +google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.30.2 h1:+ZhRj+28QT4UOH+BKznu4CBgPWgkXO7XAvMcMl0qKvI= +k8s.io/api v0.30.2/go.mod h1:ULg5g9JvOev2dG0u2hig4Z7tQ2hHIuS+m8MNZ+X6EmI= +k8s.io/apimachinery v0.30.2 h1:fEMcnBj6qkzzPGSVsAZtQThU62SmQ4ZymlXRC5yFSCg= +k8s.io/apimachinery v0.30.2/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 h1:jgGTlFYnhF1PM1Ax/lAlxUPE+KfCIXHaathvJg1C3ak= +k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.18.4 h1:87+guW1zhvuPLh1PHybKdYFLU0YJp4FhJRmiHvm5BZw= +sigs.k8s.io/controller-runtime v0.18.4/go.mod h1:TVoGrfdpbA9VRFaRnKgk9P5/atA0pMwq+f+msb9M8Sg= +sigs.k8s.io/gateway-api v1.1.0 h1:DsLDXCi6jR+Xz8/xd0Z1PYl2Pn0TyaFMOPPZIj4inDM= +sigs.k8s.io/gateway-api v1.1.0/go.mod h1:ZH4lHrL2sDi0FHZ9jjneb8kKnGzFWyrTya35sWUTrRs= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/examples/extension-server/internal/extensionserver/htpasswd.go b/examples/extension-server/internal/extensionserver/htpasswd.go new file mode 100644 index 00000000000..0e43c755320 --- /dev/null +++ b/examples/extension-server/internal/extensionserver/htpasswd.go @@ -0,0 +1,38 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package extensionserver + +import ( + "crypto/sha1" + "encoding/base64" + "fmt" + "io" + "strings" +) + +type htpasswd struct { + Users map[string]string +} + +func NewHtpasswd() htpasswd { + return htpasswd{ + Users: map[string]string{}, + } +} + +func (h *htpasswd) AddUser(user, password string) { + s := sha1.New() + io.WriteString(s, password) + h.Users[user] = fmt.Sprintf("{SHA}%s", base64.StdEncoding.EncodeToString(s.Sum(nil))) +} + +func (h *htpasswd) String() string { + var b strings.Builder + for user, password := range h.Users { + b.WriteString(fmt.Sprintf("%s:%s\n", user, password)) + } + return b.String() +} diff --git a/examples/extension-server/internal/extensionserver/server.go b/examples/extension-server/internal/extensionserver/server.go new file mode 100644 index 00000000000..a2776a9f966 --- /dev/null +++ b/examples/extension-server/internal/extensionserver/server.go @@ -0,0 +1,150 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package extensionserver + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + + pb "github.com/envoyproxy/gateway/proto/extension" + corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + listenerv3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" + bav3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/basic_auth/v3" + hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" + "github.com/envoyproxy/go-control-plane/pkg/wellknown" + "google.golang.org/protobuf/types/known/anypb" + + "github.com/exampleorg/envoygateway-extension/api/v1alpha1" +) + +type Server struct { + pb.UnimplementedEnvoyGatewayExtensionServer + + log *slog.Logger +} + +func New(logger *slog.Logger) *Server { + return &Server{ + log: logger, + } +} + +// PostHTTPListenerModify is called after Envoy Gateway is done generating a +// Listener xDS configuration and before that configuration is passed on to +// Envoy Proxy. +// This example adds Basic Authentication on the Listener level as an example. +// Note: This implementation is not secure, and should not be used to protect +// anything important. +func (s *Server) PostHTTPListenerModify(ctx context.Context, req *pb.PostHTTPListenerModifyRequest) (*pb.PostHTTPListenerModifyResponse, error) { + s.log.Info("postHTTPListenerModify callback was invoked") + // Collect all of the required username/password combinations from the + // provided contexts that were attached to the gateway. + passwords := NewHtpasswd() + for _, ext := range req.PostListenerContext.ExtensionResources { + var listenerContext v1alpha1.ListenerContextExample + if err := json.Unmarshal(ext.GetUnstructuredBytes(), &listenerContext); err != nil { + s.log.Error("failed to unmarshal the extension", slog.String("error", err.Error())) + continue + } + s.log.Info("processing an extension context", slog.String("username", listenerContext.Spec.Username)) + passwords.AddUser(listenerContext.Spec.Username, listenerContext.Spec.Password) + } + + // First, get the filter chains from the listener + filterChains := req.Listener.GetFilterChains() + defaultFC := req.Listener.DefaultFilterChain + if defaultFC != nil { + filterChains = append(filterChains, defaultFC) + } + // Go over all of the chains, and add the basic authentication http filter + for _, currChain := range filterChains { + httpConManager, hcmIndex, err := findHCM(currChain) + if err != nil { + s.log.Error("failed to find an HCM in the current chain", slog.Any("error", err)) + continue + } + // If a basic authentication filter already exists, update it. Otherwise, create it. + basicAuth, baIndex, err := findBasicAuthFilter(httpConManager.HttpFilters) + if err != nil { + s.log.Error("failed to unmarshal the existing basicAuth filter", slog.Any("error", err)) + continue + } + if baIndex == -1 { + // Create a new basic auth filter + basicAuth = &bav3.BasicAuth{ + Users: &corev3.DataSource{ + Specifier: &corev3.DataSource_InlineString{ + InlineString: passwords.String(), + }, + }, + ForwardUsernameHeader: "X-Example-Ext", + } + } else { + // Update the basic auth filter + basicAuth.Users.Specifier = &corev3.DataSource_InlineString{ + InlineString: passwords.String(), + } + } + // Add or update the Basic Authentication filter in the HCM + anyBAFilter, _ := anypb.New(basicAuth) + if baIndex > -1 { + httpConManager.HttpFilters[baIndex].ConfigType = &hcm.HttpFilter_TypedConfig{ + TypedConfig: anyBAFilter, + } + } else { + filters := []*hcm.HttpFilter{ + { + Name: "envoy.filters.http.basic_auth", + ConfigType: &hcm.HttpFilter_TypedConfig{ + TypedConfig: anyBAFilter, + }, + }, + } + filters = append(filters, httpConManager.HttpFilters...) + httpConManager.HttpFilters = filters + } + + // Write the updated HCM back to the filter chain + anyConnectionMgr, _ := anypb.New(httpConManager) + currChain.Filters[hcmIndex].ConfigType = &listenerv3.Filter_TypedConfig{ + TypedConfig: anyConnectionMgr, + } + } + + return &pb.PostHTTPListenerModifyResponse{ + Listener: req.Listener, + }, nil +} + +// Tries to find an HTTP connection manager in the provided filter chain. +func findHCM(filterChain *listenerv3.FilterChain) (*hcm.HttpConnectionManager, int, error) { + for filterIndex, filter := range filterChain.Filters { + if filter.Name == wellknown.HTTPConnectionManager { + hcm := new(hcm.HttpConnectionManager) + if err := filter.GetTypedConfig().UnmarshalTo(hcm); err != nil { + return nil, -1, err + } + return hcm, filterIndex, nil + } + } + return nil, -1, fmt.Errorf("unable to find HTTPConnectionManager in FilterChain: %s", filterChain.Name) +} + +// Tries to find the Basic Authentication HTTP filter in the provided chain +func findBasicAuthFilter(chain []*hcm.HttpFilter) (*bav3.BasicAuth, int, error) { + for i, filter := range chain { + if filter.Name == "envoy.filters.http.basic_auth" { + ba := new(bav3.BasicAuth) + if err := filter.GetTypedConfig().UnmarshalTo(ba); err != nil { + return nil, -1, err + } + return ba, i, nil + } + } + return nil, -1, nil +} diff --git a/examples/extension-server/tools/boilerplate.generatego.txt b/examples/extension-server/tools/boilerplate.generatego.txt new file mode 100644 index 00000000000..3f9e2deb710 --- /dev/null +++ b/examples/extension-server/tools/boilerplate.generatego.txt @@ -0,0 +1,5 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + diff --git a/examples/extension-server/tools/docker/extension-server/Dockerfile b/examples/extension-server/tools/docker/extension-server/Dockerfile new file mode 100644 index 00000000000..4cb549d954d --- /dev/null +++ b/examples/extension-server/tools/docker/extension-server/Dockerfile @@ -0,0 +1,6 @@ +FROM gcr.io/distroless/static:nonroot@sha256:e9ac71e2b8e279a8372741b7a0293afda17650d926900233ec3a7b2b7c22a246 +COPY ./bin/extension-server /usr/local/bin/extension-server + +USER 65532:65532 + +ENTRYPOINT ["/usr/local/bin/extension-server"] diff --git a/examples/extension-server/tools/src/controller-gen/go.mod b/examples/extension-server/tools/src/controller-gen/go.mod new file mode 100644 index 00000000000..3b5da982d27 --- /dev/null +++ b/examples/extension-server/tools/src/controller-gen/go.mod @@ -0,0 +1,38 @@ +module local + +go 1.22.5 + +require sigs.k8s.io/controller-tools v0.15.0 + +require ( + github.com/fatih/color v1.16.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/gobuffalo/flect v1.0.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/spf13/cobra v1.8.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.20.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.30.0 // indirect + k8s.io/apiextensions-apiserver v0.30.0 // indirect + k8s.io/apimachinery v0.30.0 // indirect + k8s.io/klog/v2 v2.120.1 // indirect + k8s.io/utils v0.0.0-20240423183400-0849a56e8f22 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/examples/extension-server/tools/src/controller-gen/go.sum b/examples/extension-server/tools/src/controller-gen/go.sum new file mode 100644 index 00000000000..797074f310a --- /dev/null +++ b/examples/extension-server/tools/src/controller-gen/go.sum @@ -0,0 +1,136 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/gobuffalo/flect v1.0.2 h1:eqjPGSo2WmjgY2XlpGwo2NXgL3RucAKo4k4qQMNA5sA= +github.com/gobuffalo/flect v1.0.2/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v1.33.0 h1:snPCflnZrpMsy94p4lXVEkHo12lmPnc3vY5XBbreexE= +github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= +golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.30.0 h1:siWhRq7cNjy2iHssOB9SCGNCl2spiF1dO3dABqZ8niA= +k8s.io/api v0.30.0/go.mod h1:OPlaYhoHs8EQ1ql0R/TsUgaRPhpKNxIMrKQfWUp8QSE= +k8s.io/apiextensions-apiserver v0.30.0 h1:jcZFKMqnICJfRxTgnC4E+Hpcq8UEhT8B2lhBcQ+6uAs= +k8s.io/apiextensions-apiserver v0.30.0/go.mod h1:N9ogQFGcrbWqAY9p2mUAL5mGxsLqwgtUce127VtRX5Y= +k8s.io/apimachinery v0.30.0 h1:qxVPsyDM5XS96NIh9Oj6LavoVFYff/Pon9cZeDIkHHA= +k8s.io/apimachinery v0.30.0/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= +k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= +k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/utils v0.0.0-20240423183400-0849a56e8f22 h1:ao5hUqGhsqdm+bYbjH/pRkCs0unBGe9UyDahzs9zQzQ= +k8s.io/utils v0.0.0-20240423183400-0849a56e8f22/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-tools v0.15.0 h1:4dxdABXGDhIa68Fiwaif0vcu32xfwmgQ+w8p+5CxoAI= +sigs.k8s.io/controller-tools v0.15.0/go.mod h1:8zUSS2T8Hx0APCNRhJWbS3CAQEbIxLa07khzh7pZmXM= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/examples/extension-server/tools/src/controller-gen/pin.go b/examples/extension-server/tools/src/controller-gen/pin.go new file mode 100644 index 00000000000..82c8d1cf3c5 --- /dev/null +++ b/examples/extension-server/tools/src/controller-gen/pin.go @@ -0,0 +1,11 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +//go:build pin +// +build pin + +package ignore + +import _ "sigs.k8s.io/controller-tools/cmd/controller-gen" diff --git a/examples/kubernetes/envoy-als.yaml b/examples/kubernetes/envoy-als.yaml new file mode 100644 index 00000000000..41e2d65ab52 --- /dev/null +++ b/examples/kubernetes/envoy-als.yaml @@ -0,0 +1,231 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: envoy-als +data: + go.mod: | + module envoy-als + go 1.22 + require ( + github.com/envoyproxy/go-control-plane v0.12.0 + github.com/prometheus/client_golang v1.19.1 + google.golang.org/grpc v1.64.0 + ) + + require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50 // indirect + github.com/envoyproxy/protoc-gen-validate v1.0.4 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.48.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + golang.org/x/net v0.22.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + google.golang.org/protobuf v1.33.0 // indirect + ) + go.sum: | + github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= + github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= + github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= + github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= + github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50 h1:DBmgJDC9dTfkVyGgipamEh2BpGYxScCH1TOF1LL1cXc= + github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50/go.mod h1:5e1+Vvlzido69INQaVO6d87Qn543Xr6nooe9Kz7oBFM= + github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= + github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= + github.com/envoyproxy/go-control-plane v0.12.0 h1:4X+VP1GHd1Mhj6IB5mMeGbLCleqxjletLK6K0rbxyZI= + github.com/envoyproxy/go-control-plane v0.12.0/go.mod h1:ZBTaoJ23lqITozF0M6G4/IragXCQKCnYbmlmtHvwRG0= + github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A= + github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew= + github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= + github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= + github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= + github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= + github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= + github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= + github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= + github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= + github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= + github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= + github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= + github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= + golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= + golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= + golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= + golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= + golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= + golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= + google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc= + google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= + google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= + google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= + google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= + google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= + main.go: | + package main + + import ( + "log" + "net" + "net/http" + + alsv2 "github.com/envoyproxy/go-control-plane/envoy/service/accesslog/v2" + alsv3 "github.com/envoyproxy/go-control-plane/envoy/service/accesslog/v3" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + + "google.golang.org/grpc" + ) + + var ( + LogCount = prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "log_count", + Help: "The total number of logs received.", + }, []string{"api_version"}) + ) + + func init() { + // Register the summary and the histogram with Prometheus's default registry. + prometheus.MustRegister(LogCount) + } + + type ALSServer struct { + } + + func (a *ALSServer) StreamAccessLogs(logStream alsv2.AccessLogService_StreamAccessLogsServer) error { + log.Println("Streaming als v2 logs") + for { + data, err := logStream.Recv() + if err != nil { + return err + } + + httpLogs := data.GetHttpLogs() + if httpLogs != nil { + LogCount.WithLabelValues("v2").Add(float64(len(httpLogs.LogEntry))) + } + + log.Printf("Received v2 log data: %s\n", data.String()) + } + } + + type ALSServerV3 struct { + } + + func (a *ALSServerV3) StreamAccessLogs(logStream alsv3.AccessLogService_StreamAccessLogsServer) error { + log.Println("Streaming als v3 logs") + for { + data, err := logStream.Recv() + if err != nil { + return err + } + + httpLogs := data.GetHttpLogs() + if httpLogs != nil { + LogCount.WithLabelValues("v3").Add(float64(len(httpLogs.LogEntry))) + } + + log.Printf("Received v3 log data: %s\n", data.String()) + } + } + + func NewALSServer() *ALSServer { + return &ALSServer{} + } + + func NewALSServerV3() *ALSServerV3 { + return &ALSServerV3{} + } + + func main() { + mux := http.NewServeMux() + if err := addMonitor(mux); err != nil { + log.Printf("could not establish self-monitoring: %v\n", err) + } + + s := &http.Server{ + Addr: ":19001", + Handler: mux, + } + + go func() { + s.ListenAndServe() + }() + + listener, err := net.Listen("tcp", "0.0.0.0:8080") + if err != nil { + log.Fatalf("Failed to start listener on port 8080: %v", err) + } + + var opts []grpc.ServerOption + grpcServer := grpc.NewServer(opts...) + alsv2.RegisterAccessLogServiceServer(grpcServer, NewALSServer()) + alsv3.RegisterAccessLogServiceServer(grpcServer, NewALSServerV3()) + log.Println("Starting ALS Server") + if err := grpcServer.Serve(listener); err != nil { + log.Fatalf("grpc serve err: %v", err) + } + } + + func addMonitor(mux *http.ServeMux) error { + mux.Handle("/metrics", promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{EnableOpenMetrics: true})) + + return nil + } +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: envoy-als +spec: + replicas: 1 + selector: + matchLabels: + app: envoy-als + template: + metadata: + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "19001" + labels: + app: envoy-als + spec: + containers: + - name: envoy-als + command: + - sh + - "-c" + - "cp -a /app /app-live && cd /app-live && go run . " + image: golang:1.22.3-alpine + ports: + - containerPort: 8080 + - containerPort: 19001 + volumeMounts: + - name: envoy-als + mountPath: /app + volumes: + - name: envoy-als + configMap: + name: envoy-als +--- +apiVersion: v1 +kind: Service +metadata: + name: envoy-als +spec: + selector: + app: envoy-als + type: LoadBalancer + ports: + - name: grpc-als + protocol: TCP + appProtocol: grpc + port: 8080 + targetPort: 8080 + - name: http-monitoring + protocol: TCP + port: 19001 + targetPort: 19001 diff --git a/examples/kubernetes/ext-proc-grpc-service.yaml b/examples/kubernetes/ext-proc-grpc-service.yaml index 120520cfa85..23b90b104cb 100644 --- a/examples/kubernetes/ext-proc-grpc-service.yaml +++ b/examples/kubernetes/ext-proc-grpc-service.yaml @@ -361,7 +361,7 @@ spec: - sh - "-c" - "cp -a /app /app-live && cd /app-live && go run . --certPath=/app-live/certs/ " - image: golang:1.22.3-alpine + image: golang:1.22.5-alpine ports: - containerPort: 8000 volumeMounts: diff --git a/go.mod b/go.mod index d5b5667e0fb..9f3ccfb9508 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,8 @@ go 1.22.5 replace github.com/imdario/mergo => github.com/imdario/mergo v0.3.16 require ( - fortio.org/fortio v1.65.0 - fortio.org/log v1.12.2 + fortio.org/fortio v1.66.0 + fortio.org/log v1.14.0 github.com/Masterminds/semver/v3 v3.2.1 github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc @@ -44,11 +44,11 @@ require ( go.opentelemetry.io/proto/otlp v1.3.1 go.uber.org/zap v1.27.0 golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 - golang.org/x/sys v0.21.0 - google.golang.org/grpc v1.64.0 + golang.org/x/sys v0.22.0 + google.golang.org/grpc v1.65.0 google.golang.org/protobuf v1.34.2 gopkg.in/yaml.v3 v3.0.1 - helm.sh/helm/v3 v3.15.2 + helm.sh/helm/v3 v3.15.3 k8s.io/api v0.30.2 k8s.io/apiextensions-apiserver v0.30.2 k8s.io/apimachinery v0.30.2 @@ -62,13 +62,22 @@ require ( sigs.k8s.io/yaml v1.4.0 ) -require github.com/docker/docker v27.0.3+incompatible +require ( + github.com/docker/docker v27.0.3+incompatible + github.com/replicatedhq/troubleshoot v0.95.1-0.20240707233129-f5f02f5a807c +) require ( cel.dev/expr v0.15.0 // indirect - fortio.org/cli v1.6.0 // indirect + cloud.google.com/go v0.112.1 // indirect + cloud.google.com/go/compute/metadata v0.3.0 // indirect + cloud.google.com/go/iam v1.1.7 // indirect + cloud.google.com/go/storage v1.40.0 // indirect + dario.cat/mergo v1.0.0 // indirect + filippo.io/edwards25519 v1.1.0 // indirect + fortio.org/cli v1.7.0 // indirect fortio.org/dflag v1.7.2 // indirect - fortio.org/scli v1.15.0 // indirect + fortio.org/scli v1.15.1 // indirect fortio.org/sets v1.1.1 // indirect fortio.org/struct2env v0.4.1 // indirect fortio.org/version v1.0.4 // indirect @@ -80,12 +89,23 @@ require ( github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/hcsshim v0.12.3 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/apparentlymart/go-cidr v1.1.0 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/aws/aws-sdk-go v1.48.10 // indirect + github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect github.com/blang/semver/v4 v4.0.0 // indirect + github.com/c9s/goprocinfo v0.0.0-20170724085704-0010a05ce49f // indirect + github.com/containerd/cgroups/v3 v3.0.3 // indirect github.com/containerd/containerd v1.7.17 // indirect + github.com/containerd/errdefs v0.1.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect + github.com/containers/image/v5 v5.31.1 // indirect + github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect + github.com/containers/ocicrypt v1.1.10 // indirect + github.com/containers/storage v1.54.0 // indirect github.com/cyphar/filepath-securejoin v0.2.5 // indirect + github.com/distribution/distribution/v3 v3.0.0-alpha.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker-credential-helpers v0.8.2 // indirect @@ -94,45 +114,104 @@ require ( github.com/docker/go-units v0.5.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-redis/redis/v7 v7.4.1 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/gobwas/glob v0.2.3 // indirect - github.com/gorilla/mux v1.8.0 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect + github.com/golang-sql/sqlexp v0.1.0 // indirect + github.com/google/go-intervals v0.0.2 // indirect + github.com/google/s2a-go v0.1.7 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect + github.com/googleapis/gax-go/v2 v2.12.3 // indirect + github.com/gorilla/handlers v1.5.2 // indirect + github.com/gorilla/mux v1.8.1 // indirect github.com/gosuri/uitable v0.0.4 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-getter v1.7.5 // indirect + github.com/hashicorp/go-safetemp v1.0.0 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect github.com/huandu/xstrings v1.4.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.6.0 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmoiron/sqlx v1.3.5 // indirect - github.com/klauspost/compress v1.17.2 // indirect + github.com/klauspost/compress v1.17.8 // indirect + github.com/klauspost/pgzip v1.2.6 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/lib/pq v1.10.9 // indirect + github.com/longhorn/go-iscsi-helper v0.0.0-20210330030558-49a327fb024e // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/microsoft/go-mssqldb v1.7.2 // indirect + github.com/mistifyio/go-zfs/v3 v3.0.1 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/locker v1.0.1 // indirect github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/mountinfo v0.7.1 // indirect github.com/moby/sys/sequential v0.5.0 // indirect github.com/moby/sys/user v0.1.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/opencontainers/runtime-spec v1.2.0 // indirect + github.com/opencontainers/selinux v1.11.0 // indirect + github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rubenv/sql-migrate v1.6.1 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/segmentio/ksuid v1.0.4 // indirect + github.com/shirou/gopsutil/v3 v3.24.5 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/shopspring/decimal v1.4.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/viper v1.19.0 // indirect github.com/stoewer/go-strcase v1.2.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/sylabs/sif/v2 v2.16.0 // indirect + github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect + github.com/tchap/go-patricia/v2 v2.3.1 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/ulikunitz/xz v0.5.12 // indirect github.com/vbatts/tar-split v0.11.5 // indirect + github.com/vmware-tanzu/velero v1.14.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 // indirect - golang.org/x/crypto v0.24.0 // indirect - golang.org/x/crypto/x509roots/fallback v0.0.0-20240604170348-d4e7c9cb6cb8 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + golang.org/x/crypto v0.25.0 // indirect + golang.org/x/crypto/x509roots/fallback v0.0.0-20240626151235-a6a393ffd658 // indirect + google.golang.org/api v0.172.0 // indirect + google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect k8s.io/apiserver v0.30.2 // indirect + k8s.io/kubelet v0.30.2 // indirect + k8s.io/metrics v0.30.2 // indirect oras.land/oras-go v1.2.5 // indirect + periph.io/x/host/v3 v3.8.2 // indirect ) require ( @@ -193,10 +272,10 @@ require ( go.starlark.net v0.0.0-20240520160348-046347dcd104 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.18.0 // indirect - golang.org/x/net v0.26.0 + golang.org/x/net v0.27.0 golang.org/x/oauth2 v0.21.0 // indirect golang.org/x/sync v0.7.0 // indirect - golang.org/x/term v0.21.0 // indirect + golang.org/x/term v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.22.0 // indirect @@ -206,7 +285,7 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect k8s.io/component-base v0.30.2 // indirect - k8s.io/klog/v2 v2.120.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/kustomize/api v0.17.2 // indirect diff --git a/go.sum b/go.sum index 10ee392cf07..50e98017925 100644 --- a/go.sum +++ b/go.sum @@ -3,26 +3,224 @@ cel.dev/expr v0.15.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= +cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= +cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= +cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM= +cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4= +cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= +cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= +cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= +cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= +cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= +cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= +cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= +cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= +cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= +cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= +cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= +cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= +cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= +cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= +cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= +cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= +cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= +cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= +cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= +cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= +cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= +cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= +cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= +cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= +cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= +cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= +cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= +cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= +cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= +cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= +cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= +cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= +cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= +cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= +cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= +cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= +cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= +cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= +cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8= +cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= +cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= +cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= +cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= +cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= +cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w= +cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= +cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= +cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= +cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= +cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= +cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= +cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= +cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= +cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= +cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= +cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= +cloud.google.com/go/iam v1.1.7 h1:z4VHOhwKLF/+UYXAJDFwGtNF0b6gjsW1Pk9Ml0U/IoM= +cloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA= +cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= +cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= +cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= +cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= +cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= +cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= +cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= +cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= +cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= +cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= +cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= +cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= +cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= +cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= +cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= +cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= +cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= +cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= +cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= +cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= +cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= +cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= +cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= +cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= +cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= +cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= +cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo= +cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= +cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= +cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= +cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= +cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= +cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= +cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= +cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= +cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= +cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= +cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= +cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= +cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= +cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= +cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= +cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= +cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= +cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= +cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= +cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= +cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= +cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= +cloud.google.com/go/storage v1.40.0 h1:VEpDQV5CJxFmJ6ueWNsKxcr1QAYOXEgxDa+sBbJahPw= +cloud.google.com/go/storage v1.40.0/go.mod h1:Rrj7/hKlG87BLqDJYtwR0fbPld8uJPbQ2ucUMY7Ir0g= +cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= +cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= +cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= +cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= +cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= +cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= +cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= +cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= +cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= +cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= +cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= fortio.org/assert v1.2.1 h1:48I39urpeDj65RP1KguF7akCjILNeu6vICiYMEysR7Q= fortio.org/assert v1.2.1/go.mod h1:039mG+/iYDPO8Ibx8TrNuJCm2T2SuhwRI3uL9nHTTls= -fortio.org/cli v1.6.0 h1:EX9zf+BLzgE+yrq2a3XFZz2F8CK1g9ecJj9ZXVOfoww= -fortio.org/cli v1.6.0/go.mod h1:QSCd+8OD3MrFKo2XwAHVJJu5gz/U0Jg1Vhse/4nHn3I= +fortio.org/cli v1.7.0 h1:w+uXZLGi4t3Vn/BvbeMuSw84Z1pvNPG9HqeGfpP68cc= +fortio.org/cli v1.7.0/go.mod h1:s4vxWz7P7T4cYOWdMF0NA693Nu1gK9OW4KoDj54/Do4= fortio.org/dflag v1.7.2 h1:lUhXFvDlw4CJj/q7hPv/TC+n/wVoQylzQO6bUg5GQa0= fortio.org/dflag v1.7.2/go.mod h1:6yO/NIgrWfQH195WbHJ3Y45SCx11ffivQjfx2C/FS1U= -fortio.org/fortio v1.65.0 h1:HQVyJrxYT4GXmwQe9vHJN9evmbqVPqKxhQac7qWQ+QA= -fortio.org/fortio v1.65.0/go.mod h1:R9jL6u4zKkQHO8HvuJxdWGmDg02w+2kouXKza/R3eaU= -fortio.org/log v1.12.2 h1:JwLDFvEUKGfqA09fcf+mOn8kxsvwhjXV92xghxNnnwA= -fortio.org/log v1.12.2/go.mod h1:1tMBG/Elr6YqjmJCWiejJp2FPvXg7/9UAN0Rfpkyt1o= -fortio.org/scli v1.15.0 h1:2LSnphdc3NGLHRD236/yINgrTTqvaPRmVhaK55V7wKo= -fortio.org/scli v1.15.0/go.mod h1:dFpj7h+mpMmsMYm9Bf/buVDGkpDysugl9gVemLDEsAo= +fortio.org/fortio v1.66.0 h1:9F/200qIu136z847bxs/NeAoYdJaQlVofYlppi3qwcw= +fortio.org/fortio v1.66.0/go.mod h1:eUl5MRscw6CiWAStai8aB3/8unxA9uNzJRXdhKEaq1s= +fortio.org/log v1.14.0 h1:ZkIc3Qqwfs9Dd931k07YzoC+bqCpJKEjVlZwxgXW3Nw= +fortio.org/log v1.14.0/go.mod h1:1tnXMqd5rZAgvSeHJkD2xXpyXRBzdeXtKLZuzNLIwtA= +fortio.org/scli v1.15.1 h1:Upza50brpEZwUk8Nn2gdP4BjgqJZY3J+z7KLrrAzPjY= +fortio.org/scli v1.15.1/go.mod h1:9LOD4iPe9u73KeJGYC/Af1oFniOafO7oZ9VvwENMf/c= fortio.org/sets v1.1.1 h1:Q7Z1Ft2lpUc1N7bfI8HofIK0QskrOflfYRyKT2LzBng= fortio.org/sets v1.1.1/go.mod h1:J2BwIxNOLWsSU7IMZUg541kh3Au4JEKHrghVwXs68tE= fortio.org/struct2env v0.4.1 h1:rJludAMO5eBvpWplWEQNqoVDFZr4RWMQX7RUapgZyc0= fortio.org/struct2env v0.4.1/go.mod h1:lENUe70UwA1zDUCX+8AsO663QCFqYaprk5lnPhjD410= fortio.org/version v1.0.4 h1:FWUMpJ+hVTNc4RhvvOJzb0xesrlRmG/a+D6bjbQ4+5U= fortio.org/version v1.0.4/go.mod h1:2JQp9Ax+tm6QKiGuzR5nJY63kFeANcgrZ0osoQFDVm0= +github.com/14rcole/gopopulate v0.0.0-20180821133914-b175b219e774 h1:SCbEWT58NSt7d2mcFdvxC9uyrdcTfvBbPLThhkDmXzg= +github.com/14rcole/gopopulate v0.0.0-20180821133914-b175b219e774/go.mod h1:6/0dYRLLXyJjbkIPeeGyoJ/eKOSI0eU6eTlCBYibgd0= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.2 h1:FDif4R1+UUR+00q6wquyX90K7A8dN+R5E8GEadoP7sU= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.2/go.mod h1:aiYBYui4BJ/BJCAIKs92XiPyQfTaBWqvHujDwKb6CBU= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 h1:LqbJ/WzJUwBf8UiaSzgX7aMclParm9/5Vgp+TY51uBQ= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= @@ -33,9 +231,12 @@ github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxB github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= @@ -60,15 +261,16 @@ github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= -github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alessio/shellescape v1.2.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= +github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= @@ -76,39 +278,53 @@ github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:l github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.48.10 h1:0LIFG3wp2Dt6PsxKWCg1Y1xRrn2vZnW5/gWdgaBalKg= +github.com/aws/aws-sdk-go v1.48.10/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= -github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng= -github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= -github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ= -github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= -github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= -github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/c9s/goprocinfo v0.0.0-20170724085704-0010a05ce49f h1:tRk+aBit+q3oqnj/1mF5HHhP2yxJM2lSa0afOJxQ3nE= +github.com/c9s/goprocinfo v0.0.0-20170724085704-0010a05ce49f/go.mod h1:uEyr4WpAH4hio6LFriaPkL938XnrvLpNPmQHBdrmbIE= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v1.0.3 h1:9liNh8t+u26xl5ddmWLmsOsdNLwkdRTg5AG+JnTiM80= github.com/chai2010/gettext-go v1.0.3/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b h1:ga8SEFjZ60pxLcmhnThWgvH2wg8376yUJmPhEH4H3kw= github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= -github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= -github.com/containerd/cgroups/v3 v3.0.2 h1:f5WFqIVSgo5IZmtTT3qVBo6TzI1ON6sycSBKkymb9L0= -github.com/containerd/cgroups/v3 v3.0.2/go.mod h1:JUgITrzdFqp42uI2ryGA+ge0ap/nxzYgkGmIcetmErE= +github.com/containerd/cgroups/v3 v3.0.3 h1:S5ByHZ/h9PMe5IOQoN7E+nMc2UcLEM/V48DGDJ9kip0= +github.com/containerd/cgroups/v3 v3.0.3/go.mod h1:8HBe7V3aWGLFPd/k03swSIsGjZhHI2WzJmticMgVuz0= github.com/containerd/containerd v1.7.17 h1:KjNnn0+tAVQHAoaWRjmdak9WlvnFR/8rU1CHHy8Rm2A= github.com/containerd/containerd v1.7.17/go.mod h1:vK+hhT4TIv2uejlcDlbVIc8+h/BqtKLIyNrtCZol8lI= github.com/containerd/continuity v0.4.2 h1:v3y/4Yz5jwnvqPKJJ+7Wf93fyWoCB3F5EclWG023MDM= @@ -119,6 +335,14 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU= github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk= +github.com/containers/image/v5 v5.31.1 h1:3x9soI6Biml/GiDLpkSmKrkRSwVGctxu/vONpoUdklA= +github.com/containers/image/v5 v5.31.1/go.mod h1:5QfOqSackPkSbF7Qxc1DnVNnPJKQ+KWLkfEfDpK590Q= +github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 h1:Qzk5C6cYglewc+UyGf6lc8Mj2UaPTHy/iF2De0/77CA= +github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY= +github.com/containers/ocicrypt v1.1.10 h1:r7UR6o8+lyhkEywetubUUgcKFjOWOaWz8cEBrCPX0ic= +github.com/containers/ocicrypt v1.1.10/go.mod h1:YfzSSr06PTHQwSTUKqDSjish9BeW1E4HUmreluQcMd8= +github.com/containers/storage v1.54.0 h1:xwYAlf6n9OnIlURQLLg3FYHbO74fQ/2W2N6EtQEUM4I= +github.com/containers/storage v1.54.0/go.mod h1:PlMOoinRrBSnhYODLxt4EXl0nmJt+X0kjG0Xdt9fMTw= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= @@ -127,6 +351,9 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= @@ -145,9 +372,11 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2 h1:aBfCb7iqHmDEIp6fBvC/hQUddQfg+3qdYjwzaiP9Hnc= -github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2/go.mod h1:WHNsWjnIn2V1LYOrME7e8KxSeKunYHsxEm4am0BUtcI= +github.com/distribution/distribution/v3 v3.0.0-alpha.1 h1:jn7I1gvjOvmLztH1+1cLiUFud7aeJCIQcgzugtwjyJo= +github.com/distribution/distribution/v3 v3.0.0-alpha.1/go.mod h1:LCp4JZp1ZalYg0W/TN05jarCQu+h4w7xc7ZfQF4Y/cY= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/cli v27.0.3+incompatible h1:usGs0/BoBW8MWxGeEtqPMkzOY56jZ6kYlSN5BLDioCQ= @@ -169,8 +398,8 @@ github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4= -github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= +github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo= @@ -182,7 +411,15 @@ github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/go-control-plane v0.12.1-0.20240612043845-c54ec4ce422d h1:RopQsG28t61pLLZRkwzwBsi60yDsOP8RvW47A3eAcGo= github.com/envoyproxy/go-control-plane v0.12.1-0.20240612043845-c54ec4ce422d/go.mod h1:5Wkq+JduFtdAXihLmeTJf+tRYIT4KBc2vPXDhwVo1pA= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= @@ -220,6 +457,9 @@ github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -236,6 +476,9 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-logr/zapr v0.1.0/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= @@ -285,8 +528,11 @@ github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4= -github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-redis/redis/v7 v7.4.1 h1:PASvf36gyUpr2zdOUS/9Zqc80GbM+9BDyiJSJDDOrTI= +github.com/go-redis/redis/v7 v7.4.1/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= @@ -294,32 +540,57 @@ github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZ github.com/gobuffalo/flect v0.2.0/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k= -github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= @@ -332,34 +603,87 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-containerregistry v0.19.2 h1:TannFKE1QSajsP6hPWb5oJNgKe1IKjHukIKDUmvsV6w= github.com/google/go-containerregistry v0.19.2/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI= +github.com/google/go-intervals v0.0.2 h1:FGrVEiUnTRKR8yE04qzXYaJMtnIYqobR5QbblK3ixcM= +github.com/google/go-intervals v0.0.2/go.mod h1:MkaR3LNRfeKLPmqgJYs4E66z5InYjmCjbbr4TQlcT6Y= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= +github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg= github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= +github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= +github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA= +github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.3.1/go.mod h1:on+2t9HRStVgn95RSsFWFz+6Q0Snyqv1awfrALZdbtU= +github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= -github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= -github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= -github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -377,28 +701,57 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-getter v1.7.5 h1:dT58k9hQ/vbxNMwoI5+xFYAJuv6152UNvdHokfI5wE4= +github.com/hashicorp/go-getter v1.7.5/go.mod h1:W7TalhMmbPmsSMdNjD0ZskARur/9GJ17cfHTRtXV744= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= +github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw= +github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU= +github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= +github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= @@ -413,6 +766,7 @@ github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= @@ -420,8 +774,11 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= -github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= +github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= +github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -432,6 +789,8 @@ github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= @@ -441,9 +800,16 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/longhorn/go-iscsi-helper v0.0.0-20210330030558-49a327fb024e h1:hz4quJkaJWDo+xW+G6wTF6d6/95QvJ+o2D0+bB/tJ1U= +github.com/longhorn/go-iscsi-helper v0.0.0-20210330030558-49a327fb024e/go.mod h1:9z/y9glKmWEdV50tjlUPxFwi1goQfIrrsoZbnMyIZbY= +github.com/longhorn/nsfilelock v0.0.0-20200723175406-fa7c83ad0003/go.mod h1:0CLeXlf59Lg6C0kjLSDf47ft73Dh37CwymYRKWwAn04= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lyft/gostats v0.4.14 h1:xmP4yMfDvEKtlNZEcS2sYz0cvnps1ri337ZEEbw3ab8= github.com/lyft/gostats v0.4.14/go.mod h1:cJWqEVL8JIewIJz/olUIios2F1q06Nc51hXejPQmBH0= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -463,22 +829,31 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= -github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= -github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA= +github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs= github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ= +github.com/mistifyio/go-zfs/v3 v3.0.1 h1:YaoXgBePoMA12+S1u/ddkv+QqxcfiZK4prI6HPnkFiU= +github.com/mistifyio/go-zfs/v3 v3.0.1/go.mod h1:CzVgeB0RvF2EGzQnytKVvVSDwmKJXxkOTUGbNrTja/k= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= @@ -490,8 +865,8 @@ github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkV github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= -github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= -github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/moby/sys/mountinfo v0.7.1 h1:/tTvQaSJRr2FshkhXiIpux6fQ2Zvc4j7tAhMTStAG2g= +github.com/moby/sys/mountinfo v0.7.1/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= @@ -518,14 +893,18 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.17.2 h1:7eMhcy3GimbsA3hEnVKdw/PQM9XN9krpKVXsZdph0/g= github.com/onsi/ginkgo/v2 v2.17.2/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= @@ -539,13 +918,23 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= +github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU= +github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= +github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f h1:/UDgs8FGMqwnHagNDPGOlts35QkhAZ8by3DR7nMih7M= +github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f/go.mod h1:J6OG6YJVEWopen4avK3VNQSnALmmjvniMmni/YFYAwc= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -555,6 +944,8 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= @@ -584,10 +975,20 @@ github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho= +github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U= +github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc= +github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ= +github.com/redis/go-redis/v9 v9.1.0 h1:137FnGdk+EQdCbye1FW+qOEcY5S+SpY9T0NiuqvtfMY= +github.com/redis/go-redis/v9 v9.1.0/go.mod h1:urWj3He21Dj5k4TK1y59xH8Uj6ATueP8AH1cY3lZl4c= +github.com/replicatedhq/troubleshoot v0.95.1-0.20240707233129-f5f02f5a807c h1:MS153cTX5j0/VNMLtOk7pwcW8qEpr9+rL5pP2PMqr38= +github.com/replicatedhq/troubleshoot v0.95.1-0.20240707233129-f5f02f5a807c/go.mod h1:ghle+cwNow+SgGMGZ3jRouRbAbT22uVpKbJSxNvNc00= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rubenv/sql-migrate v1.6.1 h1:bo6/sjsan9HaXAsNxYP/jCEDUGibHp8JmOBw7NTGRos= @@ -596,21 +997,40 @@ github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y= +github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= +github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= +github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= -github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= @@ -628,11 +1048,15 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -641,13 +1065,29 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/sylabs/sif/v2 v2.16.0 h1:2eqaBaQQsn5DZTzm3QZm0HupZQEjNXfxRnCmtyCihEU= +github.com/sylabs/sif/v2 v2.16.0/go.mod h1:d5TxgD/mhMUU3kWLmZmWJQ99Wg0asaTP0bq3ezR1xpg= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes= +github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= github.com/telepresenceio/telepresence/rpc/v2 v2.6.8 h1:q5V85LBT9bA/c4YPa/kMvJGyKZDgBPJTftlAMqJx7j4= github.com/telepresenceio/telepresence/rpc/v2 v2.6.8/go.mod h1:VlgfRoXaW6Tl8IZbHmMWhITne8HY09/wOFtABHGj3ic= github.com/telepresenceio/watchable v0.0.0-20220726211108-9bb86f92afa7 h1:GMw3nEaOVyi+tNiGko5kAeRtoiEIpXNHmISyZ7fpw14= github.com/telepresenceio/watchable v0.0.0-20220726211108-9bb86f92afa7/go.mod h1:ihJ97e2gsd8GuzFF/I3B1qcik3XZLpXjumQifXi8Slg= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tsaarni/certyaml v0.9.3 h1:m8HHbuUzWVUOmv8IQU9HgVZZ8r5ICExKm++54DJKCs0= @@ -656,10 +1096,15 @@ github.com/tsaarni/x500dn v1.0.0 h1:LvaWTkqRpse4VHBhB5uwf3wytokK4vF9IOyNAEyiA+U= github.com/tsaarni/x500dn v1.0.0/go.mod h1:QaHa3EcUKC4dfCAZmj8+ZRGLKukWgpGv9H3oOCsAbcE= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= +github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts= github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk= github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= +github.com/vmware-tanzu/velero v1.14.0 h1:ZYy9TLtokdHInIdWTfwHYIZhRr+xLd0nGzHyQrXMCIM= +github.com/vmware-tanzu/velero v1.14.0/go.mod h1:yeGs7/xq35yOGDPCV0ryxoybQBsTLXmrxwzXBXtiwp8= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -671,15 +1116,14 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTNSBd8DVTNGHx7o/IKu9HYDvLMffDhbyLccI= -github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= -github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMztlGpl/VA+Zm1AcTPHYkHJPbHqE6WJUXE= -github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= -github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1:ERexzlUfuTvpE74urLSbIQW0Z/6hF9t8U4NsJLaioAY= -github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= @@ -687,24 +1131,38 @@ go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qL go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 h1:x8Z78aZx8cOF0+Kkazoc7lwUNMGy0LrzEMxTm4BbTxg= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0/go.mod h1:62CPTSry9QZtOaSsE3tOzhx6LzDhHnXJ6xHeMNNiM6Q= +go.opentelemetry.io/contrib/exporters/autoexport v0.46.1 h1:ysCfPZB9AjUlMa1UHYup3c9dAOCMQX/6sxSfPBUoxHw= +go.opentelemetry.io/contrib/exporters/autoexport v0.46.1/go.mod h1:ha0aiYm+DOPsLHjh0zoQ8W8sLT+LJ58J3j47lGpSLrU= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0 h1:U2guen0GhqH8o/G2un8f/aG/y++OuW6MyCo6hT9prXk= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0/go.mod h1:yeGZANgEcpdx/WK0IvvRFC+2oLiMS2u4L/0Rj2M2Qr0= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.28.0 h1:aLmmtjRke7LPDQ3lvpFz+kNEH43faFhzW7v8BFIEydg= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.28.0/go.mod h1:TC1pyCt6G9Sjb4bQpShH+P5R53pO6ZuGnHuuln9xMeE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 h1:digkEZCJWobwBqMwC0cwCq8/wkkRy/OowZg5OArWZrM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0/go.mod h1:/OpE/y70qVkndM0TrxT4KBoN3RsFZP0QaofcfYrj76I= go.opentelemetry.io/otel/exporters/prometheus v0.50.0 h1:2Ewsda6hejmbhGFyUvWZjUThC98Cf8Zy6g0zkIimOng= go.opentelemetry.io/otel/exporters/prometheus v0.50.0/go.mod h1:pMm5PkUo5YwbLiuEf7t2xg4wbP0/eSJrMxIMxKosynY= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.28.0 h1:BJee2iLkfRfl9lc7aFmBwkWxY/RI1RDdXepSF6y8TPE= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.28.0/go.mod h1:DIzlHs3DRscCIBU3Y9YSzPfScwnYnzfnCd4g8zA7bZc= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0 h1:VhlEQAPp9R1ktYfrPk5SOryw1e9LDDTZCbIPFrho0ec= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0/go.mod h1:kB3ufRbfU+CQ4MlUcqtW8Z7YEOBeK2DJ6CmR5rYYF3E= go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= @@ -713,6 +1171,7 @@ go.opentelemetry.io/otel/sdk/metric v1.28.0 h1:OkuaKgKrgAbYrrY0t92c+cC+2F6hsFNnC go.opentelemetry.io/otel/sdk/metric v1.28.0/go.mod h1:cWPjykihLAPvXKi4iZc1dpER3Jdq2Z0YLse3moQUCpg= go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.starlark.net v0.0.0-20240520160348-046347dcd104 h1:3qhteRISupnJvaWshOmeqEUs2y9oc/+/ePPvDh3Eygg= @@ -732,6 +1191,8 @@ golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -739,19 +1200,47 @@ golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= -golang.org/x/crypto/x509roots/fallback v0.0.0-20240604170348-d4e7c9cb6cb8 h1:+kWDWI3Eb5cPIOr4cP+R2RLDwK3/dXppL+7XmSOh2LA= -golang.org/x/crypto/x509roots/fallback v0.0.0-20240604170348-d4e7c9cb6cb8/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/crypto/x509roots/fallback v0.0.0-20240626151235-a6a393ffd658 h1:i7K6wQLN/0oxF7FT3tKkfMCstxoT4VGG36YIB9ZKLzI= +golang.org/x/crypto/x509roots/fallback v0.0.0-20240626151235-a6a393ffd658/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= @@ -767,23 +1256,82 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A= golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -792,8 +1340,14 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -806,42 +1360,106 @@ golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190524152521-dbbf3f1254d4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= @@ -849,6 +1467,7 @@ golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -859,14 +1478,58 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= @@ -874,55 +1537,262 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gomodules.xyz/jsonpatch/v2 v2.0.1/go.mod h1:IhYNNY4jnS53ZnfE4PAmpKtDpTCj1JFXc+3mwe7XcUU= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= +google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= +google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= +google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= +google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= +google.golang.org/api v0.172.0 h1:/1OcMZGPmW1rX2LCu2CmGUD1KXK1+pfzxotxyRUCCdk= +google.golang.org/api v0.172.0/go.mod h1:+fJZq6QXWfa9pXhnIzsjx4yI22d4aI9ZpLb58gvXjis= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= +google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= +google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= +google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= +google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221025140454-527a21cfbd71/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0= google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw= google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= -google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20160105164936-4f90aeace3a2/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -935,13 +1805,17 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= -gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= -gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= -helm.sh/helm/v3 v3.15.2 h1:/3XINUFinJOBjQplGnjw92eLGpgXXp1L8chWPkCkDuw= -helm.sh/helm/v3 v3.15.2/go.mod h1:FzSIP8jDQaa6WAVg9F+OkKz7J0ZmAga4MABtTbsb9WQ= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +helm.sh/helm/v3 v3.15.3 h1:HcZDaVFe9uHa6hpsR54mJjYyRy4uz/pc6csg27nxFOc= +helm.sh/helm/v3 v3.15.3/go.mod h1:FzSIP8jDQaa6WAVg9F+OkKz7J0ZmAga4MABtTbsb9WQ= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/api v0.18.2/go.mod h1:SJCWI7OLzhZSvbY7U8zwNl9UA4o1fizoug34OV/2r78= k8s.io/api v0.18.4/go.mod h1:lOIQAKYgai1+vz9J7YcDZwC26Z0zQewYOGWdyIPUUQ4= k8s.io/api v0.30.2 h1:+ZhRj+28QT4UOH+BKznu4CBgPWgkXO7XAvMcMl0qKvI= @@ -976,20 +1850,29 @@ k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUc k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= -k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E= k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E= k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a h1:zD1uj3Jf+mD4zmA7W+goE5TxDkI7OGJjBNBzq5fJtLA= k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a/go.mod h1:UxDHUPsUwTOOxSU+oXURfFBcAS6JwiRXTYqYwfuGowc= k8s.io/kubectl v0.30.2 h1:cgKNIvsOiufgcs4yjvgkK0+aPCfa8pUwzXdJtkbhsH8= k8s.io/kubectl v0.30.2/go.mod h1:rz7GHXaxwnigrqob0lJsiA07Df8RE3n1TSaC2CTeuB4= +k8s.io/kubelet v0.30.2 h1:Ck4E/pHndI20IzDXxS57dElhDGASPO5pzXF7BcKfmCY= +k8s.io/kubelet v0.30.2/go.mod h1:DSwwTbLQmdNkebAU7ypIALR4P9aXZNFwgRmedojUE94= +k8s.io/metrics v0.30.2 h1:zj4kIPTCfEbY0RHEogpA7QtlItU7xaO11+Gz1zVDxlc= +k8s.io/metrics v0.30.2/go.mod h1:GpoO5XTy/g8CclVLtgA5WTrr2Cy5vCsqr5Xa/0ETWIk= k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= k8s.io/utils v0.0.0-20200603063816-c1c6865ac451/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 h1:jgGTlFYnhF1PM1Ax/lAlxUPE+KfCIXHaathvJg1C3ak= k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= oras.land/oras-go v1.2.5 h1:XpYuAwAb0DfQsunIyMfeET92emK8km3W4yEzZvUbsTo= oras.land/oras-go v1.2.5/go.mod h1:PuAwRShRZCsZb7g8Ar3jKKQR/2A/qN+pkYxIOd/FAoo= +periph.io/x/host/v3 v3.8.2 h1:ayKUDzgUCN0g8+/xM9GTkWaOBhSLVcVHGTfjAOi8OsQ= +periph.io/x/host/v3 v3.8.2/go.mod h1:yFL76AesNHR68PboofSWYaQTKmvPXsQH2Apvp/ls/K4= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.7/go.mod h1:PHgbrJT7lCHcxMU+mDHEm+nx46H4zuuHZkDP6icnhu0= sigs.k8s.io/controller-runtime v0.6.1/go.mod h1:XRYBPdbf5XJu9kpS84VJiZ7h/u1hF3gEORz0efEja7A= sigs.k8s.io/controller-runtime v0.18.4 h1:87+guW1zhvuPLh1PHybKdYFLU0YJp4FhJRmiHvm5BZw= diff --git a/internal/cmd/egctl/collect.go b/internal/cmd/egctl/collect.go new file mode 100644 index 00000000000..e4eece22e53 --- /dev/null +++ b/internal/cmd/egctl/collect.go @@ -0,0 +1,103 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package egctl + +import ( + "context" + "fmt" + "os" + "os/signal" + "path/filepath" + "strings" + "syscall" + "time" + + "github.com/replicatedhq/troubleshoot/pkg/convert" + "github.com/spf13/cobra" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + + "github.com/envoyproxy/gateway/internal/cmd/options" + tb "github.com/envoyproxy/gateway/internal/troubleshoot" +) + +type collectOptions struct { + outPath string + envoyGatewayNamespace string +} + +func newCollectCommand() *cobra.Command { + collectOpts := &collectOptions{} + collectCommand := &cobra.Command{ + Use: "collect", + Short: "Collect configurations from the cluster to help diagnose any issues offline", + Example: ` # Collect configurations from current context. + egctl experimental collect + `, + Run: func(c *cobra.Command, args []string) { + cmdutil.CheckErr(runCollect(*collectOpts)) + }, + } + + flags := collectCommand.Flags() + options.AddKubeConfigFlags(flags) + + collectCommand.PersistentFlags().StringVarP(&collectOpts.outPath, "output", "o", "", + "Specify the output file path for collected data. If not specified, a timestamped file will be created in the current directory.") + collectCommand.PersistentFlags().StringVarP(&collectOpts.envoyGatewayNamespace, "envoy-system-namespace", "", "envoy-gateway-system", + "Specify the namespace where the Envoy Gateway controller is installed.") + + return collectCommand +} + +func runCollect(collectOpts collectOptions) error { + cc := options.DefaultConfigFlags.ToRawKubeConfigLoader() + restConfig, err := cc.ClientConfig() + if err != nil { + return err + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Hour) + defer cancel() + go waitForSignal(ctx, cancel) + + tmpDir, err := os.MkdirTemp("", "envoy-gateway-support-bundle") + if err != nil { + return fmt.Errorf("create temp dir: %w", err) + } + defer func(path string) { + _ = os.RemoveAll(path) + }(tmpDir) + + basename := "" + if collectOpts.outPath != "" { + // use override output path + overridePath, err := convert.ValidateOutputPath(collectOpts.outPath) + if err != nil { + return fmt.Errorf("override output file path: %w", err) + } + basename = strings.TrimSuffix(overridePath, ".tar.gz") + } else { + // use default output path + basename = fmt.Sprintf("envoy-gateway-%s", time.Now().Format("2006-01-02T15_04_05")) + } + bundlePath := filepath.Join(tmpDir, strings.TrimSuffix(basename, ".tar.gz")) + if err := os.MkdirAll(bundlePath, 0o777); err != nil { + return fmt.Errorf("create bundle dir: %w", err) + } + + result := tb.CollectResult(ctx, restConfig, bundlePath, collectOpts.envoyGatewayNamespace) + return result.ArchiveSupportBundle(bundlePath, fmt.Sprintf("%s.tar.gz", basename)) +} + +func waitForSignal(c context.Context, cancel context.CancelFunc) { + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + select { + case <-c.Done(): + case <-sigCh: + cancel() + } +} diff --git a/internal/cmd/egctl/experimental.go b/internal/cmd/egctl/experimental.go index 420dc3de526..70f46650ff3 100644 --- a/internal/cmd/egctl/experimental.go +++ b/internal/cmd/egctl/experimental.go @@ -28,6 +28,7 @@ func newExperimentalCommand() *cobra.Command { experimentalCommand.AddCommand(newDashboardCommand()) experimentalCommand.AddCommand(newInstallCommand()) experimentalCommand.AddCommand(newUnInstallCommand()) + experimentalCommand.AddCommand(newCollectCommand()) return experimentalCommand } diff --git a/internal/cmd/egctl/testdata/translate/out/default-resources.all.yaml b/internal/cmd/egctl/testdata/translate/out/default-resources.all.yaml index ac5bde3ed57..86df2f9de4c 100644 --- a/internal/cmd/egctl/testdata/translate/out/default-resources.all.yaml +++ b/internal/cmd/egctl/testdata/translate/out/default-resources.all.yaml @@ -196,29 +196,6 @@ gatewayClass: reason: Accepted status: "True" type: Accepted - supportedFeatures: - - GRPCRoute - - Gateway - - GatewayPort8080 - - HTTPRoute - - HTTPRouteBackendProtocolH2C - - HTTPRouteBackendProtocolWebSocket - - HTTPRouteBackendTimeout - - HTTPRouteDestinationPortMatching - - HTTPRouteHostRewrite - - HTTPRouteMethodMatching - - HTTPRouteParentRefPort - - HTTPRoutePathRedirect - - HTTPRoutePathRewrite - - HTTPRoutePortRedirect - - HTTPRouteQueryParamMatching - - HTTPRouteRequestMirror - - HTTPRouteRequestMultipleMirrors - - HTTPRouteRequestTimeout - - HTTPRouteResponseHeaderModification - - HTTPRouteSchemeRedirect - - ReferenceGrant - - TLSRoute gateways: - metadata: creationTimestamp: null diff --git a/internal/cmd/egctl/testdata/translate/out/echo-gateway-api.cluster.yaml b/internal/cmd/egctl/testdata/translate/out/echo-gateway-api.cluster.yaml index 85b5c588ecb..3d88f20f51d 100644 --- a/internal/cmd/egctl/testdata/translate/out/echo-gateway-api.cluster.yaml +++ b/internal/cmd/egctl/testdata/translate/out/echo-gateway-api.cluster.yaml @@ -12,29 +12,6 @@ gatewayClass: reason: Accepted status: "True" type: Accepted - supportedFeatures: - - GRPCRoute - - Gateway - - GatewayPort8080 - - HTTPRoute - - HTTPRouteBackendProtocolH2C - - HTTPRouteBackendProtocolWebSocket - - HTTPRouteBackendTimeout - - HTTPRouteDestinationPortMatching - - HTTPRouteHostRewrite - - HTTPRouteMethodMatching - - HTTPRouteParentRefPort - - HTTPRoutePathRedirect - - HTTPRoutePathRewrite - - HTTPRoutePortRedirect - - HTTPRouteQueryParamMatching - - HTTPRouteRequestMirror - - HTTPRouteRequestMultipleMirrors - - HTTPRouteRequestTimeout - - HTTPRouteResponseHeaderModification - - HTTPRouteSchemeRedirect - - ReferenceGrant - - TLSRoute gateways: - metadata: creationTimestamp: null diff --git a/internal/cmd/egctl/testdata/translate/out/echo-gateway-api.route.json b/internal/cmd/egctl/testdata/translate/out/echo-gateway-api.route.json index 663ce5404e7..41dfd6683e7 100644 --- a/internal/cmd/egctl/testdata/translate/out/echo-gateway-api.route.json +++ b/internal/cmd/egctl/testdata/translate/out/echo-gateway-api.route.json @@ -17,30 +17,6 @@ "reason": "Accepted", "message": "Valid GatewayClass" } - ], - "supportedFeatures": [ - "GRPCRoute", - "Gateway", - "GatewayPort8080", - "HTTPRoute", - "HTTPRouteBackendProtocolH2C", - "HTTPRouteBackendProtocolWebSocket", - "HTTPRouteBackendTimeout", - "HTTPRouteDestinationPortMatching", - "HTTPRouteHostRewrite", - "HTTPRouteMethodMatching", - "HTTPRouteParentRefPort", - "HTTPRoutePathRedirect", - "HTTPRoutePathRewrite", - "HTTPRoutePortRedirect", - "HTTPRouteQueryParamMatching", - "HTTPRouteRequestMirror", - "HTTPRouteRequestMultipleMirrors", - "HTTPRouteRequestTimeout", - "HTTPRouteResponseHeaderModification", - "HTTPRouteSchemeRedirect", - "ReferenceGrant", - "TLSRoute" ] } }, diff --git a/internal/cmd/egctl/testdata/translate/out/invalid-envoyproxy.all.yaml b/internal/cmd/egctl/testdata/translate/out/invalid-envoyproxy.all.yaml index df303817570..81df01be9e4 100644 --- a/internal/cmd/egctl/testdata/translate/out/invalid-envoyproxy.all.yaml +++ b/internal/cmd/egctl/testdata/translate/out/invalid-envoyproxy.all.yaml @@ -38,29 +38,6 @@ gatewayClass: reason: InvalidParameters status: "False" type: Accepted - supportedFeatures: - - GRPCRoute - - Gateway - - GatewayPort8080 - - HTTPRoute - - HTTPRouteBackendProtocolH2C - - HTTPRouteBackendProtocolWebSocket - - HTTPRouteBackendTimeout - - HTTPRouteDestinationPortMatching - - HTTPRouteHostRewrite - - HTTPRouteMethodMatching - - HTTPRouteParentRefPort - - HTTPRoutePathRedirect - - HTTPRoutePathRewrite - - HTTPRoutePortRedirect - - HTTPRouteQueryParamMatching - - HTTPRouteRequestMirror - - HTTPRouteRequestMultipleMirrors - - HTTPRouteRequestTimeout - - HTTPRouteResponseHeaderModification - - HTTPRouteSchemeRedirect - - ReferenceGrant - - TLSRoute gateways: - metadata: creationTimestamp: null diff --git a/internal/cmd/egctl/testdata/translate/out/rejected-http-route.route.yaml b/internal/cmd/egctl/testdata/translate/out/rejected-http-route.route.yaml index 1dd8cc46a93..c578d14aef5 100644 --- a/internal/cmd/egctl/testdata/translate/out/rejected-http-route.route.yaml +++ b/internal/cmd/egctl/testdata/translate/out/rejected-http-route.route.yaml @@ -12,29 +12,6 @@ gatewayClass: reason: Accepted status: "True" type: Accepted - supportedFeatures: - - GRPCRoute - - Gateway - - GatewayPort8080 - - HTTPRoute - - HTTPRouteBackendProtocolH2C - - HTTPRouteBackendProtocolWebSocket - - HTTPRouteBackendTimeout - - HTTPRouteDestinationPortMatching - - HTTPRouteHostRewrite - - HTTPRouteMethodMatching - - HTTPRouteParentRefPort - - HTTPRoutePathRedirect - - HTTPRoutePathRewrite - - HTTPRoutePortRedirect - - HTTPRouteQueryParamMatching - - HTTPRouteRequestMirror - - HTTPRouteRequestMultipleMirrors - - HTTPRouteRequestTimeout - - HTTPRouteResponseHeaderModification - - HTTPRouteSchemeRedirect - - ReferenceGrant - - TLSRoute gateways: - metadata: creationTimestamp: null diff --git a/internal/cmd/egctl/testdata/translate/out/valid-envoyproxy.all.yaml b/internal/cmd/egctl/testdata/translate/out/valid-envoyproxy.all.yaml index 09fb3051c14..638390ff440 100644 --- a/internal/cmd/egctl/testdata/translate/out/valid-envoyproxy.all.yaml +++ b/internal/cmd/egctl/testdata/translate/out/valid-envoyproxy.all.yaml @@ -31,29 +31,6 @@ gatewayClass: reason: Accepted status: "True" type: Accepted - supportedFeatures: - - GRPCRoute - - Gateway - - GatewayPort8080 - - HTTPRoute - - HTTPRouteBackendProtocolH2C - - HTTPRouteBackendProtocolWebSocket - - HTTPRouteBackendTimeout - - HTTPRouteDestinationPortMatching - - HTTPRouteHostRewrite - - HTTPRouteMethodMatching - - HTTPRouteParentRefPort - - HTTPRoutePathRedirect - - HTTPRoutePathRewrite - - HTTPRoutePortRedirect - - HTTPRouteQueryParamMatching - - HTTPRouteRequestMirror - - HTTPRouteRequestMultipleMirrors - - HTTPRouteRequestTimeout - - HTTPRouteResponseHeaderModification - - HTTPRouteSchemeRedirect - - ReferenceGrant - - TLSRoute gateways: - metadata: creationTimestamp: null diff --git a/internal/cmd/egctl/translate_test.go b/internal/cmd/egctl/translate_test.go index 28d07afc977..43fac41ebdd 100644 --- a/internal/cmd/egctl/translate_test.go +++ b/internal/cmd/egctl/translate_test.go @@ -22,7 +22,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/yaml" - "github.com/envoyproxy/gateway/internal/gatewayapi/status" "github.com/envoyproxy/gateway/internal/utils/field" "github.com/envoyproxy/gateway/internal/utils/file" ) @@ -360,9 +359,10 @@ func TestTranslate(t *testing.T) { // Supported features are dynamic, instead of hard-coding them in the output files // we define them here. - if want.GatewayClass != nil { - want.GatewayClass.Status.SupportedFeatures = status.GatewaySupportedFeatures - } + // Disabled until GatewayClass.Status.SupportedFeatures is stable + // if want.GatewayClass != nil { + // want.GatewayClass.Status.SupportedFeatures = status.GatewaySupportedFeatures + // } opts := cmpopts.IgnoreFields(metav1.Condition{}, "LastTransitionTime") require.Empty(t, cmp.Diff(want, got, opts)) diff --git a/internal/gatewayapi/backendtlspolicy.go b/internal/gatewayapi/backendtlspolicy.go index 87682425d6b..28f77bf825b 100644 --- a/internal/gatewayapi/backendtlspolicy.go +++ b/internal/gatewayapi/backendtlspolicy.go @@ -69,7 +69,7 @@ func (t *Translator) processBackendTLSPolicy( if envoyProxy.Spec.BackendTLS.MinVersion != nil { tlsBundle.MinVersion = ptr.To(ir.TLSVersion(*envoyProxy.Spec.BackendTLS.MinVersion)) } - if envoyProxy.Spec.BackendTLS.MinVersion != nil { + if envoyProxy.Spec.BackendTLS.MaxVersion != nil { tlsBundle.MaxVersion = ptr.To(ir.TLSVersion(*envoyProxy.Spec.BackendTLS.MaxVersion)) } if len(envoyProxy.Spec.BackendTLS.ALPNProtocols) > 0 { diff --git a/internal/gatewayapi/conformance/suite.go b/internal/gatewayapi/conformance/suite.go index 33d8b61e9e4..4637e023779 100644 --- a/internal/gatewayapi/conformance/suite.go +++ b/internal/gatewayapi/conformance/suite.go @@ -15,8 +15,7 @@ import ( // SkipTests is a list of tests that are skipped in the conformance suite. var SkipTests = []suite.ConformanceTest{ tests.GatewayStaticAddresses, - tests.GatewayHTTPListenerIsolation, // https://github.com/kubernetes-sigs/gateway-api/issues/3352 - tests.HTTPRouteBackendRequestHeaderModifier, // https://github.com/envoyproxy/gateway/issues/3338 + tests.GatewayHTTPListenerIsolation, // https://github.com/envoyproxy/gateway/issues/3352 } func skipTestsShortNames(skipTests []suite.ConformanceTest) []string { diff --git a/internal/gatewayapi/envoyextensionpolicy.go b/internal/gatewayapi/envoyextensionpolicy.go index 9d1b0f9229e..081e8f641a2 100644 --- a/internal/gatewayapi/envoyextensionpolicy.go +++ b/internal/gatewayapi/envoyextensionpolicy.go @@ -321,7 +321,7 @@ func (t *Translator) translateEnvoyExtensionPolicyForRoute( ) if wasms, err = t.buildWasms(policy, resources); err != nil { - err = perr.WithMessage(err, "WASM") + err = perr.WithMessage(err, "Wasm") errs = errors.Join(errs, err) } @@ -351,8 +351,10 @@ func (t *Translator) translateEnvoyExtensionPolicyForRoute( if irListener != nil { for _, r := range irListener.Routes { if strings.HasPrefix(r.Name, prefix) { - r.ExtProcs = extProcs - r.Wasms = wasms + r.EnvoyExtensions = &ir.EnvoyExtensionFeatures{ + ExtProcs: extProcs, + Wasms: wasms, + } } } } @@ -380,7 +382,7 @@ func (t *Translator) translateEnvoyExtensionPolicyForGateway( errs = errors.Join(errs, err) } if wasms, err = t.buildWasms(policy, resources); err != nil { - err = perr.WithMessage(err, "WASM") + err = perr.WithMessage(err, "Wasm") errs = errors.Join(errs, err) } @@ -405,16 +407,13 @@ func (t *Translator) translateEnvoyExtensionPolicyForGateway( // targeting a lesser specific scope(Gateway). for _, r := range http.Routes { // if already set - there's a route level policy, so skip - if r.ExtProcs != nil || - r.Wasms != nil { + if r.EnvoyExtensions != nil { continue } - if r.ExtProcs == nil { - r.ExtProcs = extProcs - } - if r.Wasms == nil { - r.Wasms = wasms + r.EnvoyExtensions = &ir.EnvoyExtensionFeatures{ + ExtProcs: extProcs, + Wasms: wasms, } } } diff --git a/internal/gatewayapi/ext_service.go b/internal/gatewayapi/ext_service.go index 3574e8249ef..d0c50e5f97e 100644 --- a/internal/gatewayapi/ext_service.go +++ b/internal/gatewayapi/ext_service.go @@ -82,7 +82,7 @@ func (t *Translator) processExtServiceDestination( return ds, nil } -// TODO: also refer to extension type, as WASM may also introduce destinations +// TODO: also refer to extension type, as Wasm may also introduce destinations func irIndexedExtServiceDestinationName(policyNamespacedName types.NamespacedName, policyKind string, idx int) string { return strings.ToLower(fmt.Sprintf( "%s/%s/%s/%d", diff --git a/internal/gatewayapi/extensionserverpolicy.go b/internal/gatewayapi/extensionserverpolicy.go index fc917d06a98..0f7c4f7524b 100644 --- a/internal/gatewayapi/extensionserverpolicy.go +++ b/internal/gatewayapi/extensionserverpolicy.go @@ -18,6 +18,7 @@ import ( gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" "github.com/envoyproxy/gateway/internal/gatewayapi/status" "github.com/envoyproxy/gateway/internal/ir" "github.com/envoyproxy/gateway/internal/utils" @@ -50,7 +51,7 @@ func (t *Translator) ProcessExtensionServerPolicies(policies []unstructured.Unst policy := policy.DeepCopy() var policyStatus gwapiv1a2.PolicyStatus accepted := false - targetRefs, err := extractTargetRefs(policy) + targetRefs, err := extractTargetRefs(policy, gateways) if err != nil { errs = errors.Join(errs, fmt.Errorf("error finding targetRefs for policy %s: %w", policy.GetName(), err)) continue @@ -96,55 +97,26 @@ func (t *Translator) ProcessExtensionServerPolicies(policies []unstructured.Unst return res, errs } -func extractTargetRefs(policy *unstructured.Unstructured) ([]gwapiv1a2.LocalPolicyTargetReferenceWithSectionName, error) { - ret := []gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{} +func extractTargetRefs(policy *unstructured.Unstructured, gateways []*GatewayContext) ([]gwapiv1a2.LocalPolicyTargetReferenceWithSectionName, error) { spec, found := policy.Object["spec"].(map[string]any) if !found { return nil, fmt.Errorf("no targets found for the policy") } - targetRefs, found := spec["targetRefs"] - if found { - if refArr, ok := targetRefs.([]any); ok { - for i := range refArr { - ref, err := extractSingleTargetRef(refArr[i]) - if err != nil { - return nil, err - } - ret = append(ret, ref) - } - } else { - return nil, fmt.Errorf("targetRefs is not an array") - } + specAsJSON, err := json.Marshal(spec) + if err != nil { + return nil, fmt.Errorf("no targets found for the policy") } - targetRef, found := spec["targetRef"] - if found { - ref, err := extractSingleTargetRef(targetRef) - if err != nil { - return nil, err - } - ret = append(ret, ref) + var targetRefs egv1a1.PolicyTargetReferences + if err := json.Unmarshal(specAsJSON, &targetRefs); err != nil { + return nil, fmt.Errorf("no targets found for the policy") } + ret := getPolicyTargetRefs(targetRefs, gateways) if len(ret) == 0 { return nil, fmt.Errorf("no targets found for the policy") } return ret, nil } -func extractSingleTargetRef(data any) (gwapiv1a2.LocalPolicyTargetReferenceWithSectionName, error) { - var currRef gwapiv1a2.LocalPolicyTargetReferenceWithSectionName - d, err := json.Marshal(data) - if err != nil { - return currRef, err - } - if err := json.Unmarshal(d, &currRef); err != nil { - return currRef, err - } - if currRef.Group == "" || currRef.Name == "" || currRef.Kind == "" { - return currRef, fmt.Errorf("invalid targetRef found: %s", string(d)) - } - return currRef, nil -} - func policyStatusToUnstructured(policyStatus gwapiv1a2.PolicyStatus) map[string]any { ret := map[string]any{} // No need to check the marshal/unmarshal error here diff --git a/internal/gatewayapi/extensionserverpolicy_test.go b/internal/gatewayapi/extensionserverpolicy_test.go index 6837f7e5ce8..f6ebf8e361c 100644 --- a/internal/gatewayapi/extensionserverpolicy_test.go +++ b/internal/gatewayapi/extensionserverpolicy_test.go @@ -10,7 +10,6 @@ import ( "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/utils/ptr" gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" ) @@ -41,7 +40,7 @@ func TestExtractTargetRefs(t *testing.T) { "targetRefs": "someValue", }, output: nil, - expectedError: "targetRefs is not an array", + expectedError: "no targets found for the policy", }, { desc: "invalid targetref", @@ -51,7 +50,7 @@ func TestExtractTargetRefs(t *testing.T) { }, }, output: nil, - expectedError: "invalid targetRef found: {\"someKey\":\"someValue\"}", + expectedError: "no targets found for the policy", }, { desc: "valid single targetRef", @@ -105,53 +104,6 @@ func TestExtractTargetRefs(t *testing.T) { }, }, }, - { - desc: "valid multiple targetRefs and targetRef", - specInput: map[string]any{ - "targetRefs": []any{ - map[string]any{ - "group": "some.group", - "kind": "SomeKind2", - "name": "othername", - }, - map[string]any{ - "group": "some.group", - "kind": "SomeKind", - "name": "name", - }, - }, - "targetRef": map[string]any{ - "group": "some.group", - "kind": "SomeKind", - "name": "three", - "sectionName": "one", - }, - }, - output: []gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{ - { - LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{ - Group: "some.group", - Kind: "SomeKind2", - Name: "othername", - }, - }, - { - LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{ - Group: "some.group", - Kind: "SomeKind", - Name: "name", - }, - }, - { - LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{ - Group: "some.group", - Kind: "SomeKind", - Name: "three", - }, - SectionName: ptr.To(gwapiv1a2.SectionName("one")), - }, - }, - }, } for _, currTest := range tests { @@ -160,7 +112,7 @@ func TestExtractTargetRefs(t *testing.T) { Object: map[string]any{}, } policy.Object["spec"] = currTest.specInput - targets, err := extractTargetRefs(policy) + targets, err := extractTargetRefs(policy, []*GatewayContext{}) if currTest.expectedError != "" { require.EqualError(t, err, currTest.expectedError) diff --git a/internal/gatewayapi/filters.go b/internal/gatewayapi/filters.go index 2630ed1cf84..b3d2ddb4074 100644 --- a/internal/gatewayapi/filters.go +++ b/internal/gatewayapi/filters.go @@ -825,7 +825,6 @@ func (t *Translator) processUnresolvedHTTPFilter(errMsg string, filterContext *H errMsg, ) filterContext.DirectResponse = &ir.DirectResponse{ - Body: &errMsg, StatusCode: 500, } } @@ -842,7 +841,6 @@ func (t *Translator) processUnsupportedHTTPFilter(filterType string, filterConte errMsg, ) filterContext.DirectResponse = &ir.DirectResponse{ - Body: &errMsg, StatusCode: 500, } } @@ -859,7 +857,6 @@ func (t *Translator) processInvalidHTTPFilter(filterType string, filterContext * errMsg, ) filterContext.DirectResponse = &ir.DirectResponse{ - Body: &errMsg, StatusCode: 500, } } diff --git a/internal/gatewayapi/helpers.go b/internal/gatewayapi/helpers.go index 269c968b4e0..22c81032ebb 100644 --- a/internal/gatewayapi/helpers.go +++ b/internal/gatewayapi/helpers.go @@ -109,7 +109,7 @@ func KindDerefOr(kind *gwapiv1.Kind, defaultKind string) string { // to a Gateway with the given namespace/name, irrespective of whether a // section/listener name has been specified (i.e. a parent ref to a listener // on the specified gateway will return "true"). -func IsRefToGateway(parentRef gwapiv1.ParentReference, gateway types.NamespacedName) bool { +func IsRefToGateway(routeNamespace gwapiv1.Namespace, parentRef gwapiv1.ParentReference, gateway types.NamespacedName) bool { if parentRef.Group != nil && string(*parentRef.Group) != gwapiv1.GroupName { return false } @@ -118,7 +118,12 @@ func IsRefToGateway(parentRef gwapiv1.ParentReference, gateway types.NamespacedN return false } - if parentRef.Namespace != nil && string(*parentRef.Namespace) != gateway.Namespace { + ns := routeNamespace + if parentRef.Namespace != nil { + ns = *parentRef.Namespace + } + + if string(ns) != gateway.Namespace { return false } @@ -129,11 +134,11 @@ func IsRefToGateway(parentRef gwapiv1.ParentReference, gateway types.NamespacedN // in the given list, and if so, a list of the Listeners within that Gateway that // are included by the parent ref (either one specific Listener, or all Listeners // in the Gateway, depending on whether section name is specified or not). -func GetReferencedListeners(parentRef gwapiv1.ParentReference, gateways []*GatewayContext) (bool, []*ListenerContext) { +func GetReferencedListeners(routeNamespace gwapiv1.Namespace, parentRef gwapiv1.ParentReference, gateways []*GatewayContext) (bool, []*ListenerContext) { var referencedListeners []*ListenerContext for _, gateway := range gateways { - if IsRefToGateway(parentRef, utils.NamespacedName(gateway)) { + if IsRefToGateway(routeNamespace, parentRef, utils.NamespacedName(gateway)) { // The parentRef may be to the entire Gateway, or to a specific listener. for _, listener := range gateway.listeners { if (parentRef.SectionName == nil || *parentRef.SectionName == listener.Name) && (parentRef.Port == nil || *parentRef.Port == listener.Port) { @@ -542,7 +547,12 @@ func getPolicyTargetRefs[T client.Object](policy egv1a1.PolicyTargetReferences, // to targets that were already found via the selectors. Only add them to the returned list if // they are not yet there. Always add them at the end. fastLookup := sets.New(ret...) + var emptyTargetRef gwapiv1a2.LocalPolicyTargetReferenceWithSectionName for _, v := range policy.GetTargetRefs() { + if v == emptyTargetRef { + // This can happen when the targetRef structure is read from extension server policies + continue + } if !fastLookup.Has(v) { ret = append(ret, v) } diff --git a/internal/gatewayapi/helpers_test.go b/internal/gatewayapi/helpers_test.go index 0339141de24..a6469715f4d 100644 --- a/internal/gatewayapi/helpers_test.go +++ b/internal/gatewayapi/helpers_test.go @@ -17,6 +17,7 @@ import ( "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" @@ -485,3 +486,71 @@ func TestGetPolicyTargetRefs(t *testing.T) { }) } } + +func TestIsRefToGateway(t *testing.T) { + cases := []struct { + name string + routeNamespace gwapiv1.Namespace + parentRef gwapiv1.ParentReference + gatewayNN types.NamespacedName + expected bool + }{ + { + name: "match without namespace-true", + routeNamespace: gwapiv1.Namespace("ns1"), + parentRef: gwapiv1.ParentReference{ + Name: gwapiv1.ObjectName("eg"), + }, + gatewayNN: types.NamespacedName{ + Name: "eg", + Namespace: "ns1", + }, + expected: true, + }, + { + name: "match without namespace-false", + routeNamespace: gwapiv1.Namespace("ns1"), + parentRef: gwapiv1.ParentReference{ + Name: gwapiv1.ObjectName("eg"), + }, + gatewayNN: types.NamespacedName{ + Name: "eg", + Namespace: "ns2", + }, + expected: false, + }, + { + name: "match with namespace-true", + routeNamespace: gwapiv1.Namespace("ns1"), + parentRef: gwapiv1.ParentReference{ + Name: gwapiv1.ObjectName("eg"), + Namespace: NamespacePtr("ns1"), + }, + gatewayNN: types.NamespacedName{ + Name: "eg", + Namespace: "ns1", + }, + expected: true, + }, + { + name: "match without namespace2-false", + routeNamespace: gwapiv1.Namespace("ns1"), + parentRef: gwapiv1.ParentReference{ + Name: gwapiv1.ObjectName("eg"), + Namespace: NamespacePtr("ns2"), + }, + gatewayNN: types.NamespacedName{ + Name: "eg", + Namespace: "ns1", + }, + expected: false, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := IsRefToGateway(tc.routeNamespace, tc.parentRef, tc.gatewayNN) + require.Equal(t, tc.expected, got) + }) + } +} diff --git a/internal/gatewayapi/listener.go b/internal/gatewayapi/listener.go index 4eac0e11c29..adbd302b957 100644 --- a/internal/gatewayapi/listener.go +++ b/internal/gatewayapi/listener.go @@ -257,6 +257,21 @@ func (t *Translator) processAccessLog(envoyproxy *egv1a1.EnvoyProxy, resources * } } + var ( + validExprs []string + errs []error + ) + for _, expr := range accessLog.Matches { + if !validCELExpression(expr) { + errs = append(errs, fmt.Errorf("invalid CEL expression: %s", expr)) + continue + } + validExprs = append(validExprs, expr) + } + if len(errs) > 0 { + return nil, utilerrors.NewAggregate(errs) + } + for j, sink := range accessLog.Sinks { switch sink.Type { case egv1a1.ProxyAccessLogSinkTypeFile: @@ -267,8 +282,9 @@ func (t *Translator) processAccessLog(envoyproxy *egv1a1.EnvoyProxy, resources * switch format.Type { case egv1a1.ProxyAccessLogFormatTypeText: al := &ir.TextAccessLog{ - Format: format.Text, - Path: sink.File.Path, + Format: format.Text, + Path: sink.File.Path, + CELMatches: validExprs, } irAccessLog.Text = append(irAccessLog.Text, al) case egv1a1.ProxyAccessLogFormatTypeJSON: @@ -278,8 +294,9 @@ func (t *Translator) processAccessLog(envoyproxy *egv1a1.EnvoyProxy, resources * } al := &ir.JSONAccessLog{ - JSON: format.JSON, - Path: sink.File.Path, + JSON: format.JSON, + Path: sink.File.Path, + CELMatches: validExprs, } irAccessLog.JSON = append(irAccessLog.JSON, al) } @@ -307,7 +324,8 @@ func (t *Translator) processAccessLog(envoyproxy *egv1a1.EnvoyProxy, resources * Name: fmt.Sprintf("accesslog_als_%d_%d", i, j), // TODO: rename this, so that we can share backend with tracing? Settings: ds, }, - Type: sink.ALS.Type, + Type: sink.ALS.Type, + CELMatches: validExprs, } if al.Type == egv1a1.ALSEnvoyProxyAccessLogTypeHTTP && sink.ALS.HTTP != nil { @@ -334,7 +352,8 @@ func (t *Translator) processAccessLog(envoyproxy *egv1a1.EnvoyProxy, resources * // TODO: remove support for Host/Port in v1.2 al := &ir.OpenTelemetryAccessLog{ - Resources: sink.OpenTelemetry.Resources, + CELMatches: validExprs, + Resources: sink.OpenTelemetry.Resources, } // TODO: how to get authority from the backendRefs? @@ -368,22 +387,6 @@ func (t *Translator) processAccessLog(envoyproxy *egv1a1.EnvoyProxy, resources * irAccessLog.OpenTelemetry = append(irAccessLog.OpenTelemetry, al) } } - - var ( - validExprs []string - errs []error - ) - for _, expr := range accessLog.Matches { - if !validCELExpression(expr) { - errs = append(errs, fmt.Errorf("invalid CEL expression: %s", expr)) - continue - } - validExprs = append(validExprs, expr) - } - if len(errs) > 0 { - return nil, utilerrors.NewAggregate(errs) - } - irAccessLog.CELMatches = validExprs } return irAccessLog, nil diff --git a/internal/gatewayapi/route.go b/internal/gatewayapi/route.go index c6719015d1c..336e931cfce 100644 --- a/internal/gatewayapi/route.go +++ b/internal/gatewayapi/route.go @@ -1409,9 +1409,9 @@ func inspectAppProtocolByRouteKind(kind gwapiv1.Kind) ir.AppProtocol { // attach for each parentRef. func (t *Translator) processAllowedListenersForParentRefs(routeContext RouteContext, gateways []*GatewayContext, resources *Resources) bool { var relevantRoute bool - + ns := gwapiv1.Namespace(routeContext.GetNamespace()) for _, parentRef := range GetParentReferences(routeContext) { - isRelevantParentRef, selectedListeners := GetReferencedListeners(parentRef, gateways) + isRelevantParentRef, selectedListeners := GetReferencedListeners(ns, parentRef, gateways) // Parent ref is not to a Gateway that we control: skip it if !isRelevantParentRef { diff --git a/internal/gatewayapi/securitypolicy.go b/internal/gatewayapi/securitypolicy.go index 57153bc7e41..9a0fefbafc5 100644 --- a/internal/gatewayapi/securitypolicy.go +++ b/internal/gatewayapi/securitypolicy.go @@ -394,8 +394,6 @@ func (t *Translator) translateSecurityPolicyForRoute( } // Apply IR to all relevant routes - // Note: there are multiple features in a security policy, even if some of them - // are invalid, we still want to apply the valid ones. prefix := irRoutePrefix(route) parentRefs := GetParentReferences(route) for _, p := range parentRefs { @@ -430,6 +428,12 @@ func (t *Translator) translateSecurityPolicyForRoute( ExtAuth: extAuth, Authorization: authorization, } + if errs != nil { + // Return a 500 direct response to avoid unauthorized access + r.DirectResponse = &ir.DirectResponse{ + StatusCode: 500, + } + } } } } @@ -523,7 +527,6 @@ func (t *Translator) translateSecurityPolicyForGateway( if r.Security != nil { continue } - r.Security = &ir.SecurityFeatures{ CORS: cors, JWT: jwt, @@ -532,6 +535,12 @@ func (t *Translator) translateSecurityPolicyForGateway( ExtAuth: extAuth, Authorization: authorization, } + if errs != nil { + // Return a 500 direct response to avoid unauthorized access + r.DirectResponse = &ir.DirectResponse{ + StatusCode: 500, + } + } } } return errs diff --git a/internal/gatewayapi/status/gatewayclass.go b/internal/gatewayapi/status/gatewayclass.go index ad8a032a27f..490fc61b9fe 100644 --- a/internal/gatewayapi/status/gatewayclass.go +++ b/internal/gatewayapi/status/gatewayclass.go @@ -37,7 +37,10 @@ const ( // for the provided GatewayClass. func SetGatewayClassAccepted(gc *gwapiv1.GatewayClass, accepted bool, reason, msg string) *gwapiv1.GatewayClass { gc.Status.Conditions = MergeConditions(gc.Status.Conditions, computeGatewayClassAcceptedCondition(gc, accepted, reason, msg)) - gc.Status.SupportedFeatures = GatewaySupportedFeatures + // Disable SupportedFeatures until the field moves from experimental to stable to avoid + // status failures due to changes in the datatype. This can occur because we cannot control + // how a CRD is installed in the cluster + // gc.Status.SupportedFeatures = GatewaySupportedFeatures return gc } diff --git a/internal/gatewayapi/testdata/backend-invalid-feature-disabled.out.yaml b/internal/gatewayapi/testdata/backend-invalid-feature-disabled.out.yaml index c1425425b20..643df05037b 100644 --- a/internal/gatewayapi/testdata/backend-invalid-feature-disabled.out.yaml +++ b/internal/gatewayapi/testdata/backend-invalid-feature-disabled.out.yaml @@ -174,6 +174,7 @@ xdsIR: - weight: 1 directResponse: statusCode: 500 + envoyExtensions: {} hostname: '*' isHTTP2: false metadata: diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-override-replace.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-override-replace.in.yaml index b9f40aee084..0b844dc0c86 100644 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-override-replace.in.yaml +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-override-replace.in.yaml @@ -52,6 +52,25 @@ httpRoutes: backendRefs: - name: service-1 port: 8080 +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-3 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/baz" + backendRefs: + - name: service-1 + port: 8080 backendTrafficPolicies: - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: BackendTrafficPolicy @@ -85,3 +104,13 @@ backendTrafficPolicies: type: ConsistentHash consistentHash: type: SourceIP +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + namespace: default + name: policy-for-route-3 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-3 diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-override-replace.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-override-replace.out.yaml index 59da4617777..8aafd70c0bb 100644 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-override-replace.out.yaml +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-override-replace.out.yaml @@ -29,6 +29,32 @@ backendTrafficPolicies: status: "True" type: Accepted controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + creationTimestamp: null + name: policy-for-route-3 + namespace: default + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-3 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: BackendTrafficPolicy metadata: @@ -63,7 +89,7 @@ backendTrafficPolicies: type: Accepted - lastTransitionTime: null message: 'This policy is being overridden by other backendTrafficPolicies - for these routes: [default/httproute-1]' + for these routes: [default/httproute-1 default/httproute-3]' reason: Overridden status: "True" type: Overridden @@ -86,7 +112,7 @@ gateways: protocol: HTTP status: listeners: - - attachedRoutes: 2 + - attachedRoutes: 3 conditions: - lastTransitionTime: null message: Sending translated listener configuration to the data plane @@ -186,6 +212,44 @@ httpRoutes: name: gateway-1 namespace: envoy-gateway sectionName: http +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-3 + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: /baz + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http infraIR: envoy-gateway/gateway-1: proxy: @@ -276,3 +340,24 @@ xdsIR: maxConnectionDuration: 22s tcp: connectTimeout: 20s + - destination: + name: httproute/default/httproute-3/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-3 + namespace: default + name: httproute/default/httproute-3/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: /baz + traffic: {} diff --git a/internal/gatewayapi/testdata/custom-filter-order.out.yaml b/internal/gatewayapi/testdata/custom-filter-order.out.yaml index fcab6d9f5a6..6967bf280f3 100644 --- a/internal/gatewayapi/testdata/custom-filter-order.out.yaml +++ b/internal/gatewayapi/testdata/custom-filter-order.out.yaml @@ -246,6 +246,30 @@ xdsIR: - weight: 1 directResponse: statusCode: 500 + envoyExtensions: + wasms: + - config: + parameter1: + key1: value1 + key2: value2 + parameter2: value3 + failOpen: false + httpWasmCode: + originalDownloadingURL: https://www.example.com/wasm-filter-1.wasm + servingURL: https://envoy-gateway:18002/5c90b9a82642ce00a7753923fabead306b9d9a54a7c0bd2463a1af3efcfb110b.wasm + sha256: 746df05c8f3a0b07a46c0967cfbc5cbe5b9d48d0f79b6177eeedf8be6c8b34b5 + name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/wasm/0 + wasmName: wasm-filter-1 + - config: + parameter1: value1 + parameter2: value2 + failOpen: false + httpWasmCode: + originalDownloadingURL: https://www.example.com/wasm-filter-2.wasm + servingURL: https://envoy-gateway:18002/593e4cc60a7e0fa4d4f86531a5e20e785213a52000f056a7a8b5c5afcb908052.wasm + sha256: a1efca12ea51069abb123bf9c77889fcc2a31cc5483fc14d115e44fdf07c7980 + name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/wasm/1 + wasmName: wasm-filter-2 hostname: www.example.com isHTTP2: false metadata: @@ -292,26 +316,3 @@ xdsIR: name: example2 remoteJWKS: uri: http://two.example.com/jwt/public-key/jwks.json - wasm: - - config: - parameter1: - key1: value1 - key2: value2 - parameter2: value3 - failOpen: false - httpWasmCode: - originalDownloadingURL: https://www.example.com/wasm-filter-1.wasm - servingURL: https://envoy-gateway:18002/5c90b9a82642ce00a7753923fabead306b9d9a54a7c0bd2463a1af3efcfb110b.wasm - sha256: 746df05c8f3a0b07a46c0967cfbc5cbe5b9d48d0f79b6177eeedf8be6c8b34b5 - name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/wasm/0 - wasmName: wasm-filter-1 - - config: - parameter1: value1 - parameter2: value2 - failOpen: false - httpWasmCode: - originalDownloadingURL: https://www.example.com/wasm-filter-2.wasm - servingURL: https://envoy-gateway:18002/593e4cc60a7e0fa4d4f86531a5e20e785213a52000f056a7a8b5c5afcb908052.wasm - sha256: a1efca12ea51069abb123bf9c77889fcc2a31cc5483fc14d115e44fdf07c7980 - name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/wasm/1 - wasmName: wasm-filter-2 diff --git a/internal/gatewayapi/testdata/envoyextensionpolicy-override-replace.in.yaml b/internal/gatewayapi/testdata/envoyextensionpolicy-override-replace.in.yaml index 3ad36f07152..f9c9175594b 100644 --- a/internal/gatewayapi/testdata/envoyextensionpolicy-override-replace.in.yaml +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-override-replace.in.yaml @@ -73,6 +73,25 @@ httpRoutes: backendRefs: - name: service-1 port: 8080 +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-3 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/baz" + backendRefs: + - name: service-1 + port: 8080 envoyExtensionPolicies: - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: EnvoyExtensionPolicy @@ -102,3 +121,13 @@ envoyExtensionPolicies: - backendRefs: - name: grpc-backend-2 port: 8000 +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + namespace: default + name: policy-for-route-3 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-3 diff --git a/internal/gatewayapi/testdata/envoyextensionpolicy-override-replace.out.yaml b/internal/gatewayapi/testdata/envoyextensionpolicy-override-replace.out.yaml index 08a6152194a..4f055e7bc4d 100644 --- a/internal/gatewayapi/testdata/envoyextensionpolicy-override-replace.out.yaml +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-override-replace.out.yaml @@ -29,6 +29,32 @@ envoyExtensionPolicies: status: "True" type: Accepted controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + creationTimestamp: null + name: policy-for-route-3 + namespace: default + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-3 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: EnvoyExtensionPolicy metadata: @@ -59,7 +85,7 @@ envoyExtensionPolicies: type: Accepted - lastTransitionTime: null message: 'This policy is being overridden by other envoyExtensionPolicies - for these routes: [default/httproute-1]' + for these routes: [default/httproute-1 default/httproute-3]' reason: Overridden status: "True" type: Overridden @@ -82,7 +108,7 @@ gateways: protocol: HTTP status: listeners: - - attachedRoutes: 2 + - attachedRoutes: 3 conditions: - lastTransitionTime: null message: Sending translated listener configuration to the data plane @@ -182,6 +208,44 @@ httpRoutes: name: gateway-1 namespace: envoy-gateway sectionName: http +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-3 + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: /baz + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http infraIR: envoy-gateway/gateway-1: proxy: @@ -228,14 +292,15 @@ xdsIR: port: 8080 protocol: HTTP weight: 1 - extProc: - - authority: grpc-backend-2.default:8000 - destination: - name: envoyextensionpolicy/default/policy-for-route-1/0 - settings: - - protocol: GRPC - weight: 1 - name: envoyextensionpolicy/default/policy-for-route-1/extproc/0 + envoyExtensions: + extProcs: + - authority: grpc-backend-2.default:8000 + destination: + name: envoyextensionpolicy/default/policy-for-route-1/0 + settings: + - protocol: GRPC + weight: 1 + name: envoyextensionpolicy/default/policy-for-route-1/extproc/0 hostname: gateway.envoyproxy.io isHTTP2: false metadata: @@ -256,14 +321,15 @@ xdsIR: port: 8080 protocol: HTTP weight: 1 - extProc: - - authority: grpc-backend.envoy-gateway:9000 - destination: - name: envoyextensionpolicy/envoy-gateway/policy-for-gateway-1/0 - settings: - - protocol: GRPC - weight: 1 - name: envoyextensionpolicy/envoy-gateway/policy-for-gateway-1/extproc/0 + envoyExtensions: + extProcs: + - authority: grpc-backend.envoy-gateway:9000 + destination: + name: envoyextensionpolicy/envoy-gateway/policy-for-gateway-1/0 + settings: + - protocol: GRPC + weight: 1 + name: envoyextensionpolicy/envoy-gateway/policy-for-gateway-1/extproc/0 hostname: gateway.envoyproxy.io isHTTP2: false metadata: @@ -275,3 +341,24 @@ xdsIR: distinct: false name: "" prefix: /bar + - destination: + name: httproute/default/httproute-3/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + envoyExtensions: {} + hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-3 + namespace: default + name: httproute/default/httproute-3/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: /baz diff --git a/internal/gatewayapi/testdata/envoyextensionpolicy-status-conditions.out.yaml b/internal/gatewayapi/testdata/envoyextensionpolicy-status-conditions.out.yaml index ef83112c186..0c4264fd4b9 100644 --- a/internal/gatewayapi/testdata/envoyextensionpolicy-status-conditions.out.yaml +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-status-conditions.out.yaml @@ -561,6 +561,7 @@ xdsIR: - weight: 1 directResponse: statusCode: 500 + envoyExtensions: {} hostname: '*' isHTTP2: false metadata: @@ -598,6 +599,7 @@ xdsIR: - weight: 1 directResponse: statusCode: 500 + envoyExtensions: {} headerMatches: - distinct: false exact: foo diff --git a/internal/gatewayapi/testdata/envoyextensionpolicy-with-extproc-with-backendtlspolicy.out.yaml b/internal/gatewayapi/testdata/envoyextensionpolicy-with-extproc-with-backendtlspolicy.out.yaml index 638084126b8..4bfbd4e7df5 100644 --- a/internal/gatewayapi/testdata/envoyextensionpolicy-with-extproc-with-backendtlspolicy.out.yaml +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-extproc-with-backendtlspolicy.out.yaml @@ -304,25 +304,26 @@ xdsIR: port: 8080 protocol: HTTP weight: 1 - extProc: - - authority: grpc-backend-2.default:9000 - destination: - name: envoyextensionpolicy/default/policy-for-http-route/0 - settings: - - addressType: IP - endpoints: - - host: 8.8.8.8 - port: 9000 - protocol: GRPC - tls: - caCertificate: - certificate: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURKekNDQWcrZ0F3SUJBZ0lVQWw2VUtJdUttenRlODFjbGx6NVBmZE4ySWxJd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0l6RVFNQTRHQTFVRUF3d0hiWGxqYVdWdWRERVBNQTBHQTFVRUNnd0dhM1ZpWldSaU1CNFhEVEl6TVRBdwpNakExTkRFMU4xb1hEVEkwTVRBd01UQTFOREUxTjFvd0l6RVFNQTRHQTFVRUF3d0hiWGxqYVdWdWRERVBNQTBHCkExVUVDZ3dHYTNWaVpXUmlNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXdTVGMKMXlqOEhXNjJueW5rRmJYbzRWWEt2MmpDMFBNN2RQVmt5ODdGd2VaY1RLTG9XUVZQUUUycDJrTERLNk9Fc3ptTQp5eXIreHhXdHlpdmVyZW1yV3FuS2tOVFloTGZZUGhnUWtjemliN2VVYWxtRmpVYmhXZEx2SGFrYkVnQ29kbjNiCmt6NTdtSW5YMlZwaURPS2c0a3lIZml1WFdwaUJxckN4MEtOTHB4bzNERVFjRmNzUVRlVEh6aDQ3NTJHVjA0UlUKVGkvR0VXeXpJc2w0Umc3dEd0QXdtY0lQZ1VOVWZZMlEzOTBGR3FkSDRhaG4rbXcvNmFGYlczMVc2M2Q5WUpWcQppb3lPVmNhTUlwTTVCL2M3UWM4U3VoQ0kxWUdoVXlnNGNSSExFdzVWdGlraW95RTNYMDRrbmEzalFBajU0WWJSCmJwRWhjMzVhcEtMQjIxSE9VUUlEQVFBQm8xTXdVVEFkQmdOVkhRNEVGZ1FVeXZsMFZJNXZKVlN1WUZYdTdCNDgKNlBiTUVBb3dId1lEVlIwakJCZ3dGb0FVeXZsMFZJNXZKVlN1WUZYdTdCNDg2UGJNRUFvd0R3WURWUjBUQVFILwpCQVV3QXdFQi96QU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFNTHhyZ0ZWTXVOUnEyd0F3Y0J0N1NuTlI1Q2Z6CjJNdlhxNUVVbXVhd0lVaTlrYVlqd2RWaURSRUdTams3SlcxN3ZsNTc2SGpEa2RmUndpNEUyOFN5ZFJJblpmNkoKaThIWmNaN2NhSDZEeFIzMzVmZ0hWekxpNU5pVGNlL09qTkJRelEyTUpYVkRkOERCbUc1ZnlhdEppT0pRNGJXRQpBN0ZsUDBSZFAzQ08zR1dFME01aVhPQjJtMXFXa0UyZXlPNFVIdndUcU5RTGRyZEFYZ0RRbGJhbTllNEJHM0dnCmQvNnRoQWtXRGJ0L1FOVCtFSkhEQ3ZoRFJLaDFSdUdIeWcrWSsvbmViVFdXckZXc2t0UnJiT29IQ1ppQ3BYSTEKM2VYRTZudDBZa2d0RHhHMjJLcW5ocEFnOWdVU3MyaGxob3h5dmt6eUYwbXU2TmhQbHdBZ25xNysvUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K - name: policy-btls-grpc-2/default-ca - sni: grpc-backend-2 - weight: 1 - name: envoyextensionpolicy/default/policy-for-http-route/extproc/0 - requestHeaderProcessing: true - responseHeaderProcessing: true + envoyExtensions: + extProcs: + - authority: grpc-backend-2.default:9000 + destination: + name: envoyextensionpolicy/default/policy-for-http-route/0 + settings: + - addressType: IP + endpoints: + - host: 8.8.8.8 + port: 9000 + protocol: GRPC + tls: + caCertificate: + certificate: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURKekNDQWcrZ0F3SUJBZ0lVQWw2VUtJdUttenRlODFjbGx6NVBmZE4ySWxJd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0l6RVFNQTRHQTFVRUF3d0hiWGxqYVdWdWRERVBNQTBHQTFVRUNnd0dhM1ZpWldSaU1CNFhEVEl6TVRBdwpNakExTkRFMU4xb1hEVEkwTVRBd01UQTFOREUxTjFvd0l6RVFNQTRHQTFVRUF3d0hiWGxqYVdWdWRERVBNQTBHCkExVUVDZ3dHYTNWaVpXUmlNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXdTVGMKMXlqOEhXNjJueW5rRmJYbzRWWEt2MmpDMFBNN2RQVmt5ODdGd2VaY1RLTG9XUVZQUUUycDJrTERLNk9Fc3ptTQp5eXIreHhXdHlpdmVyZW1yV3FuS2tOVFloTGZZUGhnUWtjemliN2VVYWxtRmpVYmhXZEx2SGFrYkVnQ29kbjNiCmt6NTdtSW5YMlZwaURPS2c0a3lIZml1WFdwaUJxckN4MEtOTHB4bzNERVFjRmNzUVRlVEh6aDQ3NTJHVjA0UlUKVGkvR0VXeXpJc2w0Umc3dEd0QXdtY0lQZ1VOVWZZMlEzOTBGR3FkSDRhaG4rbXcvNmFGYlczMVc2M2Q5WUpWcQppb3lPVmNhTUlwTTVCL2M3UWM4U3VoQ0kxWUdoVXlnNGNSSExFdzVWdGlraW95RTNYMDRrbmEzalFBajU0WWJSCmJwRWhjMzVhcEtMQjIxSE9VUUlEQVFBQm8xTXdVVEFkQmdOVkhRNEVGZ1FVeXZsMFZJNXZKVlN1WUZYdTdCNDgKNlBiTUVBb3dId1lEVlIwakJCZ3dGb0FVeXZsMFZJNXZKVlN1WUZYdTdCNDg2UGJNRUFvd0R3WURWUjBUQVFILwpCQVV3QXdFQi96QU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFNTHhyZ0ZWTXVOUnEyd0F3Y0J0N1NuTlI1Q2Z6CjJNdlhxNUVVbXVhd0lVaTlrYVlqd2RWaURSRUdTams3SlcxN3ZsNTc2SGpEa2RmUndpNEUyOFN5ZFJJblpmNkoKaThIWmNaN2NhSDZEeFIzMzVmZ0hWekxpNU5pVGNlL09qTkJRelEyTUpYVkRkOERCbUc1ZnlhdEppT0pRNGJXRQpBN0ZsUDBSZFAzQ08zR1dFME01aVhPQjJtMXFXa0UyZXlPNFVIdndUcU5RTGRyZEFYZ0RRbGJhbTllNEJHM0dnCmQvNnRoQWtXRGJ0L1FOVCtFSkhEQ3ZoRFJLaDFSdUdIeWcrWSsvbmViVFdXckZXc2t0UnJiT29IQ1ppQ3BYSTEKM2VYRTZudDBZa2d0RHhHMjJLcW5ocEFnOWdVU3MyaGxob3h5dmt6eUYwbXU2TmhQbHdBZ25xNysvUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + name: policy-btls-grpc-2/default-ca + sni: grpc-backend-2 + weight: 1 + name: envoyextensionpolicy/default/policy-for-http-route/extproc/0 + requestHeaderProcessing: true + responseHeaderProcessing: true hostname: www.foo.com isHTTP2: false metadata: @@ -343,26 +344,27 @@ xdsIR: port: 8080 protocol: HTTP weight: 1 - extProc: - - authority: grpc-backend.envoy-gateway:8000 - destination: - name: envoyextensionpolicy/default/policy-for-gateway/0 - settings: - - addressType: IP - protocol: GRPC - tls: - caCertificate: - certificate: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURKekNDQWcrZ0F3SUJBZ0lVQWw2VUtJdUttenRlODFjbGx6NVBmZE4ySWxJd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0l6RVFNQTRHQTFVRUF3d0hiWGxqYVdWdWRERVBNQTBHQTFVRUNnd0dhM1ZpWldSaU1CNFhEVEl6TVRBdwpNakExTkRFMU4xb1hEVEkwTVRBd01UQTFOREUxTjFvd0l6RVFNQTRHQTFVRUF3d0hiWGxqYVdWdWRERVBNQTBHCkExVUVDZ3dHYTNWaVpXUmlNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXdTVGMKMXlqOEhXNjJueW5rRmJYbzRWWEt2MmpDMFBNN2RQVmt5ODdGd2VaY1RLTG9XUVZQUUUycDJrTERLNk9Fc3ptTQp5eXIreHhXdHlpdmVyZW1yV3FuS2tOVFloTGZZUGhnUWtjemliN2VVYWxtRmpVYmhXZEx2SGFrYkVnQ29kbjNiCmt6NTdtSW5YMlZwaURPS2c0a3lIZml1WFdwaUJxckN4MEtOTHB4bzNERVFjRmNzUVRlVEh6aDQ3NTJHVjA0UlUKVGkvR0VXeXpJc2w0Umc3dEd0QXdtY0lQZ1VOVWZZMlEzOTBGR3FkSDRhaG4rbXcvNmFGYlczMVc2M2Q5WUpWcQppb3lPVmNhTUlwTTVCL2M3UWM4U3VoQ0kxWUdoVXlnNGNSSExFdzVWdGlraW95RTNYMDRrbmEzalFBajU0WWJSCmJwRWhjMzVhcEtMQjIxSE9VUUlEQVFBQm8xTXdVVEFkQmdOVkhRNEVGZ1FVeXZsMFZJNXZKVlN1WUZYdTdCNDgKNlBiTUVBb3dId1lEVlIwakJCZ3dGb0FVeXZsMFZJNXZKVlN1WUZYdTdCNDg2UGJNRUFvd0R3WURWUjBUQVFILwpCQVV3QXdFQi96QU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFNTHhyZ0ZWTXVOUnEyd0F3Y0J0N1NuTlI1Q2Z6CjJNdlhxNUVVbXVhd0lVaTlrYVlqd2RWaURSRUdTams3SlcxN3ZsNTc2SGpEa2RmUndpNEUyOFN5ZFJJblpmNkoKaThIWmNaN2NhSDZEeFIzMzVmZ0hWekxpNU5pVGNlL09qTkJRelEyTUpYVkRkOERCbUc1ZnlhdEppT0pRNGJXRQpBN0ZsUDBSZFAzQ08zR1dFME01aVhPQjJtMXFXa0UyZXlPNFVIdndUcU5RTGRyZEFYZ0RRbGJhbTllNEJHM0dnCmQvNnRoQWtXRGJ0L1FOVCtFSkhEQ3ZoRFJLaDFSdUdIeWcrWSsvbmViVFdXckZXc2t0UnJiT29IQ1ppQ3BYSTEKM2VYRTZudDBZa2d0RHhHMjJLcW5ocEFnOWdVU3MyaGxob3h5dmt6eUYwbXU2TmhQbHdBZ25xNysvUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K - name: policy-btls-grpc/envoy-gateway-ca - sni: grpc-backend - weight: 1 - failOpen: true - messageTimeout: 5s - name: envoyextensionpolicy/default/policy-for-gateway/extproc/0 - requestBodyProcessingMode: Buffered - requestHeaderProcessing: true - responseBodyProcessingMode: Streamed - responseHeaderProcessing: true + envoyExtensions: + extProcs: + - authority: grpc-backend.envoy-gateway:8000 + destination: + name: envoyextensionpolicy/default/policy-for-gateway/0 + settings: + - addressType: IP + protocol: GRPC + tls: + caCertificate: + certificate: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURKekNDQWcrZ0F3SUJBZ0lVQWw2VUtJdUttenRlODFjbGx6NVBmZE4ySWxJd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0l6RVFNQTRHQTFVRUF3d0hiWGxqYVdWdWRERVBNQTBHQTFVRUNnd0dhM1ZpWldSaU1CNFhEVEl6TVRBdwpNakExTkRFMU4xb1hEVEkwTVRBd01UQTFOREUxTjFvd0l6RVFNQTRHQTFVRUF3d0hiWGxqYVdWdWRERVBNQTBHCkExVUVDZ3dHYTNWaVpXUmlNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXdTVGMKMXlqOEhXNjJueW5rRmJYbzRWWEt2MmpDMFBNN2RQVmt5ODdGd2VaY1RLTG9XUVZQUUUycDJrTERLNk9Fc3ptTQp5eXIreHhXdHlpdmVyZW1yV3FuS2tOVFloTGZZUGhnUWtjemliN2VVYWxtRmpVYmhXZEx2SGFrYkVnQ29kbjNiCmt6NTdtSW5YMlZwaURPS2c0a3lIZml1WFdwaUJxckN4MEtOTHB4bzNERVFjRmNzUVRlVEh6aDQ3NTJHVjA0UlUKVGkvR0VXeXpJc2w0Umc3dEd0QXdtY0lQZ1VOVWZZMlEzOTBGR3FkSDRhaG4rbXcvNmFGYlczMVc2M2Q5WUpWcQppb3lPVmNhTUlwTTVCL2M3UWM4U3VoQ0kxWUdoVXlnNGNSSExFdzVWdGlraW95RTNYMDRrbmEzalFBajU0WWJSCmJwRWhjMzVhcEtMQjIxSE9VUUlEQVFBQm8xTXdVVEFkQmdOVkhRNEVGZ1FVeXZsMFZJNXZKVlN1WUZYdTdCNDgKNlBiTUVBb3dId1lEVlIwakJCZ3dGb0FVeXZsMFZJNXZKVlN1WUZYdTdCNDg2UGJNRUFvd0R3WURWUjBUQVFILwpCQVV3QXdFQi96QU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFNTHhyZ0ZWTXVOUnEyd0F3Y0J0N1NuTlI1Q2Z6CjJNdlhxNUVVbXVhd0lVaTlrYVlqd2RWaURSRUdTams3SlcxN3ZsNTc2SGpEa2RmUndpNEUyOFN5ZFJJblpmNkoKaThIWmNaN2NhSDZEeFIzMzVmZ0hWekxpNU5pVGNlL09qTkJRelEyTUpYVkRkOERCbUc1ZnlhdEppT0pRNGJXRQpBN0ZsUDBSZFAzQ08zR1dFME01aVhPQjJtMXFXa0UyZXlPNFVIdndUcU5RTGRyZEFYZ0RRbGJhbTllNEJHM0dnCmQvNnRoQWtXRGJ0L1FOVCtFSkhEQ3ZoRFJLaDFSdUdIeWcrWSsvbmViVFdXckZXc2t0UnJiT29IQ1ppQ3BYSTEKM2VYRTZudDBZa2d0RHhHMjJLcW5ocEFnOWdVU3MyaGxob3h5dmt6eUYwbXU2TmhQbHdBZ25xNysvUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + name: policy-btls-grpc/envoy-gateway-ca + sni: grpc-backend + weight: 1 + failOpen: true + messageTimeout: 5s + name: envoyextensionpolicy/default/policy-for-gateway/extproc/0 + requestBodyProcessingMode: Buffered + requestHeaderProcessing: true + responseBodyProcessingMode: Streamed + responseHeaderProcessing: true hostname: www.bar.com isHTTP2: false metadata: diff --git a/internal/gatewayapi/testdata/envoyextensionpolicy-with-extproc-with-multiple-backendrefs.out.yaml b/internal/gatewayapi/testdata/envoyextensionpolicy-with-extproc-with-multiple-backendrefs.out.yaml index d71c0ab298d..fda8a8185fc 100644 --- a/internal/gatewayapi/testdata/envoyextensionpolicy-with-extproc-with-multiple-backendrefs.out.yaml +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-extproc-with-multiple-backendrefs.out.yaml @@ -304,43 +304,44 @@ xdsIR: port: 8080 protocol: HTTP weight: 1 - extProc: - - authority: grpc-backend.envoy-gateway:8000 - destination: - name: envoyextensionpolicy/default/policy-for-http-route/0 - settings: - - addressType: IP - protocol: GRPC - tls: - caCertificate: - certificate: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURKekNDQWcrZ0F3SUJBZ0lVQWw2VUtJdUttenRlODFjbGx6NVBmZE4ySWxJd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0l6RVFNQTRHQTFVRUF3d0hiWGxqYVdWdWRERVBNQTBHQTFVRUNnd0dhM1ZpWldSaU1CNFhEVEl6TVRBdwpNakExTkRFMU4xb1hEVEkwTVRBd01UQTFOREUxTjFvd0l6RVFNQTRHQTFVRUF3d0hiWGxqYVdWdWRERVBNQTBHCkExVUVDZ3dHYTNWaVpXUmlNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXdTVGMKMXlqOEhXNjJueW5rRmJYbzRWWEt2MmpDMFBNN2RQVmt5ODdGd2VaY1RLTG9XUVZQUUUycDJrTERLNk9Fc3ptTQp5eXIreHhXdHlpdmVyZW1yV3FuS2tOVFloTGZZUGhnUWtjemliN2VVYWxtRmpVYmhXZEx2SGFrYkVnQ29kbjNiCmt6NTdtSW5YMlZwaURPS2c0a3lIZml1WFdwaUJxckN4MEtOTHB4bzNERVFjRmNzUVRlVEh6aDQ3NTJHVjA0UlUKVGkvR0VXeXpJc2w0Umc3dEd0QXdtY0lQZ1VOVWZZMlEzOTBGR3FkSDRhaG4rbXcvNmFGYlczMVc2M2Q5WUpWcQppb3lPVmNhTUlwTTVCL2M3UWM4U3VoQ0kxWUdoVXlnNGNSSExFdzVWdGlraW95RTNYMDRrbmEzalFBajU0WWJSCmJwRWhjMzVhcEtMQjIxSE9VUUlEQVFBQm8xTXdVVEFkQmdOVkhRNEVGZ1FVeXZsMFZJNXZKVlN1WUZYdTdCNDgKNlBiTUVBb3dId1lEVlIwakJCZ3dGb0FVeXZsMFZJNXZKVlN1WUZYdTdCNDg2UGJNRUFvd0R3WURWUjBUQVFILwpCQVV3QXdFQi96QU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFNTHhyZ0ZWTXVOUnEyd0F3Y0J0N1NuTlI1Q2Z6CjJNdlhxNUVVbXVhd0lVaTlrYVlqd2RWaURSRUdTams3SlcxN3ZsNTc2SGpEa2RmUndpNEUyOFN5ZFJJblpmNkoKaThIWmNaN2NhSDZEeFIzMzVmZ0hWekxpNU5pVGNlL09qTkJRelEyTUpYVkRkOERCbUc1ZnlhdEppT0pRNGJXRQpBN0ZsUDBSZFAzQ08zR1dFME01aVhPQjJtMXFXa0UyZXlPNFVIdndUcU5RTGRyZEFYZ0RRbGJhbTllNEJHM0dnCmQvNnRoQWtXRGJ0L1FOVCtFSkhEQ3ZoRFJLaDFSdUdIeWcrWSsvbmViVFdXckZXc2t0UnJiT29IQ1ppQ3BYSTEKM2VYRTZudDBZa2d0RHhHMjJLcW5ocEFnOWdVU3MyaGxob3h5dmt6eUYwbXU2TmhQbHdBZ25xNysvUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K - name: policy-btls-grpc/envoy-gateway-ca - sni: grpc-backend - weight: 1 - - addressType: IP - endpoints: - - host: 8.8.8.8 - port: 9000 - protocol: GRPC - weight: 1 - - addressType: IP - endpoints: - - host: 1.1.1.1 - port: 3001 - protocol: GRPC - weight: 1 - - addressType: IP - endpoints: - - host: 2.2.2.2 - port: 3443 - protocol: GRPC - tls: - caCertificate: - certificate: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURKekNDQWcrZ0F3SUJBZ0lVQWw2VUtJdUttenRlODFjbGx6NVBmZE4ySWxJd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0l6RVFNQTRHQTFVRUF3d0hiWGxqYVdWdWRERVBNQTBHQTFVRUNnd0dhM1ZpWldSaU1CNFhEVEl6TVRBdwpNakExTkRFMU4xb1hEVEkwTVRBd01UQTFOREUxTjFvd0l6RVFNQTRHQTFVRUF3d0hiWGxqYVdWdWRERVBNQTBHCkExVUVDZ3dHYTNWaVpXUmlNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXdTVGMKMXlqOEhXNjJueW5rRmJYbzRWWEt2MmpDMFBNN2RQVmt5ODdGd2VaY1RLTG9XUVZQUUUycDJrTERLNk9Fc3ptTQp5eXIreHhXdHlpdmVyZW1yV3FuS2tOVFloTGZZUGhnUWtjemliN2VVYWxtRmpVYmhXZEx2SGFrYkVnQ29kbjNiCmt6NTdtSW5YMlZwaURPS2c0a3lIZml1WFdwaUJxckN4MEtOTHB4bzNERVFjRmNzUVRlVEh6aDQ3NTJHVjA0UlUKVGkvR0VXeXpJc2w0Umc3dEd0QXdtY0lQZ1VOVWZZMlEzOTBGR3FkSDRhaG4rbXcvNmFGYlczMVc2M2Q5WUpWcQppb3lPVmNhTUlwTTVCL2M3UWM4U3VoQ0kxWUdoVXlnNGNSSExFdzVWdGlraW95RTNYMDRrbmEzalFBajU0WWJSCmJwRWhjMzVhcEtMQjIxSE9VUUlEQVFBQm8xTXdVVEFkQmdOVkhRNEVGZ1FVeXZsMFZJNXZKVlN1WUZYdTdCNDgKNlBiTUVBb3dId1lEVlIwakJCZ3dGb0FVeXZsMFZJNXZKVlN1WUZYdTdCNDg2UGJNRUFvd0R3WURWUjBUQVFILwpCQVV3QXdFQi96QU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFNTHhyZ0ZWTXVOUnEyd0F3Y0J0N1NuTlI1Q2Z6CjJNdlhxNUVVbXVhd0lVaTlrYVlqd2RWaURSRUdTams3SlcxN3ZsNTc2SGpEa2RmUndpNEUyOFN5ZFJJblpmNkoKaThIWmNaN2NhSDZEeFIzMzVmZ0hWekxpNU5pVGNlL09qTkJRelEyTUpYVkRkOERCbUc1ZnlhdEppT0pRNGJXRQpBN0ZsUDBSZFAzQ08zR1dFME01aVhPQjJtMXFXa0UyZXlPNFVIdndUcU5RTGRyZEFYZ0RRbGJhbTllNEJHM0dnCmQvNnRoQWtXRGJ0L1FOVCtFSkhEQ3ZoRFJLaDFSdUdIeWcrWSsvbmViVFdXckZXc2t0UnJiT29IQ1ppQ3BYSTEKM2VYRTZudDBZa2d0RHhHMjJLcW5ocEFnOWdVU3MyaGxob3h5dmt6eUYwbXU2TmhQbHdBZ25xNysvUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K - name: policy-btls-backend-ip/envoy-gateway-ca - sni: ip-backend - weight: 1 - name: envoyextensionpolicy/default/policy-for-http-route/extproc/0 + envoyExtensions: + extProcs: + - authority: grpc-backend.envoy-gateway:8000 + destination: + name: envoyextensionpolicy/default/policy-for-http-route/0 + settings: + - addressType: IP + protocol: GRPC + tls: + caCertificate: + certificate: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURKekNDQWcrZ0F3SUJBZ0lVQWw2VUtJdUttenRlODFjbGx6NVBmZE4ySWxJd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0l6RVFNQTRHQTFVRUF3d0hiWGxqYVdWdWRERVBNQTBHQTFVRUNnd0dhM1ZpWldSaU1CNFhEVEl6TVRBdwpNakExTkRFMU4xb1hEVEkwTVRBd01UQTFOREUxTjFvd0l6RVFNQTRHQTFVRUF3d0hiWGxqYVdWdWRERVBNQTBHCkExVUVDZ3dHYTNWaVpXUmlNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXdTVGMKMXlqOEhXNjJueW5rRmJYbzRWWEt2MmpDMFBNN2RQVmt5ODdGd2VaY1RLTG9XUVZQUUUycDJrTERLNk9Fc3ptTQp5eXIreHhXdHlpdmVyZW1yV3FuS2tOVFloTGZZUGhnUWtjemliN2VVYWxtRmpVYmhXZEx2SGFrYkVnQ29kbjNiCmt6NTdtSW5YMlZwaURPS2c0a3lIZml1WFdwaUJxckN4MEtOTHB4bzNERVFjRmNzUVRlVEh6aDQ3NTJHVjA0UlUKVGkvR0VXeXpJc2w0Umc3dEd0QXdtY0lQZ1VOVWZZMlEzOTBGR3FkSDRhaG4rbXcvNmFGYlczMVc2M2Q5WUpWcQppb3lPVmNhTUlwTTVCL2M3UWM4U3VoQ0kxWUdoVXlnNGNSSExFdzVWdGlraW95RTNYMDRrbmEzalFBajU0WWJSCmJwRWhjMzVhcEtMQjIxSE9VUUlEQVFBQm8xTXdVVEFkQmdOVkhRNEVGZ1FVeXZsMFZJNXZKVlN1WUZYdTdCNDgKNlBiTUVBb3dId1lEVlIwakJCZ3dGb0FVeXZsMFZJNXZKVlN1WUZYdTdCNDg2UGJNRUFvd0R3WURWUjBUQVFILwpCQVV3QXdFQi96QU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFNTHhyZ0ZWTXVOUnEyd0F3Y0J0N1NuTlI1Q2Z6CjJNdlhxNUVVbXVhd0lVaTlrYVlqd2RWaURSRUdTams3SlcxN3ZsNTc2SGpEa2RmUndpNEUyOFN5ZFJJblpmNkoKaThIWmNaN2NhSDZEeFIzMzVmZ0hWekxpNU5pVGNlL09qTkJRelEyTUpYVkRkOERCbUc1ZnlhdEppT0pRNGJXRQpBN0ZsUDBSZFAzQ08zR1dFME01aVhPQjJtMXFXa0UyZXlPNFVIdndUcU5RTGRyZEFYZ0RRbGJhbTllNEJHM0dnCmQvNnRoQWtXRGJ0L1FOVCtFSkhEQ3ZoRFJLaDFSdUdIeWcrWSsvbmViVFdXckZXc2t0UnJiT29IQ1ppQ3BYSTEKM2VYRTZudDBZa2d0RHhHMjJLcW5ocEFnOWdVU3MyaGxob3h5dmt6eUYwbXU2TmhQbHdBZ25xNysvUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + name: policy-btls-grpc/envoy-gateway-ca + sni: grpc-backend + weight: 1 + - addressType: IP + endpoints: + - host: 8.8.8.8 + port: 9000 + protocol: GRPC + weight: 1 + - addressType: IP + endpoints: + - host: 1.1.1.1 + port: 3001 + protocol: GRPC + weight: 1 + - addressType: IP + endpoints: + - host: 2.2.2.2 + port: 3443 + protocol: GRPC + tls: + caCertificate: + certificate: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURKekNDQWcrZ0F3SUJBZ0lVQWw2VUtJdUttenRlODFjbGx6NVBmZE4ySWxJd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0l6RVFNQTRHQTFVRUF3d0hiWGxqYVdWdWRERVBNQTBHQTFVRUNnd0dhM1ZpWldSaU1CNFhEVEl6TVRBdwpNakExTkRFMU4xb1hEVEkwTVRBd01UQTFOREUxTjFvd0l6RVFNQTRHQTFVRUF3d0hiWGxqYVdWdWRERVBNQTBHCkExVUVDZ3dHYTNWaVpXUmlNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXdTVGMKMXlqOEhXNjJueW5rRmJYbzRWWEt2MmpDMFBNN2RQVmt5ODdGd2VaY1RLTG9XUVZQUUUycDJrTERLNk9Fc3ptTQp5eXIreHhXdHlpdmVyZW1yV3FuS2tOVFloTGZZUGhnUWtjemliN2VVYWxtRmpVYmhXZEx2SGFrYkVnQ29kbjNiCmt6NTdtSW5YMlZwaURPS2c0a3lIZml1WFdwaUJxckN4MEtOTHB4bzNERVFjRmNzUVRlVEh6aDQ3NTJHVjA0UlUKVGkvR0VXeXpJc2w0Umc3dEd0QXdtY0lQZ1VOVWZZMlEzOTBGR3FkSDRhaG4rbXcvNmFGYlczMVc2M2Q5WUpWcQppb3lPVmNhTUlwTTVCL2M3UWM4U3VoQ0kxWUdoVXlnNGNSSExFdzVWdGlraW95RTNYMDRrbmEzalFBajU0WWJSCmJwRWhjMzVhcEtMQjIxSE9VUUlEQVFBQm8xTXdVVEFkQmdOVkhRNEVGZ1FVeXZsMFZJNXZKVlN1WUZYdTdCNDgKNlBiTUVBb3dId1lEVlIwakJCZ3dGb0FVeXZsMFZJNXZKVlN1WUZYdTdCNDg2UGJNRUFvd0R3WURWUjBUQVFILwpCQVV3QXdFQi96QU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFNTHhyZ0ZWTXVOUnEyd0F3Y0J0N1NuTlI1Q2Z6CjJNdlhxNUVVbXVhd0lVaTlrYVlqd2RWaURSRUdTams3SlcxN3ZsNTc2SGpEa2RmUndpNEUyOFN5ZFJJblpmNkoKaThIWmNaN2NhSDZEeFIzMzVmZ0hWekxpNU5pVGNlL09qTkJRelEyTUpYVkRkOERCbUc1ZnlhdEppT0pRNGJXRQpBN0ZsUDBSZFAzQ08zR1dFME01aVhPQjJtMXFXa0UyZXlPNFVIdndUcU5RTGRyZEFYZ0RRbGJhbTllNEJHM0dnCmQvNnRoQWtXRGJ0L1FOVCtFSkhEQ3ZoRFJLaDFSdUdIeWcrWSsvbmViVFdXckZXc2t0UnJiT29IQ1ppQ3BYSTEKM2VYRTZudDBZa2d0RHhHMjJLcW5ocEFnOWdVU3MyaGxob3h5dmt6eUYwbXU2TmhQbHdBZ25xNysvUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + name: policy-btls-backend-ip/envoy-gateway-ca + sni: ip-backend + weight: 1 + name: envoyextensionpolicy/default/policy-for-http-route/extproc/0 hostname: www.foo.com isHTTP2: false metadata: diff --git a/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-targetrefs.out.yaml b/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-targetrefs.out.yaml index 24ef89d273f..4abc9f59092 100644 --- a/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-targetrefs.out.yaml +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-targetrefs.out.yaml @@ -228,6 +228,30 @@ xdsIR: - weight: 1 directResponse: statusCode: 500 + envoyExtensions: + wasms: + - config: + parameter1: + key1: value1 + key2: value2 + parameter2: value3 + failOpen: false + httpWasmCode: + originalDownloadingURL: https://www.example.com/wasm-filter-1.wasm + servingURL: https://envoy-gateway:18002/5c90b9a82642ce00a7753923fabead306b9d9a54a7c0bd2463a1af3efcfb110b.wasm + sha256: 746df05c8f3a0b07a46c0967cfbc5cbe5b9d48d0f79b6177eeedf8be6c8b34b5 + name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/wasm/0 + wasmName: wasm-filter-1 + - config: + parameter1: value1 + parameter2: value2 + failOpen: false + httpWasmCode: + originalDownloadingURL: https://www.example.com/wasm-filter-2.wasm + servingURL: https://envoy-gateway:18002/593e4cc60a7e0fa4d4f86531a5e20e785213a52000f056a7a8b5c5afcb908052.wasm + sha256: a1efca12ea51069abb123bf9c77889fcc2a31cc5483fc14d115e44fdf07c7980 + name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/wasm/1 + wasmName: wasm-filter-2 hostname: www.example.com isHTTP2: false metadata: @@ -239,35 +263,36 @@ xdsIR: distinct: false name: "" prefix: /foo - wasm: - - config: - parameter1: - key1: value1 - key2: value2 - parameter2: value3 - failOpen: false - httpWasmCode: - originalDownloadingURL: https://www.example.com/wasm-filter-1.wasm - servingURL: https://envoy-gateway:18002/5c90b9a82642ce00a7753923fabead306b9d9a54a7c0bd2463a1af3efcfb110b.wasm - sha256: 746df05c8f3a0b07a46c0967cfbc5cbe5b9d48d0f79b6177eeedf8be6c8b34b5 - name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/wasm/0 - wasmName: wasm-filter-1 - - config: - parameter1: value1 - parameter2: value2 - failOpen: false - httpWasmCode: - originalDownloadingURL: https://www.example.com/wasm-filter-2.wasm - servingURL: https://envoy-gateway:18002/593e4cc60a7e0fa4d4f86531a5e20e785213a52000f056a7a8b5c5afcb908052.wasm - sha256: a1efca12ea51069abb123bf9c77889fcc2a31cc5483fc14d115e44fdf07c7980 - name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/wasm/1 - wasmName: wasm-filter-2 - destination: name: httproute/envoy-gateway/httproute-2/rule/0 settings: - weight: 1 directResponse: statusCode: 500 + envoyExtensions: + wasms: + - config: + parameter1: + key1: value1 + key2: value2 + parameter2: value3 + failOpen: false + httpWasmCode: + originalDownloadingURL: https://www.example.com/wasm-filter-1.wasm + servingURL: https://envoy-gateway:18002/5c90b9a82642ce00a7753923fabead306b9d9a54a7c0bd2463a1af3efcfb110b.wasm + sha256: 746df05c8f3a0b07a46c0967cfbc5cbe5b9d48d0f79b6177eeedf8be6c8b34b5 + name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/wasm/0 + wasmName: wasm-filter-1 + - config: + parameter1: value1 + parameter2: value2 + failOpen: false + httpWasmCode: + originalDownloadingURL: https://www.example.com/wasm-filter-2.wasm + servingURL: https://envoy-gateway:18002/593e4cc60a7e0fa4d4f86531a5e20e785213a52000f056a7a8b5c5afcb908052.wasm + sha256: a1efca12ea51069abb123bf9c77889fcc2a31cc5483fc14d115e44fdf07c7980 + name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/wasm/1 + wasmName: wasm-filter-2 hostname: www.example.com isHTTP2: false metadata: @@ -279,26 +304,3 @@ xdsIR: distinct: false name: "" prefix: /bar - wasm: - - config: - parameter1: - key1: value1 - key2: value2 - parameter2: value3 - failOpen: false - httpWasmCode: - originalDownloadingURL: https://www.example.com/wasm-filter-1.wasm - servingURL: https://envoy-gateway:18002/5c90b9a82642ce00a7753923fabead306b9d9a54a7c0bd2463a1af3efcfb110b.wasm - sha256: 746df05c8f3a0b07a46c0967cfbc5cbe5b9d48d0f79b6177eeedf8be6c8b34b5 - name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/wasm/0 - wasmName: wasm-filter-1 - - config: - parameter1: value1 - parameter2: value2 - failOpen: false - httpWasmCode: - originalDownloadingURL: https://www.example.com/wasm-filter-2.wasm - servingURL: https://envoy-gateway:18002/593e4cc60a7e0fa4d4f86531a5e20e785213a52000f056a7a8b5c5afcb908052.wasm - sha256: a1efca12ea51069abb123bf9c77889fcc2a31cc5483fc14d115e44fdf07c7980 - name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/wasm/1 - wasmName: wasm-filter-2 diff --git a/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm.out.yaml b/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm.out.yaml index d7627ea333f..68cfaf92515 100644 --- a/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm.out.yaml +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm.out.yaml @@ -265,6 +265,21 @@ xdsIR: port: 8080 protocol: HTTP weight: 1 + envoyExtensions: + wasms: + - config: + parameter1: + key1: value1 + parameter2: + key2: + key3: value3 + failOpen: true + httpWasmCode: + originalDownloadingURL: https://www.test.com/wasm-filter-4.wasm + servingURL: https://envoy-gateway:18002/fe571e7b1ef5dc626ceb2c2c86782a134a92989a2643485238951696ae4334c3.wasm + sha256: a1f0b78b8c1320690327800e3a5de10e7dbba7b6c752e702193a395a52c727b6 + name: envoyextensionpolicy/default/policy-for-http-route/wasm/0 + wasmName: wasm-filter-4 hostname: www.example.com isHTTP2: false metadata: @@ -276,20 +291,6 @@ xdsIR: distinct: false name: "" prefix: /foo - wasm: - - config: - parameter1: - key1: value1 - parameter2: - key2: - key3: value3 - failOpen: true - httpWasmCode: - originalDownloadingURL: https://www.test.com/wasm-filter-4.wasm - servingURL: https://envoy-gateway:18002/fe571e7b1ef5dc626ceb2c2c86782a134a92989a2643485238951696ae4334c3.wasm - sha256: a1f0b78b8c1320690327800e3a5de10e7dbba7b6c752e702193a395a52c727b6 - name: envoyextensionpolicy/default/policy-for-http-route/wasm/0 - wasmName: wasm-filter-4 - destination: name: httproute/default/httproute-2/rule/0 settings: @@ -299,6 +300,39 @@ xdsIR: port: 8080 protocol: HTTP weight: 1 + envoyExtensions: + wasms: + - config: + parameter1: + key1: value1 + key2: value2 + parameter2: value3 + failOpen: false + httpWasmCode: + originalDownloadingURL: https://www.example.com/wasm-filter-1.wasm + servingURL: https://envoy-gateway:18002/5c90b9a82642ce00a7753923fabead306b9d9a54a7c0bd2463a1af3efcfb110b.wasm + sha256: 746df05c8f3a0b07a46c0967cfbc5cbe5b9d48d0f79b6177eeedf8be6c8b34b5 + name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/wasm/0 + wasmName: wasm-filter-1 + - config: + parameter1: value1 + parameter2: value2 + failOpen: false + httpWasmCode: + originalDownloadingURL: oci://www.example.com/wasm-filter-2:v1.0.0 + servingURL: https://envoy-gateway:18002/7abf116e5cd5a20389604a5ba0f3bd04fdf76f92181fe67506b42c2ee596d3fd.wasm + sha256: 314100af781b98a8ca175d5bf90a8bf76576e20a2f397a88223404edc6ebfd46 + name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/wasm/1 + rootID: my-root-id + wasmName: wasm-filter-2 + - config: null + failOpen: false + httpWasmCode: + originalDownloadingURL: oci://www.example.com:8080/wasm-filter-3:latest + servingURL: https://envoy-gateway:18002/42d30b4a4cc631415e6e48c02d244700da327201eb273f752cacf745715b31d9.wasm + sha256: 2a19e4f337e5223d7287e7fccd933fb01905deaff804292e5257f8c681b82bee + name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/wasm/2 + wasmName: envoyextensionpolicy/envoy-gateway/policy-for-gateway/wasm/2 hostname: www.example.com isHTTP2: false metadata: @@ -310,35 +344,3 @@ xdsIR: distinct: false name: "" prefix: /bar - wasm: - - config: - parameter1: - key1: value1 - key2: value2 - parameter2: value3 - failOpen: false - httpWasmCode: - originalDownloadingURL: https://www.example.com/wasm-filter-1.wasm - servingURL: https://envoy-gateway:18002/5c90b9a82642ce00a7753923fabead306b9d9a54a7c0bd2463a1af3efcfb110b.wasm - sha256: 746df05c8f3a0b07a46c0967cfbc5cbe5b9d48d0f79b6177eeedf8be6c8b34b5 - name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/wasm/0 - wasmName: wasm-filter-1 - - config: - parameter1: value1 - parameter2: value2 - failOpen: false - httpWasmCode: - originalDownloadingURL: oci://www.example.com/wasm-filter-2:v1.0.0 - servingURL: https://envoy-gateway:18002/7abf116e5cd5a20389604a5ba0f3bd04fdf76f92181fe67506b42c2ee596d3fd.wasm - sha256: 314100af781b98a8ca175d5bf90a8bf76576e20a2f397a88223404edc6ebfd46 - name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/wasm/1 - rootID: my-root-id - wasmName: wasm-filter-2 - - config: null - failOpen: false - httpWasmCode: - originalDownloadingURL: oci://www.example.com:8080/wasm-filter-3:latest - servingURL: https://envoy-gateway:18002/42d30b4a4cc631415e6e48c02d244700da327201eb273f752cacf745715b31d9.wasm - sha256: 2a19e4f337e5223d7287e7fccd933fb01905deaff804292e5257f8c681b82bee - name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/wasm/2 - wasmName: envoyextensionpolicy/envoy-gateway/policy-for-gateway/wasm/2 diff --git a/internal/gatewayapi/testdata/envoyproxy-accesslog-cel.in.yaml b/internal/gatewayapi/testdata/envoyproxy-accesslog-cel.in.yaml index c60d371c64f..5a953cb19ad 100644 --- a/internal/gatewayapi/testdata/envoyproxy-accesslog-cel.in.yaml +++ b/internal/gatewayapi/testdata/envoyproxy-accesslog-cel.in.yaml @@ -24,6 +24,14 @@ envoyProxyForGatewayClass: port: 4317 resources: k8s.cluster.name: "cluster-1" + - sinks: + - type: ALS + als: + backendRefs: + - name: envoy-als + namespace: monitoring + port: 9000 + type: TCP provider: type: Kubernetes kubernetes: @@ -87,3 +95,36 @@ gateways: allowedRoutes: namespaces: from: Same +services: +- apiVersion: v1 + kind: Service + metadata: + name: envoy-als + namespace: monitoring + spec: + type: ClusterIP + ports: + - name: grpc + port: 9000 + appProtocol: grpc + protocol: TCP + targetPort: 9000 +endpointSlices: +- apiVersion: discovery.k8s.io/v1 + kind: EndpointSlice + metadata: + name: endpointslice-envoy-als + namespace: monitoring + labels: + kubernetes.io/service-name: envoy-als + addressType: IPv4 + ports: + - name: grpc + appProtocol: grpc + protocol: TCP + port: 9090 + endpoints: + - addresses: + - "10.240.0.10" + conditions: + ready: true diff --git a/internal/gatewayapi/testdata/envoyproxy-accesslog-cel.out.yaml b/internal/gatewayapi/testdata/envoyproxy-accesslog-cel.out.yaml index 638c72209dc..436eec793ce 100644 --- a/internal/gatewayapi/testdata/envoyproxy-accesslog-cel.out.yaml +++ b/internal/gatewayapi/testdata/envoyproxy-accesslog-cel.out.yaml @@ -118,6 +118,14 @@ infraIR: resources: k8s.cluster.name: cluster-1 type: OpenTelemetry + - sinks: + - als: + backendRefs: + - name: envoy-als + namespace: monitoring + port: 9000 + type: TCP + type: ALS status: {} listeners: - address: null @@ -135,10 +143,21 @@ infraIR: xdsIR: envoy-gateway/gateway-1: accessLog: - celMatches: - - response.code >= 400 + als: + - destination: + name: accesslog_als_1_0 + settings: + - addressType: IP + endpoints: + - host: 10.240.0.10 + port: 9090 + protocol: GRPC + name: envoy-gateway-system/test + type: TCP openTelemetry: - authority: otel-collector.monitoring.svc.cluster.local + celMatches: + - response.code >= 400 destination: name: accesslog_otel_0_1 settings: @@ -152,7 +171,9 @@ xdsIR: text: | [%START_TIME%] "%REQ(:METHOD)% %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 text: - - format: | + - celMatches: + - response.code >= 400 + format: | [%START_TIME%] "%REQ(:METHOD)% %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 path: /dev/stdout http: diff --git a/internal/gatewayapi/testdata/envoyproxy-accesslog-without-format.out.yaml b/internal/gatewayapi/testdata/envoyproxy-accesslog-without-format.out.yaml index ccab7025d8b..85d7b807101 100644 --- a/internal/gatewayapi/testdata/envoyproxy-accesslog-without-format.out.yaml +++ b/internal/gatewayapi/testdata/envoyproxy-accesslog-without-format.out.yaml @@ -102,8 +102,7 @@ infraIR: telemetry: accessLog: settings: - - format: null - sinks: + - sinks: - file: path: /dev/stdout type: File diff --git a/internal/gatewayapi/testdata/httproute-with-multi-gateways-notmatch.in.yaml b/internal/gatewayapi/testdata/httproute-with-multi-gateways-notmatch.in.yaml new file mode 100644 index 00000000000..d0f3a2cbe93 --- /dev/null +++ b/internal/gatewayapi/testdata/httproute-with-multi-gateways-notmatch.in.yaml @@ -0,0 +1,45 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All + - apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-2 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All +httpRoutes: + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + parentRefs: + - name: gateway-1 + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 diff --git a/internal/gatewayapi/testdata/httproute-with-multi-gateways-notmatch.out.yaml b/internal/gatewayapi/testdata/httproute-with-multi-gateways-notmatch.out.yaml new file mode 100644 index 00000000000..c7801a560d0 --- /dev/null +++ b/internal/gatewayapi/testdata/httproute-with-multi-gateways-notmatch.out.yaml @@ -0,0 +1,151 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-2 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +infraIR: + envoy-gateway/gateway-1: + proxy: + listeners: + - address: null + name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway/gateway-1 + envoy-gateway/gateway-2: + proxy: + listeners: + - address: null + name: envoy-gateway/gateway-2/http + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-2 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway/gateway-2 +xdsIR: + envoy-gateway/gateway-1: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + name: envoy-gateway/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + envoy-gateway/gateway-2: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + metadata: + kind: Gateway + name: gateway-2 + namespace: envoy-gateway + sectionName: http + name: envoy-gateway/gateway-2/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 diff --git a/internal/gatewayapi/testdata/httproute-with-multi-gateways-with-same-name.in.yaml b/internal/gatewayapi/testdata/httproute-with-multi-gateways-with-same-name.in.yaml new file mode 100644 index 00000000000..f22fdca792a --- /dev/null +++ b/internal/gatewayapi/testdata/httproute-with-multi-gateways-with-same-name.in.yaml @@ -0,0 +1,45 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All + - apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: default + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All +httpRoutes: + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + parentRefs: + - name: gateway-1 + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 diff --git a/internal/gatewayapi/testdata/httproute-with-multi-gateways-with-same-name.out.yaml b/internal/gatewayapi/testdata/httproute-with-multi-gateways-with-same-name.out.yaml new file mode 100644 index 00000000000..f3c1a6b1ed4 --- /dev/null +++ b/internal/gatewayapi/testdata/httproute-with-multi-gateways-with-same-name.out.yaml @@ -0,0 +1,205 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-1 + namespace: default + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-1 + namespace: default + spec: + parentRefs: + - name: gateway-1 + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: / + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 +infraIR: + default/gateway-1: + proxy: + listeners: + - address: null + name: default/gateway-1/http + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: default + name: default/gateway-1 + envoy-gateway/gateway-1: + proxy: + listeners: + - address: null + name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway/gateway-1 +xdsIR: + default/gateway-1: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + metadata: + kind: Gateway + name: gateway-1 + namespace: default + sectionName: http + name: default/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + hostname: '*' + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/* + pathMatch: + distinct: false + name: "" + prefix: / + envoy-gateway/gateway-1: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + name: envoy-gateway/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 diff --git a/internal/gatewayapi/testdata/securitypolicy-override-replace.in.yaml b/internal/gatewayapi/testdata/securitypolicy-override-replace.in.yaml index debfe1dd46d..bf3bfcedc9d 100644 --- a/internal/gatewayapi/testdata/securitypolicy-override-replace.in.yaml +++ b/internal/gatewayapi/testdata/securitypolicy-override-replace.in.yaml @@ -52,6 +52,25 @@ httpRoutes: backendRefs: - name: service-1 port: 8080 +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-3 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/baz" + backendRefs: + - name: service-1 + port: 8080 securityPolicies: - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: SecurityPolicy @@ -113,3 +132,13 @@ securityPolicies: - "x-header-7" - "x-header-8" maxAge: 2000s +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + namespace: default + name: policy-for-route-3 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-3 diff --git a/internal/gatewayapi/testdata/securitypolicy-override-replace.out.yaml b/internal/gatewayapi/testdata/securitypolicy-override-replace.out.yaml index 7c0dcab6060..c6f72065531 100644 --- a/internal/gatewayapi/testdata/securitypolicy-override-replace.out.yaml +++ b/internal/gatewayapi/testdata/securitypolicy-override-replace.out.yaml @@ -16,7 +16,7 @@ gateways: protocol: HTTP status: listeners: - - attachedRoutes: 2 + - attachedRoutes: 3 conditions: - lastTransitionTime: null message: Sending translated listener configuration to the data plane @@ -116,6 +116,44 @@ httpRoutes: name: gateway-1 namespace: envoy-gateway sectionName: http +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-3 + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: /baz + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http infraIR: envoy-gateway/gateway-1: proxy: @@ -173,6 +211,32 @@ securityPolicies: status: "True" type: Accepted controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + creationTimestamp: null + name: policy-for-route-3 + namespace: default + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-3 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: SecurityPolicy metadata: @@ -225,7 +289,7 @@ securityPolicies: type: Accepted - lastTransitionTime: null message: 'This policy is being overridden by other securityPolicies for these - routes: [default/httproute-1]' + routes: [default/httproute-1 default/httproute-3]' reason: Overridden status: "True" type: Overridden @@ -343,3 +407,24 @@ xdsIR: name: example1 remoteJWKS: uri: https://one.example.com/jwt/public-key/jwks.json + - destination: + name: httproute/default/httproute-3/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-3 + namespace: default + name: httproute/default/httproute-3/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: /baz + security: {} diff --git a/internal/gatewayapi/testdata/securitypolicy-with-extauth-invalid-no-matching-port.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-extauth-invalid-no-matching-port.out.yaml index 6494f2d68c4..c5bf4237f52 100644 --- a/internal/gatewayapi/testdata/securitypolicy-with-extauth-invalid-no-matching-port.out.yaml +++ b/internal/gatewayapi/testdata/securitypolicy-with-extauth-invalid-no-matching-port.out.yaml @@ -160,6 +160,8 @@ xdsIR: port: 8080 protocol: HTTP weight: 1 + directResponse: + statusCode: 500 hostname: www.foo.com isHTTP2: false metadata: diff --git a/internal/gatewayapi/testdata/securitypolicy-with-extauth-invalid-no-port.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-extauth-invalid-no-port.out.yaml index 619025bcf9d..3f5e60f11e8 100644 --- a/internal/gatewayapi/testdata/securitypolicy-with-extauth-invalid-no-port.out.yaml +++ b/internal/gatewayapi/testdata/securitypolicy-with-extauth-invalid-no-port.out.yaml @@ -160,6 +160,8 @@ xdsIR: port: 8080 protocol: HTTP weight: 1 + directResponse: + statusCode: 500 hostname: www.foo.com isHTTP2: false metadata: diff --git a/internal/gatewayapi/testdata/securitypolicy-with-extauth-invalid-no-reference-grant.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-extauth-invalid-no-reference-grant.out.yaml index e3f8f65f700..1f8fd280ad6 100644 --- a/internal/gatewayapi/testdata/securitypolicy-with-extauth-invalid-no-reference-grant.out.yaml +++ b/internal/gatewayapi/testdata/securitypolicy-with-extauth-invalid-no-reference-grant.out.yaml @@ -161,6 +161,8 @@ xdsIR: port: 8080 protocol: HTTP weight: 1 + directResponse: + statusCode: 500 hostname: www.foo.com isHTTP2: false metadata: diff --git a/internal/gatewayapi/testdata/securitypolicy-with-extauth-invalid-no-service.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-extauth-invalid-no-service.out.yaml index 3b63963bc55..294267b90e0 100644 --- a/internal/gatewayapi/testdata/securitypolicy-with-extauth-invalid-no-service.out.yaml +++ b/internal/gatewayapi/testdata/securitypolicy-with-extauth-invalid-no-service.out.yaml @@ -160,6 +160,8 @@ xdsIR: port: 8080 protocol: HTTP weight: 1 + directResponse: + statusCode: 500 hostname: www.foo.com isHTTP2: false metadata: diff --git a/internal/gatewayapi/testdata/securitypolicy-with-jwt-and-invalid-oidc.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-jwt-and-invalid-oidc.out.yaml index 3ad2f4f10e7..d5731870d17 100644 --- a/internal/gatewayapi/testdata/securitypolicy-with-jwt-and-invalid-oidc.out.yaml +++ b/internal/gatewayapi/testdata/securitypolicy-with-jwt-and-invalid-oidc.out.yaml @@ -258,6 +258,8 @@ xdsIR: port: 8080 protocol: HTTP weight: 1 + directResponse: + statusCode: 500 hostname: gateway.envoyproxy.io isHTTP2: false metadata: @@ -290,6 +292,8 @@ xdsIR: port: 8080 protocol: HTTP weight: 1 + directResponse: + statusCode: 500 hostname: gateway.envoyproxy.io isHTTP2: false metadata: diff --git a/internal/globalratelimit/runner/runner.go b/internal/globalratelimit/runner/runner.go index f5590983441..0a2d987c182 100644 --- a/internal/globalratelimit/runner/runner.go +++ b/internal/globalratelimit/runner/runner.go @@ -17,6 +17,7 @@ import ( "strconv" discoveryv3 "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" + cachetype "github.com/envoyproxy/go-control-plane/pkg/cache/types" cachev3 "github.com/envoyproxy/go-control-plane/pkg/cache/v3" resourcev3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3" serverv3 "github.com/envoyproxy/go-control-plane/pkg/server/v3" @@ -109,27 +110,40 @@ func (r *Runner) serveXdsConfigServer(ctx context.Context) { } } +func buildXDSResourceFromCache(rateLimitConfigsCache map[string][]cachetype.Resource) types.XdsResources { + xdsResourcesToUpdate := types.XdsResources{} + for _, xdsR := range rateLimitConfigsCache { + xdsResourcesToUpdate[resourcev3.RateLimitConfigType] = append(xdsResourcesToUpdate[resourcev3.RateLimitConfigType], xdsR...) + } + + return xdsResourcesToUpdate +} + func (r *Runner) subscribeAndTranslate(ctx context.Context) { + // rateLimitConfigsCache is a cache of the rate limit config, which is keyed by the xdsIR key. + rateLimitConfigsCache := map[string][]cachetype.Resource{} + // Subscribe to resources. message.HandleSubscription(message.Metadata{Runner: string(egv1a1.LogComponentGlobalRateLimitRunner), Message: "xds-ir"}, r.XdsIR.Subscribe(ctx), func(update message.Update[string, *ir.Xds], errChan chan error) { r.Logger.Info("received a notification") if update.Delete { - if err := r.addNewSnapshot(ctx, nil); err != nil { - r.Logger.Error(err, "failed to update the config snapshot") - errChan <- err - } + delete(rateLimitConfigsCache, update.Key) + r.updateSnapshot(ctx, buildXDSResourceFromCache(rateLimitConfigsCache)) } else { // Translate to ratelimit xDS Config. rvt, err := r.translate(update.Value) if err != nil { - r.Logger.Error(err, err.Error()) + r.Logger.Error(err, "failed to translate an updated xds-ir to ratelimit xDS Config") + errChan <- err } // Update ratelimit xDS config cache. if rvt != nil { - r.updateSnapshot(ctx, rvt.XdsResources) + // Build XdsResources to use for the snapshot update from the cache. + rateLimitConfigsCache[update.Key] = rvt.XdsResources[resourcev3.RateLimitConfigType] + r.updateSnapshot(ctx, buildXDSResourceFromCache(rateLimitConfigsCache)) } } }, diff --git a/internal/globalratelimit/runner/runner_test.go b/internal/globalratelimit/runner/runner_test.go new file mode 100644 index 00000000000..e25f714792b --- /dev/null +++ b/internal/globalratelimit/runner/runner_test.go @@ -0,0 +1,244 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package runner + +import ( + "context" + "fmt" + "testing" + "time" + + cachetypes "github.com/envoyproxy/go-control-plane/pkg/cache/types" + cachev3 "github.com/envoyproxy/go-control-plane/pkg/cache/v3" + resourcev3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3" + rlsconfv3 "github.com/envoyproxy/go-control-plane/ratelimit/config/ratelimit/v3" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" + "github.com/envoyproxy/gateway/internal/envoygateway/config" + "github.com/envoyproxy/gateway/internal/infrastructure/kubernetes/ratelimit" + "github.com/envoyproxy/gateway/internal/ir" + "github.com/envoyproxy/gateway/internal/message" +) + +func Test_subscribeAndTranslate(t *testing.T) { + t.Parallel() + + testxds := func(gwName string) *ir.Xds { + return &ir.Xds{ + HTTP: []*ir.HTTPListener{ + { + CoreListenerDetails: ir.CoreListenerDetails{ + Name: fmt.Sprintf("default/%s/listener-0", gwName), + }, + Routes: []*ir.HTTPRoute{ + { + Name: "route-0", + Traffic: &ir.TrafficFeatures{ + RateLimit: &ir.RateLimit{ + Global: &ir.GlobalRateLimit{ + Rules: []*ir.RateLimitRule{ + { + HeaderMatches: []*ir.StringMatch{ + { + Name: "x-user-id", + Distinct: true, + }, + }, + Limit: ir.RateLimitValue{ + Requests: 100, + Unit: ir.RateLimitUnit(egv1a1.RateLimitUnitMinute), + }, + }, + { + HeaderMatches: []*ir.StringMatch{ + { + Name: "x-another-user-id", + Distinct: true, + }, + }, + Limit: ir.RateLimitValue{ + Requests: 10, + Unit: ir.RateLimitUnit(egv1a1.RateLimitUnitSecond), + }, + }, + }, + }, + }, + }, + }, + { + Name: "route-1", + Traffic: &ir.TrafficFeatures{ + RateLimit: &ir.RateLimit{ + Global: &ir.GlobalRateLimit{ + Rules: []*ir.RateLimitRule{ + { + HeaderMatches: []*ir.StringMatch{ + { + Name: "x-user-id", + Distinct: true, + }, + }, + Limit: ir.RateLimitValue{ + Requests: 100, + Unit: ir.RateLimitUnit(egv1a1.RateLimitUnitMinute), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + } + + testRateLimitConfig := func(gwName string) *rlsconfv3.RateLimitConfig { + return &rlsconfv3.RateLimitConfig{ + Name: fmt.Sprintf("default/%s/listener-0", gwName), + Domain: fmt.Sprintf("default/%s/listener-0", gwName), + Descriptors: []*rlsconfv3.RateLimitDescriptor{ + { + Key: "route-0", + Value: "route-0", + Descriptors: []*rlsconfv3.RateLimitDescriptor{ + { + Key: "rule-0-match-0", + RateLimit: &rlsconfv3.RateLimitPolicy{ + Unit: rlsconfv3.RateLimitUnit_MINUTE, + RequestsPerUnit: 100, + }, + }, + { + Key: "rule-1-match-0", + RateLimit: &rlsconfv3.RateLimitPolicy{ + Unit: rlsconfv3.RateLimitUnit_SECOND, + RequestsPerUnit: 10, + }, + }, + }, + }, + { + Key: "route-1", + Value: "route-1", + Descriptors: []*rlsconfv3.RateLimitDescriptor{ + { + Key: "rule-0-match-0", + RateLimit: &rlsconfv3.RateLimitPolicy{ + Unit: rlsconfv3.RateLimitUnit_MINUTE, + RequestsPerUnit: 100, + }, + }, + }, + }, + }, + } + } + + testCases := []struct { + name string + // xdsIRs contains a list of xds updates that the runner will receive. + xdsIRs []message.Update[string, *ir.Xds] + wantRateLimitConfigs map[string]cachetypes.Resource + }{ + { + name: "one xds is added", + xdsIRs: []message.Update[string, *ir.Xds]{ + { + Key: "gw0", + Value: testxds("gw0"), + }, + }, + wantRateLimitConfigs: map[string]cachetypes.Resource{ + "default/gw0/listener-0": testRateLimitConfig("gw0"), + }, + }, + { + name: "two xds are added", + xdsIRs: []message.Update[string, *ir.Xds]{ + { + Key: "gw0", + Value: testxds("gw0"), + }, + { + Key: "gw1", + Value: testxds("gw1"), + }, + }, + wantRateLimitConfigs: map[string]cachetypes.Resource{ + "default/gw0/listener-0": testRateLimitConfig("gw0"), + "default/gw1/listener-0": testRateLimitConfig("gw1"), + }, + }, + { + name: "one xds is deleted", + xdsIRs: []message.Update[string, *ir.Xds]{ + { + Key: "gw0", + Value: testxds("gw0"), + }, + { + Key: "gw1", + Value: testxds("gw1"), + }, + { + Key: "gw0", + Delete: true, + }, + }, + wantRateLimitConfigs: map[string]cachetypes.Resource{ + "default/gw1/listener-0": testRateLimitConfig("gw1"), + }, + }, + } + + for _, tt := range testCases { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + xdsIR := new(message.XdsIR) + defer xdsIR.Close() + cfg, err := config.New() + require.NoError(t, err) + + r := New(&Config{ + Server: *cfg, + XdsIR: xdsIR, + cache: cachev3.NewSnapshotCache(false, cachev3.IDHash{}, nil), + }) + + go r.subscribeAndTranslate(ctx) + + for _, xds := range tt.xdsIRs { + if xds.Delete { + xdsIR.Delete(xds.Key) + continue + } + xdsIR.Store(xds.Key, xds.Value) + } + + diff := "" + if !assert.Eventually(t, func() bool { + rs, err := r.cache.GetSnapshot(ratelimit.InfraName) + require.NoError(t, err) + + diff = cmp.Diff(tt.wantRateLimitConfigs, rs.GetResources(resourcev3.RateLimitConfigType), cmpopts.IgnoreUnexported(rlsconfv3.RateLimitConfig{}, rlsconfv3.RateLimitDescriptor{}, rlsconfv3.RateLimitPolicy{})) + return diff == "" + }, time.Second*1, time.Millisecond*20) { + t.Fatalf("snapshot mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/internal/infrastructure/kubernetes/proxy/resource.go b/internal/infrastructure/kubernetes/proxy/resource.go index c4da07d893d..f89491f4380 100644 --- a/internal/infrastructure/kubernetes/proxy/resource.go +++ b/internal/infrastructure/kubernetes/proxy/resource.go @@ -204,6 +204,19 @@ func expectedProxyContainers(infra *ir.ProxyInfra, VolumeMounts: expectedContainerVolumeMounts(containerSpec), TerminationMessagePolicy: corev1.TerminationMessageReadFile, TerminationMessagePath: "/dev/termination-log", + StartupProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: bootstrap.EnvoyReadinessPath, + Port: intstr.IntOrString{Type: intstr.Int, IntVal: bootstrap.EnvoyReadinessPort}, + Scheme: corev1.URISchemeHTTP, + }, + }, + TimeoutSeconds: 1, + PeriodSeconds: 10, + SuccessThreshold: 1, + FailureThreshold: 30, + }, ReadinessProbe: &corev1.Probe{ ProbeHandler: corev1.ProbeHandler{ HTTPGet: &corev1.HTTPGetAction{ @@ -237,6 +250,19 @@ func expectedProxyContainers(infra *ir.ProxyInfra, Resources: *egv1a1.DefaultShutdownManagerContainerResourceRequirements(), TerminationMessagePolicy: corev1.TerminationMessageReadFile, TerminationMessagePath: "/dev/termination-log", + StartupProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: envoy.ShutdownManagerHealthCheckPath, + Port: intstr.IntOrString{Type: intstr.Int, IntVal: envoy.ShutdownManagerPort}, + Scheme: corev1.URISchemeHTTP, + }, + }, + TimeoutSeconds: 1, + PeriodSeconds: 10, + SuccessThreshold: 1, + FailureThreshold: 30, + }, ReadinessProbe: &corev1.Probe{ ProbeHandler: corev1.ProbeHandler{ HTTPGet: &corev1.HTTPGetAction{ diff --git a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/component-level.yaml b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/component-level.yaml index 1d3743ee836..9065d07a543 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/component-level.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/component-level.yaml @@ -86,6 +86,15 @@ spec: requests: cpu: 100m memory: 512Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -142,6 +151,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/custom.yaml b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/custom.yaml index b116a942356..fc524284fb4 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/custom.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/custom.yaml @@ -275,6 +275,15 @@ spec: memory: 1Gi securityContext: privileged: true + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -331,6 +340,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/default-env.yaml b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/default-env.yaml index 9b374a2f59c..fbeddc169b8 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/default-env.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/default-env.yaml @@ -273,6 +273,15 @@ spec: memory: 1Gi securityContext: privileged: true + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -329,6 +338,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/default.yaml b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/default.yaml index 88c43394664..80cf7c12e75 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/default.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/default.yaml @@ -253,6 +253,15 @@ spec: requests: cpu: 100m memory: 512Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -309,6 +318,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/disable-prometheus.yaml b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/disable-prometheus.yaml index 9db5c211c2d..81dda722f1c 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/disable-prometheus.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/disable-prometheus.yaml @@ -224,6 +224,15 @@ spec: requests: cpu: 100m memory: 512Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -280,6 +289,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/extension-env.yaml b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/extension-env.yaml index d2049910591..314e8bcea7a 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/extension-env.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/extension-env.yaml @@ -277,6 +277,15 @@ spec: memory: 1Gi securityContext: privileged: true + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -333,6 +342,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/override-labels-and-annotations.yaml b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/override-labels-and-annotations.yaml index 53eb74be933..9ce0aa5a085 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/override-labels-and-annotations.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/override-labels-and-annotations.yaml @@ -264,6 +264,15 @@ spec: requests: cpu: 100m memory: 512Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -320,6 +329,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/patch-daemonset.yaml b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/patch-daemonset.yaml index 1c6cadae521..518f239a5d4 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/patch-daemonset.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/patch-daemonset.yaml @@ -253,6 +253,15 @@ spec: requests: cpu: 100m memory: 512Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -309,6 +318,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirstWithHostNet diff --git a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/shutdown-manager.yaml b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/shutdown-manager.yaml index b06d2e8a2ea..506a2824f80 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/shutdown-manager.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/shutdown-manager.yaml @@ -254,6 +254,15 @@ spec: requests: cpu: 100m memory: 512Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -322,6 +331,15 @@ spec: memory: 64Mi securityContext: runAsUser: 1234 + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/volumes.yaml b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/volumes.yaml index 9f1b54d5f14..fc59ec6739b 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/volumes.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/volumes.yaml @@ -277,6 +277,15 @@ spec: memory: 1Gi securityContext: privileged: true + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -333,6 +342,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/with-annotations.yaml b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/with-annotations.yaml index 1124681117b..c19e55794ef 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/with-annotations.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/with-annotations.yaml @@ -258,6 +258,15 @@ spec: requests: cpu: 100m memory: 512Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -314,6 +323,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/with-concurrency.yaml b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/with-concurrency.yaml index ce215d293f1..f941541e76b 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/with-concurrency.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/with-concurrency.yaml @@ -86,6 +86,15 @@ spec: requests: cpu: 100m memory: 512Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -142,6 +151,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/with-extra-args.yaml b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/with-extra-args.yaml index ca663b65dba..1d5c3c4154d 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/with-extra-args.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/with-extra-args.yaml @@ -255,6 +255,15 @@ spec: requests: cpu: 100m memory: 512Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -311,6 +320,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/with-image-pull-secrets.yaml b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/with-image-pull-secrets.yaml index 71ed1d7ebe7..f2353034499 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/with-image-pull-secrets.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/with-image-pull-secrets.yaml @@ -253,6 +253,15 @@ spec: requests: cpu: 100m memory: 512Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -309,6 +318,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/with-name.yaml b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/with-name.yaml index ded596e492c..a7e467f7a6c 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/with-name.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/with-name.yaml @@ -253,6 +253,15 @@ spec: requests: cpu: 100m memory: 512Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -309,6 +318,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/with-node-selector.yaml b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/with-node-selector.yaml index 3ea2db9b4d8..bacf7118001 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/with-node-selector.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/with-node-selector.yaml @@ -253,6 +253,15 @@ spec: requests: cpu: 100m memory: 512Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -309,6 +318,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/with-topology-spread-constraints.yaml b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/with-topology-spread-constraints.yaml index 753466b88de..552012f7728 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/with-topology-spread-constraints.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/daemonsets/with-topology-spread-constraints.yaml @@ -253,6 +253,15 @@ spec: requests: cpu: 100m memory: 512Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -309,6 +318,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/proxy/testdata/deployments/bootstrap.yaml b/internal/infrastructure/kubernetes/proxy/testdata/deployments/bootstrap.yaml index a6f4ac3b9bd..e6574edec1c 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/deployments/bootstrap.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/deployments/bootstrap.yaml @@ -89,6 +89,15 @@ spec: requests: cpu: 100m memory: 512Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -145,6 +154,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/proxy/testdata/deployments/component-level.yaml b/internal/infrastructure/kubernetes/proxy/testdata/deployments/component-level.yaml index 63d26245b04..19cd5ebb2c9 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/deployments/component-level.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/deployments/component-level.yaml @@ -90,6 +90,15 @@ spec: requests: cpu: 100m memory: 512Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -146,6 +155,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/proxy/testdata/deployments/custom.yaml b/internal/infrastructure/kubernetes/proxy/testdata/deployments/custom.yaml index 7b6dbacf6c1..0434fb4cab4 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/deployments/custom.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/deployments/custom.yaml @@ -280,6 +280,15 @@ spec: memory: 1Gi securityContext: privileged: true + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -336,6 +345,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/proxy/testdata/deployments/custom_with_initcontainers.yaml b/internal/infrastructure/kubernetes/proxy/testdata/deployments/custom_with_initcontainers.yaml index fc4f7d4db4a..89c92870887 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/deployments/custom_with_initcontainers.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/deployments/custom_with_initcontainers.yaml @@ -280,6 +280,15 @@ spec: memory: 1Gi securityContext: privileged: true + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -338,6 +347,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/proxy/testdata/deployments/default-env.yaml b/internal/infrastructure/kubernetes/proxy/testdata/deployments/default-env.yaml index ae5de51bab0..c5e2d4ce3c1 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/deployments/default-env.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/deployments/default-env.yaml @@ -278,6 +278,15 @@ spec: memory: 1Gi securityContext: privileged: true + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -334,6 +343,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/proxy/testdata/deployments/default.yaml b/internal/infrastructure/kubernetes/proxy/testdata/deployments/default.yaml index a33d0f742b4..c5c17adaafc 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/deployments/default.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/deployments/default.yaml @@ -257,6 +257,15 @@ spec: requests: cpu: 100m memory: 512Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -313,6 +322,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/proxy/testdata/deployments/disable-prometheus.yaml b/internal/infrastructure/kubernetes/proxy/testdata/deployments/disable-prometheus.yaml index 3323352858a..acaad907b24 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/deployments/disable-prometheus.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/deployments/disable-prometheus.yaml @@ -228,6 +228,15 @@ spec: requests: cpu: 100m memory: 512Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -284,6 +293,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/proxy/testdata/deployments/extension-env.yaml b/internal/infrastructure/kubernetes/proxy/testdata/deployments/extension-env.yaml index 4afb4a82326..4ff157e8cd8 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/deployments/extension-env.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/deployments/extension-env.yaml @@ -282,6 +282,15 @@ spec: memory: 1Gi securityContext: privileged: true + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -338,6 +347,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/proxy/testdata/deployments/override-labels-and-annotations.yaml b/internal/infrastructure/kubernetes/proxy/testdata/deployments/override-labels-and-annotations.yaml index 6139d1f589b..bfaf6686d14 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/deployments/override-labels-and-annotations.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/deployments/override-labels-and-annotations.yaml @@ -268,6 +268,15 @@ spec: requests: cpu: 100m memory: 512Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -324,6 +333,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/proxy/testdata/deployments/patch-deployment.yaml b/internal/infrastructure/kubernetes/proxy/testdata/deployments/patch-deployment.yaml index facb9949246..2c4e97641db 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/deployments/patch-deployment.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/deployments/patch-deployment.yaml @@ -257,6 +257,15 @@ spec: requests: cpu: 100m memory: 512Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -313,6 +322,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirstWithHostNet diff --git a/internal/infrastructure/kubernetes/proxy/testdata/deployments/shutdown-manager.yaml b/internal/infrastructure/kubernetes/proxy/testdata/deployments/shutdown-manager.yaml index a00e887bf6d..c4c7be51fc4 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/deployments/shutdown-manager.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/deployments/shutdown-manager.yaml @@ -258,6 +258,15 @@ spec: requests: cpu: 100m memory: 512Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -326,6 +335,15 @@ spec: memory: 64Mi securityContext: runAsUser: 1234 + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/proxy/testdata/deployments/volumes.yaml b/internal/infrastructure/kubernetes/proxy/testdata/deployments/volumes.yaml index e91cc8f9fbd..ea54e6252d5 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/deployments/volumes.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/deployments/volumes.yaml @@ -282,6 +282,15 @@ spec: memory: 1Gi securityContext: privileged: true + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -338,6 +347,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/proxy/testdata/deployments/with-annotations.yaml b/internal/infrastructure/kubernetes/proxy/testdata/deployments/with-annotations.yaml index 1f9e65555fc..819d66a8f5a 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/deployments/with-annotations.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/deployments/with-annotations.yaml @@ -262,6 +262,15 @@ spec: requests: cpu: 100m memory: 512Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -318,6 +327,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/proxy/testdata/deployments/with-concurrency.yaml b/internal/infrastructure/kubernetes/proxy/testdata/deployments/with-concurrency.yaml index 39f8a856161..cc626d7eb58 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/deployments/with-concurrency.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/deployments/with-concurrency.yaml @@ -90,6 +90,15 @@ spec: requests: cpu: 100m memory: 512Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -146,6 +155,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/proxy/testdata/deployments/with-empty-memory-limits.yaml b/internal/infrastructure/kubernetes/proxy/testdata/deployments/with-empty-memory-limits.yaml index 551753f4960..c20a70d36ff 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/deployments/with-empty-memory-limits.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/deployments/with-empty-memory-limits.yaml @@ -256,6 +256,15 @@ spec: resources: limits: cpu: 400m + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -312,6 +321,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/proxy/testdata/deployments/with-extra-args.yaml b/internal/infrastructure/kubernetes/proxy/testdata/deployments/with-extra-args.yaml index 91e135a7c4d..f5fcb2bd848 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/deployments/with-extra-args.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/deployments/with-extra-args.yaml @@ -259,6 +259,15 @@ spec: requests: cpu: 100m memory: 512Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -315,6 +324,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/proxy/testdata/deployments/with-image-pull-secrets.yaml b/internal/infrastructure/kubernetes/proxy/testdata/deployments/with-image-pull-secrets.yaml index fad1d51870d..ebfa5b5fcd1 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/deployments/with-image-pull-secrets.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/deployments/with-image-pull-secrets.yaml @@ -257,6 +257,15 @@ spec: requests: cpu: 100m memory: 512Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -313,6 +322,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/proxy/testdata/deployments/with-name.yaml b/internal/infrastructure/kubernetes/proxy/testdata/deployments/with-name.yaml index a4ae135150f..1797b157e10 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/deployments/with-name.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/deployments/with-name.yaml @@ -257,6 +257,15 @@ spec: requests: cpu: 100m memory: 512Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -313,6 +322,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/proxy/testdata/deployments/with-node-selector.yaml b/internal/infrastructure/kubernetes/proxy/testdata/deployments/with-node-selector.yaml index 04df115a5e0..13f8d10c719 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/deployments/with-node-selector.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/deployments/with-node-selector.yaml @@ -257,6 +257,15 @@ spec: requests: cpu: 100m memory: 512Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -313,6 +322,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/proxy/testdata/deployments/with-topology-spread-constraints.yaml b/internal/infrastructure/kubernetes/proxy/testdata/deployments/with-topology-spread-constraints.yaml index c33f05fbcf6..7d2fa77e462 100644 --- a/internal/infrastructure/kubernetes/proxy/testdata/deployments/with-topology-spread-constraints.yaml +++ b/internal/infrastructure/kubernetes/proxy/testdata/deployments/with-topology-spread-constraints.yaml @@ -257,6 +257,15 @@ spec: requests: cpu: 100m memory: 512Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /ready + port: 19001 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -313,6 +322,15 @@ spec: requests: cpu: 10m memory: 32Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz + port: 19002 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/internal/infrastructure/kubernetes/ratelimit/resource.go b/internal/infrastructure/kubernetes/ratelimit/resource.go index 6ab5b19433f..9353b0ced00 100644 --- a/internal/infrastructure/kubernetes/ratelimit/resource.go +++ b/internal/infrastructure/kubernetes/ratelimit/resource.go @@ -162,6 +162,19 @@ func expectedRateLimitContainers(rateLimit *egv1a1.RateLimit, rateLimitDeploymen VolumeMounts: expectedContainerVolumeMounts(rateLimit, rateLimitDeployment), TerminationMessagePolicy: corev1.TerminationMessageReadFile, TerminationMessagePath: "/dev/termination-log", + StartupProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: ReadinessPath, + Port: intstr.IntOrString{Type: intstr.Int, IntVal: ReadinessPort}, + Scheme: corev1.URISchemeHTTP, + }, + }, + TimeoutSeconds: 1, + PeriodSeconds: 10, + SuccessThreshold: 1, + FailureThreshold: 30, + }, ReadinessProbe: &corev1.Probe{ ProbeHandler: corev1.ProbeHandler{ HTTPGet: &corev1.HTTPGetAction{ diff --git a/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/custom.yaml b/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/custom.yaml index 4092846d55d..bfd2c28cc6a 100644 --- a/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/custom.yaml +++ b/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/custom.yaml @@ -104,6 +104,15 @@ spec: memory: 1Gi securityContext: privileged: true + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthcheck + port: 8080 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: diff --git a/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/default-env.yaml b/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/default-env.yaml index 4092846d55d..bfd2c28cc6a 100644 --- a/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/default-env.yaml +++ b/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/default-env.yaml @@ -104,6 +104,15 @@ spec: memory: 1Gi securityContext: privileged: true + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthcheck + port: 8080 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: diff --git a/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/default.yaml b/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/default.yaml index 6cd1177643e..17536708a1b 100644 --- a/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/default.yaml +++ b/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/default.yaml @@ -100,6 +100,15 @@ spec: requests: cpu: 100m memory: 512Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthcheck + port: 8080 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: diff --git a/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/disable-prometheus.yaml b/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/disable-prometheus.yaml index 4b58fdd9918..cc20111528c 100644 --- a/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/disable-prometheus.yaml +++ b/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/disable-prometheus.yaml @@ -96,6 +96,15 @@ spec: requests: cpu: 100m memory: 512Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthcheck + port: 8080 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: diff --git a/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/enable-tracing-custom.yaml b/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/enable-tracing-custom.yaml index 2b475eddcfa..fa8916ed89e 100644 --- a/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/enable-tracing-custom.yaml +++ b/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/enable-tracing-custom.yaml @@ -115,6 +115,15 @@ spec: requests: cpu: 100m memory: 512Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthcheck + port: 8080 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: diff --git a/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/enable-tracing.yaml b/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/enable-tracing.yaml index 14082a0ca00..9b364f88752 100644 --- a/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/enable-tracing.yaml +++ b/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/enable-tracing.yaml @@ -115,6 +115,15 @@ spec: requests: cpu: 100m memory: 512Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthcheck + port: 8080 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: diff --git a/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/extension-env.yaml b/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/extension-env.yaml index e13d7f6e25a..95516ce6c46 100644 --- a/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/extension-env.yaml +++ b/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/extension-env.yaml @@ -108,6 +108,15 @@ spec: memory: 1Gi securityContext: privileged: true + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthcheck + port: 8080 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: diff --git a/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/override-env.yaml b/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/override-env.yaml index 4710a8879fb..8a9cfb60c97 100644 --- a/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/override-env.yaml +++ b/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/override-env.yaml @@ -104,6 +104,15 @@ spec: memory: 1Gi securityContext: privileged: true + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthcheck + port: 8080 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: diff --git a/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/patch-deployment.yaml b/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/patch-deployment.yaml index 1e90c86bf4b..7e0f600c016 100644 --- a/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/patch-deployment.yaml +++ b/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/patch-deployment.yaml @@ -100,6 +100,15 @@ spec: requests: cpu: 100m memory: 512Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthcheck + port: 8080 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: diff --git a/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/redis-tls-settings.yaml b/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/redis-tls-settings.yaml index 69cec41350c..9854f37f4e2 100644 --- a/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/redis-tls-settings.yaml +++ b/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/redis-tls-settings.yaml @@ -112,6 +112,15 @@ spec: memory: 1Gi securityContext: privileged: true + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthcheck + port: 8080 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: diff --git a/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/tolerations.yaml b/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/tolerations.yaml index 8c81fdf70b4..56fdb156dcf 100644 --- a/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/tolerations.yaml +++ b/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/tolerations.yaml @@ -112,6 +112,15 @@ spec: memory: 1Gi securityContext: privileged: true + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthcheck + port: 8080 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: diff --git a/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/volumes.yaml b/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/volumes.yaml index aca38e6cc97..01e779e9e49 100644 --- a/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/volumes.yaml +++ b/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/volumes.yaml @@ -112,6 +112,15 @@ spec: memory: 1Gi securityContext: privileged: true + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthcheck + port: 8080 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: diff --git a/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/with-node-selector.yaml b/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/with-node-selector.yaml index 6f16d6b37ad..e6105d59114 100644 --- a/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/with-node-selector.yaml +++ b/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/with-node-selector.yaml @@ -100,6 +100,15 @@ spec: requests: cpu: 100m memory: 512Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthcheck + port: 8080 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: diff --git a/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/with-topology-spread-constraints.yaml b/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/with-topology-spread-constraints.yaml index 2b708ef778e..d5c9a6d7460 100644 --- a/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/with-topology-spread-constraints.yaml +++ b/internal/infrastructure/kubernetes/ratelimit/testdata/deployments/with-topology-spread-constraints.yaml @@ -100,6 +100,15 @@ spec: requests: cpu: 100m memory: 512Mi + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthcheck + port: 8080 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: diff --git a/internal/ir/xds.go b/internal/ir/xds.go index 8dc95e783cf..3d300f1a539 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -553,15 +553,12 @@ type HTTPRoute struct { URLRewrite *URLRewrite `json:"urlRewrite,omitempty" yaml:"urlRewrite,omitempty"` // ExtensionRefs holds unstructured resources that were introduced by an extension and used on the HTTPRoute as extensionRef filters ExtensionRefs []*UnstructuredRef `json:"extensionRefs,omitempty" yaml:"extensionRefs,omitempty"` - // External Processing extensions - ExtProcs []ExtProc `json:"extProc,omitempty" yaml:"extProc,omitempty"` - // Wasm extensions - Wasms []Wasm `json:"wasm,omitempty" yaml:"wasm,omitempty"` - // Traffic holds the features associated with BackendTrafficPolicy Traffic *TrafficFeatures `json:"traffic,omitempty" yaml:"traffic,omitempty"` // Security holds the features associated with SecurityPolicy Security *SecurityFeatures `json:"security,omitempty" yaml:"security,omitempty"` + // EnvoyExtension holds the features associated with EnvoyExtensionPolicy + EnvoyExtensions *EnvoyExtensionFeatures `json:"envoyExtensions,omitempty" yaml:"envoyExtensions,omitempty"` // UseClientProtocol enables using the same protocol upstream that was used downstream UseClientProtocol *bool `json:"useClientProtocol,omitempty" yaml:"useClientProtocol,omitempty"` // Metadata is used to enrich envoy route metadata with user and provider-specific information @@ -652,6 +649,15 @@ func (s *SecurityFeatures) Validate() error { return errs } +// EnvoyExtensionFeatures holds the information associated with the Envoy Extension Policy. +// +k8s:deepcopy-gen=true +type EnvoyExtensionFeatures struct { + // External Processing extensions + ExtProcs []ExtProc `json:"extProcs,omitempty" yaml:"extProcs,omitempty"` + // Wasm extensions + Wasms []Wasm `json:"wasms,omitempty" yaml:"wasms,omitempty"` +} + // UnstructuredRef holds unstructured data for an arbitrary k8s resource introduced by an extension // Envoy Gateway does not need to know about the resource types in order to store and pass the data for these objects // to an extension. @@ -1190,9 +1196,6 @@ func (h AddHeader) Validate() error { // DirectResponse holds the details for returning a body and status code for a route. // +k8s:deepcopy-gen=true type DirectResponse struct { - // Body configures the body of the direct response. Currently only a string response - // is supported, but in the future a config.core.v3.DataSource may replace it. - Body *string `json:"body,omitempty" yaml:"body,omitempty"` // StatusCode will be used for the direct response's status code. StatusCode uint32 `json:"statusCode" yaml:"statusCode"` } @@ -1603,7 +1606,6 @@ type RateLimitValue struct { // AccessLog holds the access logging configuration. // +k8s:deepcopy-gen=true type AccessLog struct { - CELMatches []string `json:"celMatches,omitempty" yaml:"celMatches,omitempty"` Text []*TextAccessLog `json:"text,omitempty" yaml:"text,omitempty"` JSON []*JSONAccessLog `json:"json,omitempty" yaml:"json,omitempty"` ALS []*ALSAccessLog `json:"als,omitempty" yaml:"als,omitempty"` @@ -1613,20 +1615,23 @@ type AccessLog struct { // TextAccessLog holds the configuration for text access logging. // +k8s:deepcopy-gen=true type TextAccessLog struct { - Format *string `json:"format,omitempty" yaml:"format,omitempty"` - Path string `json:"path" yaml:"path"` + CELMatches []string `json:"celMatches,omitempty" yaml:"celMatches,omitempty"` + Format *string `json:"format,omitempty" yaml:"format,omitempty"` + Path string `json:"path" yaml:"path"` } // JSONAccessLog holds the configuration for JSON access logging. // +k8s:deepcopy-gen=true type JSONAccessLog struct { - JSON map[string]string `json:"json,omitempty" yaml:"json,omitempty"` - Path string `json:"path" yaml:"path"` + CELMatches []string `json:"celMatches,omitempty" yaml:"celMatches,omitempty"` + JSON map[string]string `json:"json,omitempty" yaml:"json,omitempty"` + Path string `json:"path" yaml:"path"` } // ALSAccessLog holds the configuration for gRPC ALS access logging. // +k8s:deepcopy-gen=true type ALSAccessLog struct { + CELMatches []string `json:"celMatches,omitempty" yaml:"celMatches,omitempty"` LogName string `json:"name" yaml:"name"` Destination RouteDestination `json:"destination,omitempty" yaml:"destination,omitempty"` Type egv1a1.ALSEnvoyProxyAccessLogType `json:"type" yaml:"type"` @@ -1646,6 +1651,7 @@ type ALSAccessLogHTTP struct { // OpenTelemetryAccessLog holds the configuration for OpenTelemetry access logging. // +k8s:deepcopy-gen=true type OpenTelemetryAccessLog struct { + CELMatches []string `json:"celMatches,omitempty" yaml:"celMatches,omitempty"` Authority string `json:"authority,omitempty" yaml:"authority,omitempty"` Text *string `json:"text,omitempty" yaml:"text,omitempty"` Attributes map[string]string `json:"attributes,omitempty" yaml:"attributes,omitempty"` diff --git a/internal/ir/xds_test.go b/internal/ir/xds_test.go index aa16a614a8c..9492c378344 100644 --- a/internal/ir/xds_test.go +++ b/internal/ir/xds_test.go @@ -254,7 +254,6 @@ var ( Exact: ptr.To("filter-error"), }, DirectResponse: &DirectResponse{ - Body: ptr.To("invalid filter type"), StatusCode: uint32(500), }, } @@ -297,7 +296,6 @@ var ( Exact: ptr.To("redirect"), }, DirectResponse: &DirectResponse{ - Body: ptr.To("invalid filter type"), StatusCode: uint32(799), }, } diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index d76ea363c28..273eeb1c3ca 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -19,6 +19,11 @@ import ( // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ALSAccessLog) DeepCopyInto(out *ALSAccessLog) { *out = *in + if in.CELMatches != nil { + in, out := &in.CELMatches, &out.CELMatches + *out = make([]string, len(*in)) + copy(*out, *in) + } in.Destination.DeepCopyInto(&out.Destination) if in.Text != nil { in, out := &in.Text, &out.Text @@ -82,11 +87,6 @@ func (in *ALSAccessLogHTTP) DeepCopy() *ALSAccessLogHTTP { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AccessLog) DeepCopyInto(out *AccessLog) { *out = *in - if in.CELMatches != nil { - in, out := &in.CELMatches, &out.CELMatches - *out = make([]string, len(*in)) - copy(*out, *in) - } if in.Text != nil { in, out := &in.Text, &out.Text *out = make([]*TextAccessLog, len(*in)) @@ -666,11 +666,6 @@ func (in *DestinationSetting) DeepCopy() *DestinationSetting { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DirectResponse) DeepCopyInto(out *DirectResponse) { *out = *in - if in.Body != nil { - in, out := &in.Body, &out.Body - *out = new(string) - **out = **in - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DirectResponse. @@ -683,6 +678,35 @@ func (in *DirectResponse) DeepCopy() *DirectResponse { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EnvoyExtensionFeatures) DeepCopyInto(out *EnvoyExtensionFeatures) { + *out = *in + if in.ExtProcs != nil { + in, out := &in.ExtProcs, &out.ExtProcs + *out = make([]ExtProc, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Wasms != nil { + in, out := &in.Wasms, &out.Wasms + *out = make([]Wasm, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvoyExtensionFeatures. +func (in *EnvoyExtensionFeatures) DeepCopy() *EnvoyExtensionFeatures { + if in == nil { + return nil + } + out := new(EnvoyExtensionFeatures) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EnvoyPatchPolicy) DeepCopyInto(out *EnvoyPatchPolicy) { *out = *in @@ -1230,7 +1254,7 @@ func (in *HTTPRoute) DeepCopyInto(out *HTTPRoute) { if in.DirectResponse != nil { in, out := &in.DirectResponse, &out.DirectResponse *out = new(DirectResponse) - (*in).DeepCopyInto(*out) + **out = **in } if in.Redirect != nil { in, out := &in.Redirect, &out.Redirect @@ -1269,20 +1293,6 @@ func (in *HTTPRoute) DeepCopyInto(out *HTTPRoute) { } } } - if in.ExtProcs != nil { - in, out := &in.ExtProcs, &out.ExtProcs - *out = make([]ExtProc, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.Wasms != nil { - in, out := &in.Wasms, &out.Wasms - *out = make([]Wasm, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } if in.Traffic != nil { in, out := &in.Traffic, &out.Traffic *out = new(TrafficFeatures) @@ -1293,6 +1303,11 @@ func (in *HTTPRoute) DeepCopyInto(out *HTTPRoute) { *out = new(SecurityFeatures) (*in).DeepCopyInto(*out) } + if in.EnvoyExtensions != nil { + in, out := &in.EnvoyExtensions, &out.EnvoyExtensions + *out = new(EnvoyExtensionFeatures) + (*in).DeepCopyInto(*out) + } if in.UseClientProtocol != nil { in, out := &in.UseClientProtocol, &out.UseClientProtocol *out = new(bool) @@ -1497,6 +1512,11 @@ func (in *InfraMetadata) DeepCopy() *InfraMetadata { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JSONAccessLog) DeepCopyInto(out *JSONAccessLog) { *out = *in + if in.CELMatches != nil { + in, out := &in.CELMatches, &out.CELMatches + *out = make([]string, len(*in)) + copy(*out, *in) + } if in.JSON != nil { in, out := &in.JSON, &out.JSON *out = make(map[string]string, len(*in)) @@ -1745,6 +1765,11 @@ func (in *OIDC) DeepCopy() *OIDC { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OpenTelemetryAccessLog) DeepCopyInto(out *OpenTelemetryAccessLog) { *out = *in + if in.CELMatches != nil { + in, out := &in.CELMatches, &out.CELMatches + *out = make([]string, len(*in)) + copy(*out, *in) + } if in.Text != nil { in, out := &in.Text, &out.Text *out = new(string) @@ -2709,6 +2734,11 @@ func (in *TLSUpstreamConfig) DeepCopy() *TLSUpstreamConfig { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TextAccessLog) DeepCopyInto(out *TextAccessLog) { *out = *in + if in.CELMatches != nil { + in, out := &in.CELMatches, &out.CELMatches + *out = make([]string, len(*in)) + copy(*out, *in) + } if in.Format != nil { in, out := &in.Format, &out.Format *out = new(string) diff --git a/internal/kubernetes/client.go b/internal/kubernetes/client.go index 317ed852c37..5c58242bab6 100644 --- a/internal/kubernetes/client.go +++ b/internal/kubernetes/client.go @@ -51,19 +51,24 @@ type client struct { } func NewCLIClient(clientConfig clientcmd.ClientConfig) (CLIClient, error) { - return newClientInternal(clientConfig) + cfg, err := clientConfig.ClientConfig() + if err != nil { + return nil, err + } + return newClientInternal(cfg) +} + +func NewForRestConfig(cfg *rest.Config) (CLIClient, error) { + return newClientInternal(cfg) } -func newClientInternal(clientConfig clientcmd.ClientConfig) (*client, error) { +func newClientInternal(restCfg *rest.Config) (*client, error) { var ( c client err error ) - c.config, err = clientConfig.ClientConfig() - if err != nil { - return nil, err - } + c.config = restCfg setRestDefaults(c.config) c.restClient, err = rest.RESTClientFor(c.config) diff --git a/internal/provider/kubernetes/predicates.go b/internal/provider/kubernetes/predicates.go index 0e0f984c69c..e64c08619db 100644 --- a/internal/provider/kubernetes/predicates.go +++ b/internal/provider/kubernetes/predicates.go @@ -312,6 +312,10 @@ func (r *gatewayAPIReconciler) validateBackendForReconcile(obj client.Object) bo return true } + if r.isEnvoyProxyReferencingBackend(&nsName) { + return true + } + return r.isEnvoyExtensionPolicyReferencingBackend(&nsName) } @@ -428,6 +432,10 @@ func (r *gatewayAPIReconciler) validateEndpointSliceForReconcile(obj client.Obje return true } + if r.isEnvoyProxyReferencingBackend(&nsName) { + return true + } + return r.isEnvoyExtensionPolicyReferencingBackend(&nsName) } diff --git a/internal/troubleshoot/collect.go b/internal/troubleshoot/collect.go new file mode 100644 index 00000000000..a419a564f5b --- /dev/null +++ b/internal/troubleshoot/collect.go @@ -0,0 +1,92 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package troubleshoot + +import ( + "context" + "fmt" + + troubleshootv1b2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + tbcollect "github.com/replicatedhq/troubleshoot/pkg/collect" + "k8s.io/client-go/rest" + + "github.com/envoyproxy/gateway/internal/troubleshoot/collect" +) + +func CollectResult(ctx context.Context, restConfig *rest.Config, bundlePath string, egNamespace string) tbcollect.CollectorResult { + var result tbcollect.CollectorResult + + progressChan := make(chan interface{}) + go func() { + select { + case <-ctx.Done(): + close(progressChan) + case msg := <-progressChan: + fmt.Printf("Collecting support bundle: %v\n", msg) + } + }() + + collectors := []tbcollect.Collector{ + // Collect the custom resources from Gateway API and EG + collect.CustomResource{ + ClientConfig: restConfig, + BundlePath: bundlePath, + IncludeGroups: []string{ + "gateway.envoyproxy.io", + "gateway.networking.k8s.io", + }, + }, + // Collect resources from EnvoyGateway system namespace + collect.EnvoyGatewayResource{ + ClientConfig: restConfig, + BundlePath: bundlePath, + Namespace: egNamespace, + }, + // Collect logs from EnvoyGateway system namespace + &tbcollect.CollectLogs{ + Collector: &troubleshootv1b2.Logs{ + Name: "pod-logs", + Namespace: egNamespace, + }, + ClientConfig: restConfig, + BundlePath: bundlePath, + Context: ctx, + }, + // Collect prometheus metrics from EnvoyGateway system namespace + collect.PrometheusMetric{ + BundlePath: bundlePath, + ClientConfig: restConfig, + Namespace: egNamespace, + }, + // Collect config dump from EnvoyGateway system namespace + collect.ConfigDump{ + BundlePath: bundlePath, + ClientConfig: restConfig, + Namespace: egNamespace, + }, + } + total := len(collectors) + allCollectedData := make(map[string][]byte) + for i, collector := range collectors { + res, err := collector.Collect(progressChan) + if err != nil { + progressChan <- fmt.Errorf("failed to run collector: %s: %w", collector.Title(), err) + progressChan <- tbcollect.CollectProgress{ + CurrentName: collector.Title(), + CurrentStatus: "failed", + CompletedCount: i + 1, + TotalCount: total, + } + continue + } + for k, v := range res { + allCollectedData[k] = v + } + } + result = allCollectedData + + return result +} diff --git a/internal/troubleshoot/collect/config_dump.go b/internal/troubleshoot/collect/config_dump.go new file mode 100644 index 00000000000..fe9ff9558ad --- /dev/null +++ b/internal/troubleshoot/collect/config_dump.go @@ -0,0 +1,100 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package collect + +import ( + "bytes" + "context" + "fmt" + "path" + + troubleshootv1b2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + tbcollect "github.com/replicatedhq/troubleshoot/pkg/collect" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + + kube "github.com/envoyproxy/gateway/internal/kubernetes" +) + +var _ tbcollect.Collector = &ConfigDump{} + +// ConfigDump defines a collector that dumps the envoy configuration of the proxy pod. +type ConfigDump struct { + BundlePath string + Namespace string + ClientConfig *rest.Config +} + +func (cd ConfigDump) Title() string { + return "config-dump" +} + +func (cd ConfigDump) IsExcluded() (bool, error) { + return false, nil +} + +func (cd ConfigDump) GetRBACErrors() []error { + return nil +} + +func (cd ConfigDump) HasRBACErrors() bool { + return false +} + +func (cd ConfigDump) CheckRBAC(_ context.Context, _ tbcollect.Collector, _ *troubleshootv1b2.Collect, _ *rest.Config, _ string) error { + return nil +} + +func (cd ConfigDump) Collect(_ chan<- interface{}) (tbcollect.CollectorResult, error) { + client, err := kubernetes.NewForConfig(cd.ClientConfig) + if err != nil { + return nil, err + } + + pods, err := listPods(context.TODO(), client, cd.Namespace, labels.SelectorFromSet(map[string]string{ + "app.kubernetes.io/component": "proxy", + "app.kubernetes.io/managed-by": "envoy-gateway", + "app.kubernetes.io/name": "envoy", + })) + if err != nil { + return nil, err + } + + output := tbcollect.NewResult() + + cliClient, err := kube.NewForRestConfig(cd.ClientConfig) + if err != nil { + return output, err + } + + logs := make([]string, 0, len(pods)) + for _, pod := range pods { + nn := types.NamespacedName{Namespace: pod.Namespace, Name: pod.Name} + data, err := configDump(cliClient, nn, true) + if err != nil { + logs = append(logs, fmt.Sprintf("failed to get config dump for pod %s/%s: %v", pod.Namespace, pod.Name, err)) + continue + } + + k := fmt.Sprintf("%s-%s.json", pod.Namespace, pod.Name) + _ = output.SaveResult(cd.BundlePath, path.Join("config-dumps", k), bytes.NewBuffer(data)) + } + if len(logs) > 0 { + _ = output.SaveResult(cd.BundlePath, path.Join("config-dumps", "errors.log"), marshalErrors(logs)) + } + + return output, nil +} + +func configDump(cli kube.CLIClient, nn types.NamespacedName, includeEds bool) ([]byte, error) { + reqPath := "/config_dump" + if includeEds { + reqPath = fmt.Sprintf("%s?include_eds", reqPath) + } + return requestWithPortForwarder(cli, nn, 19000, reqPath) +} diff --git a/internal/troubleshoot/collect/custom_resource.go b/internal/troubleshoot/collect/custom_resource.go new file mode 100644 index 00000000000..c74daa7cf5b --- /dev/null +++ b/internal/troubleshoot/collect/custom_resource.go @@ -0,0 +1,93 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package collect + +import ( + "bytes" + "context" + "fmt" + "path" + + troubleshootv1b2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + tbcollect "github.com/replicatedhq/troubleshoot/pkg/collect" + "github.com/replicatedhq/troubleshoot/pkg/constants" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" +) + +var _ tbcollect.Collector = &CustomResource{} + +// CustomResource defines a custom resource collector, which collect custom resources from the cluster, +// with the given IncludeGroups and Namespaces. +type CustomResource struct { + BundlePath string + Namespace string + ClientConfig *rest.Config + Namespaces []string + IncludeGroups []string +} + +func (cr CustomResource) Title() string { + return "custom-resource" +} + +func (cr CustomResource) IsExcluded() (bool, error) { + return false, nil +} + +func (cr CustomResource) GetRBACErrors() []error { + return nil +} + +func (cr CustomResource) HasRBACErrors() bool { + return false +} + +func (cr CustomResource) CheckRBAC(_ context.Context, _ tbcollect.Collector, _ *troubleshootv1b2.Collect, _ *rest.Config, _ string) error { + return nil +} + +func (cr CustomResource) Collect(_ chan<- interface{}) (tbcollect.CollectorResult, error) { + ctx := context.Background() + output := tbcollect.NewResult() + client, err := kubernetes.NewForConfig(cr.ClientConfig) + if err != nil { + return nil, err + } + + dynamicClient, err := dynamic.NewForConfig(cr.ClientConfig) + if err != nil { + return nil, err + } + + // namespaces + var namespaceNames []string + switch { + case len(cr.Namespaces) > 0: + namespaceNames = cr.Namespaces + case cr.Namespace != "": + namespaceNames = append(namespaceNames, cr.Namespace) + default: + _, namespaceList, _ := getAllNamespaces(ctx, client) + if namespaceList != nil { + for _, namespace := range namespaceList.Items { + namespaceNames = append(namespaceNames, namespace.Name) + } + } + } + + // crs + customResources, crErrors := crs(ctx, dynamicClient, client, cr.ClientConfig, namespaceNames, cr.IncludeGroups) + for k, v := range customResources { + p := path.Join(constants.CLUSTER_RESOURCES_DIR, k) + _ = output.SaveResult(cr.BundlePath, p, bytes.NewBuffer(v)) + } + errPath := path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_CUSTOM_RESOURCES)) + _ = output.SaveResult(cr.BundlePath, errPath, marshalErrors(crErrors)) + + return output, nil +} diff --git a/internal/troubleshoot/collect/envoy_gateway_resource.go b/internal/troubleshoot/collect/envoy_gateway_resource.go new file mode 100644 index 00000000000..54cf4ff75a8 --- /dev/null +++ b/internal/troubleshoot/collect/envoy_gateway_resource.go @@ -0,0 +1,103 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package collect + +import ( + "bytes" + "context" + "fmt" + "path" + + troubleshootv1b2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + tbcollect "github.com/replicatedhq/troubleshoot/pkg/collect" + "github.com/replicatedhq/troubleshoot/pkg/constants" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" +) + +var _ tbcollect.Collector = &EnvoyGatewayResource{} + +// EnvoyGatewayResource defines a collector for the Envoy Gateway resource from the given namespace. +// This is most like the CusterResource collector, but remove unnecessary types like StatefulSet, CornJob etc. +type EnvoyGatewayResource struct { + BundlePath string + Namespace string + ClientConfig *rest.Config +} + +func (eg EnvoyGatewayResource) Title() string { + return "envoy-gateway-resource" +} + +func (eg EnvoyGatewayResource) IsExcluded() (bool, error) { + return false, nil +} + +func (eg EnvoyGatewayResource) GetRBACErrors() []error { + return nil +} + +func (eg EnvoyGatewayResource) HasRBACErrors() bool { + return false +} + +func (eg EnvoyGatewayResource) CheckRBAC(_ context.Context, _ tbcollect.Collector, _ *troubleshootv1b2.Collect, _ *rest.Config, _ string) error { + return nil +} + +func (eg EnvoyGatewayResource) Collect(_ chan<- interface{}) (tbcollect.CollectorResult, error) { + ctx := context.Background() + output := tbcollect.NewResult() + client, err := kubernetes.NewForConfig(eg.ClientConfig) + if err != nil { + return nil, err + } + namespaceNames := []string{eg.Namespace} + + // pods + pods, podErrors, _ := pods(ctx, client, namespaceNames) + for k, v := range pods { + _ = output.SaveResult(eg.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_PODS, k), bytes.NewBuffer(v)) + } + _ = output.SaveResult(eg.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_PODS)), marshalErrors(podErrors)) + + // services + services, servicesErrors := services(ctx, client, namespaceNames) + for k, v := range services { + _ = output.SaveResult(eg.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_SERVICES, k), bytes.NewBuffer(v)) + } + _ = output.SaveResult(eg.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_SERVICES)), marshalErrors(servicesErrors)) + + // deployments + deployments, deploymentsErrors := deployments(ctx, client, namespaceNames) + for k, v := range deployments { + _ = output.SaveResult(eg.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_DEPLOYMENTS, k), bytes.NewBuffer(v)) + } + _ = output.SaveResult(eg.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_DEPLOYMENTS)), marshalErrors(deploymentsErrors)) + + // daemonsets + daemonsets, daemonsetsErrors := daemonsets(ctx, client, namespaceNames) + for k, v := range daemonsets { + _ = output.SaveResult(eg.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_DAEMONSETS, k), bytes.NewBuffer(v)) + } + _ = output.SaveResult(eg.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_DAEMONSETS)), marshalErrors(daemonsetsErrors)) + + // jobs + jobs, jobsErrors := jobs(ctx, client, namespaceNames) + for k, v := range jobs { + _ = output.SaveResult(eg.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_JOBS, k), bytes.NewBuffer(v)) + } + _ = output.SaveResult(eg.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_JOBS)), marshalErrors(jobsErrors)) + + // ConfigMaps + configMaps, configMapsErrors := configMaps(ctx, client, namespaceNames) + for k, v := range configMaps { + _ = output.SaveResult(eg.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_CONFIGMAPS, k), bytes.NewBuffer(v)) + } + _ = output.SaveResult(eg.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_CONFIGMAPS)), marshalErrors(configMapsErrors)) + + return output, nil +} diff --git a/internal/troubleshoot/collect/prometheus_metrics.go b/internal/troubleshoot/collect/prometheus_metrics.go new file mode 100644 index 00000000000..785b99719af --- /dev/null +++ b/internal/troubleshoot/collect/prometheus_metrics.go @@ -0,0 +1,154 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package collect + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "path" + "strconv" + "strings" + + troubleshootv1b2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + tbcollect "github.com/replicatedhq/troubleshoot/pkg/collect" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + + kube "github.com/envoyproxy/gateway/internal/kubernetes" +) + +var _ tbcollect.Collector = &PrometheusMetric{} + +// PrometheusMetric defines a collector scraping Prometheus metrics from the selected pods. +type PrometheusMetric struct { + BundlePath string + Namespace string + ClientConfig *rest.Config +} + +func (p PrometheusMetric) Title() string { + return "prometheus-metric" +} + +func (p PrometheusMetric) IsExcluded() (bool, error) { + return false, nil +} + +func (p PrometheusMetric) GetRBACErrors() []error { + return nil +} + +func (p PrometheusMetric) HasRBACErrors() bool { + return false +} + +func (p PrometheusMetric) CheckRBAC(_ context.Context, _ tbcollect.Collector, _ *troubleshootv1b2.Collect, _ *rest.Config, _ string) error { + return nil +} + +func (p PrometheusMetric) Collect(_ chan<- interface{}) (tbcollect.CollectorResult, error) { + client, err := kubernetes.NewForConfig(p.ClientConfig) + if err != nil { + return nil, err + } + + pods, err := listPods(context.TODO(), client, p.Namespace, labels.Everything()) + if err != nil { + return nil, err + } + + output := tbcollect.NewResult() + + cliClient, err := kube.NewForRestConfig(p.ClientConfig) + if err != nil { + return output, err + } + + logs := make([]string, 0) + for _, pod := range pods { + annos := pod.GetAnnotations() + if v, ok := annos["prometheus.io/scrape"]; !ok || v != "true" { + logs = append(logs, fmt.Sprintf("pod %s/%s is skipped because of missing annotation prometheus.io/scrape", pod.Namespace, pod.Name)) + continue + } + + nn, port, reqPath := types.NamespacedName{Namespace: pod.Namespace, Name: pod.Name}, 19001, "/metrics" + if v, ok := annos["prometheus.io/port"]; !ok { + port, err = strconv.Atoi(v) + if err != nil { + logs = append(logs, fmt.Sprintf("pod %s/%s is skipped because of invalid prometheus.io/port", pod.Namespace, pod.Name)) + continue + } + } + if v, ok := annos["prometheus.io/path"]; ok { + reqPath = v + } + + data, err := requestWithPortForwarder(cliClient, nn, port, reqPath) + if err != nil { + logs = append(logs, fmt.Sprintf("pod %s/%s is skipped because of err: %v", pod.Namespace, pod.Name, err)) + continue + } + + k := fmt.Sprintf("%s-%s.prom", pod.Namespace, pod.Name) + _ = output.SaveResult(p.BundlePath, path.Join("prometheus-metrics", k), bytes.NewBuffer(data)) + } + if len(logs) > 0 { + _ = output.SaveResult(p.BundlePath, path.Join("prometheus-metrics", "error.log"), bytes.NewBuffer([]byte(strings.Join(logs, "\n")))) + } + + return output, nil +} + +func listPods(ctx context.Context, client kubernetes.Interface, namespace string, selector labels.Selector) ([]corev1.Pod, error) { + pods, err := client.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{ + LabelSelector: selector.String(), + }) + if err != nil { + return nil, err + } + + return pods.Items, nil +} + +func requestWithPortForwarder(cli kube.CLIClient, nn types.NamespacedName, port int, reqPath string) ([]byte, error) { + fw, err := kube.NewLocalPortForwarder(cli, nn, 0, port) + if err != nil { + return nil, err + } + + if err := fw.Start(); err != nil { + return nil, err + } + defer fw.Stop() + + if !strings.HasPrefix(reqPath, "/") { + reqPath = "/" + reqPath + } + + url := fmt.Sprintf("http://%s%s", fw.Address(), reqPath) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer func() { + _ = resp.Body.Close() + }() + + return io.ReadAll(resp.Body) +} diff --git a/internal/troubleshoot/collect/troubleshoot_helper.go b/internal/troubleshoot/collect/troubleshoot_helper.go new file mode 100644 index 00000000000..21c7baff49a --- /dev/null +++ b/internal/troubleshoot/collect/troubleshoot_helper.go @@ -0,0 +1,611 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +// Functions here are borrowed from the troubleshoot project because they're not exported. +// There's some differences in the implementation, but the core logic is the same. +// 1. Remove `managedFields` from the objects +// 2. Remove json output + +package collect + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "reflect" + "sort" + "strings" + + "github.com/replicatedhq/troubleshoot/pkg/k8sutil" + "github.com/replicatedhq/troubleshoot/pkg/k8sutil/discovery" + corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextensionsv1b1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + apiextv1clientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" + apiextv1b1clientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/version" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" + "sigs.k8s.io/yaml" +) + +// Use for error maps and arrays. These are guaranteed to not result in an error when marshaling. +func marshalErrors(errors interface{}) io.Reader { + if errors == nil { + return nil + } + + val := reflect.ValueOf(errors) + switch val.Kind() { + case reflect.Array, reflect.Slice, reflect.Map: + if val.Len() == 0 { + return nil + } + default: + // do nothing + } + + m, _ := json.MarshalIndent(errors, "", " ") + return bytes.NewBuffer(m) +} + +func getAllNamespaces(ctx context.Context, client *kubernetes.Clientset) ([]byte, *corev1.NamespaceList, []string) { + namespaces, err := client.CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, nil, []string{err.Error()} + } + + gvk, err := apiutil.GVKForObject(namespaces, scheme.Scheme) + if err == nil { + namespaces.GetObjectKind().SetGroupVersionKind(gvk) + } + + for i, o := range namespaces.Items { + gvk, err := apiutil.GVKForObject(&o, scheme.Scheme) + if err == nil { + namespaces.Items[i].GetObjectKind().SetGroupVersionKind(gvk) + } + } + + b, err := yaml.Marshal(namespaces) + if err != nil { + return nil, nil, []string{err.Error()} + } + + return b, namespaces, nil +} + +func crs(ctx context.Context, dyn dynamic.Interface, client *kubernetes.Clientset, config *rest.Config, namespaces []string, includeGroups []string) (map[string][]byte, map[string]string) { + includeGroupSet := sets.New[string](includeGroups...) + errorList := make(map[string]string) + ok, err := discovery.HasResource(client, "apiextensions.k8s.io/v1", "CustomResourceDefinition") + if err != nil { + return nil, map[string]string{"discover apiextensions.k8s.io/v1": err.Error()} + } + if ok { + crdClient, err := apiextv1clientset.NewForConfig(config) + if err != nil { + errorList["crdClient"] = err.Error() + return map[string][]byte{}, errorList + } + return crsV1(ctx, dyn, crdClient, namespaces, includeGroupSet) + } + + crdClient, err := apiextv1b1clientset.NewForConfig(config) + if err != nil { + errorList["crdClient"] = err.Error() + return map[string][]byte{}, errorList + } + return crsV1beta(ctx, dyn, crdClient, namespaces, includeGroupSet) +} + +func crsV1(ctx context.Context, client dynamic.Interface, crdClient apiextv1clientset.ApiextensionsV1Interface, + namespaces []string, includeGroups sets.Set[string], +) (map[string][]byte, map[string]string) { + customResources := make(map[string][]byte) + errorList := make(map[string]string) + + crds, err := crdClient.CustomResourceDefinitions().List(ctx, metav1.ListOptions{}) + if err != nil { + errorList["crdList"] = err.Error() + return customResources, errorList + } + + metaAccessor := meta.NewAccessor() + + // Loop through CRDs to fetch the CRs + for _, crd := range crds.Items { + // A resource that contains '/' is a subresource type, and it has no + // object instances + if strings.ContainsAny(crd.Name, "/") { + continue + } + + var version string + if len(crd.Spec.Versions) > 0 { + versions := []string{} + for _, v := range crd.Spec.Versions { + versions = append(versions, v.Name) + } + + version = versions[0] + if len(versions) > 1 { + version = selectCRDVersionByPriority(versions) + } + } + + if len(includeGroups) > 0 && + !includeGroups.Has(crd.Spec.Group) { + continue + } + + gvr := schema.GroupVersionResource{ + Group: crd.Spec.Group, + Version: version, + Resource: crd.Spec.Names.Plural, + } + isNamespacedResource := crd.Spec.Scope == apiextensionsv1.NamespaceScoped + + // Fetch all resources of given type + customResourceList, err := client.Resource(gvr).List(ctx, metav1.ListOptions{}) + if err != nil { + errorList[crd.Name] = err.Error() + continue + } + + if len(customResourceList.Items) == 0 { + continue + } + + if !isNamespacedResource { + objects := []map[string]interface{}{} + for _, item := range customResourceList.Items { + objects = append(objects, item.Object) + } + err := storeCustomResource(crd.Name, objects, customResources) + if err != nil { + errorList[crd.Name] = err.Error() + continue + } + } else { + // Group fetched resources by the namespace + perNamespace := map[string][]map[string]interface{}{} + errors := []string{} + + for _, item := range customResourceList.Items { + omitManagedFields(&item) + ns, err := metaAccessor.Namespace(&item) + if err != nil { + errors = append(errors, err.Error()) + continue + } + if perNamespace[ns] == nil { + perNamespace[ns] = []map[string]interface{}{} + } + perNamespace[ns] = append(perNamespace[ns], item.Object) + } + + if len(errors) > 0 { + errorList[crd.Name] = strings.Join(errors, "\n") + } + + // Only include resources from requested namespaces + for _, ns := range namespaces { + if len(perNamespace[ns]) == 0 { + continue + } + + namespacedName := fmt.Sprintf("%s/%s", crd.Name, ns) + err := storeCustomResource(namespacedName, perNamespace[ns], customResources) + if err != nil { + errorList[namespacedName] = err.Error() + continue + } + } + } + } + + return customResources, errorList +} + +func omitManagedFields(o runtime.Object) runtime.Object { + a, err := meta.Accessor(o) + if err != nil { + // The object is not a `metav1.Object`, ignore it. + return o + } + a.SetManagedFields(nil) + return o +} + +func crsV1beta(ctx context.Context, client dynamic.Interface, crdClient apiextv1b1clientset.ApiextensionsV1beta1Interface, + namespaces []string, includeGroups sets.Set[string], +) (map[string][]byte, map[string]string) { + customResources := make(map[string][]byte) + errorList := make(map[string]string) + + crds, err := crdClient.CustomResourceDefinitions().List(ctx, metav1.ListOptions{}) + if err != nil { + errorList["crdList"] = err.Error() + return customResources, errorList + } + + metaAccessor := meta.NewAccessor() + + // Loop through CRDs to fetch the CRs + for _, crd := range crds.Items { + // A resource that contains '/' is a subresource type, and it has no + // object instances + if strings.ContainsAny(crd.Name, "/") { + continue + } + + if len(includeGroups) > 0 && + includeGroups.Has(crd.Spec.Group) { + continue + } + + gvr := schema.GroupVersionResource{ + Group: crd.Spec.Group, + Version: crd.Spec.Version, + Resource: crd.Spec.Names.Plural, + } + + if len(crd.Spec.Versions) > 0 { + versions := []string{} + for _, v := range crd.Spec.Versions { + versions = append(versions, v.Name) + } + + version := versions[0] + if len(versions) > 1 { + version = selectCRDVersionByPriority(versions) + } + gvr.Version = version + } + + isNamespacedResource := crd.Spec.Scope == apiextensionsv1b1.NamespaceScoped + + // Fetch all resources of given type + customResourceList, err := client.Resource(gvr).List(ctx, metav1.ListOptions{}) + if err != nil { + errorList[crd.Name] = err.Error() + continue + } + + if len(customResourceList.Items) == 0 { + continue + } + + if !isNamespacedResource { + objects := []map[string]interface{}{} + for _, item := range customResourceList.Items { + objects = append(objects, item.Object) + } + + err = storeCustomResource(crd.Name, objects, customResources) + if err != nil { + errorList[crd.Name] = err.Error() + continue + } + + } else { + // Group fetched resources by the namespace + perNamespace := map[string][]map[string]interface{}{} + errors := []string{} + + for _, item := range customResourceList.Items { + ns, err := metaAccessor.Namespace(&item) + if err != nil { + errors = append(errors, err.Error()) + continue + } + if perNamespace[ns] == nil { + perNamespace[ns] = []map[string]interface{}{} + } + perNamespace[ns] = append(perNamespace[ns], item.Object) + } + + if len(errors) > 0 { + errorList[crd.Name] = strings.Join(errors, "\n") + } + + // Only include resources from requested namespaces + for _, ns := range namespaces { + if len(perNamespace[ns]) == 0 { + continue + } + + namespacedName := fmt.Sprintf("%s/%s", crd.Name, ns) + err := storeCustomResource(namespacedName, perNamespace[ns], customResources) + if err != nil { + errorList[namespacedName] = err.Error() + continue + } + } + } + } + + return customResources, errorList +} + +// Selects the newest version by kube-aware priority. +func selectCRDVersionByPriority(versions []string) string { + if len(versions) == 0 { + return "" + } + + sort.Slice(versions, func(i, j int) bool { + return version.CompareKubeAwareVersionStrings(versions[i], versions[j]) < 0 + }) + return versions[len(versions)-1] +} + +func storeCustomResource(name string, objects any, m map[string][]byte) error { + y, err := yaml.Marshal(objects) + if err != nil { + return err + } + + m[fmt.Sprintf("%s.yaml", name)] = y + return nil +} + +func pods(ctx context.Context, client *kubernetes.Clientset, namespaces []string) (map[string][]byte, map[string]string, []corev1.Pod) { + podsByNamespace := make(map[string][]byte) + errorsByNamespace := make(map[string]string) + unhealthyPods := []corev1.Pod{} + + for _, namespace := range namespaces { + pods, err := client.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + errorsByNamespace[namespace] = err.Error() + continue + } + + if len(pods.Items) == 0 { + continue + } + + gvk, err := apiutil.GVKForObject(pods, scheme.Scheme) + if err == nil { + pods.GetObjectKind().SetGroupVersionKind(gvk) + } + + for i, o := range pods.Items { + gvk, err := apiutil.GVKForObject(&o, scheme.Scheme) + if err == nil { + pods.Items[i].GetObjectKind().SetGroupVersionKind(gvk) + pods.Items[i].SetManagedFields(nil) + } + } + + b, err := yaml.Marshal(pods) + if err != nil { + errorsByNamespace[namespace] = err.Error() + continue + } + + for _, pod := range pods.Items { + if k8sutil.IsPodUnhealthy(&pod) { + unhealthyPods = append(unhealthyPods, pod) + } + } + + podsByNamespace[namespace+".yaml"] = b + } + + return podsByNamespace, errorsByNamespace, unhealthyPods +} + +func services(ctx context.Context, client *kubernetes.Clientset, namespaces []string) (map[string][]byte, map[string]string) { + servicesByNamespace := make(map[string][]byte) + errorsByNamespace := make(map[string]string) + + for _, namespace := range namespaces { + services, err := client.CoreV1().Services(namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + errorsByNamespace[namespace] = err.Error() + continue + } + + if len(services.Items) == 0 { + continue + } + + gvk, err := apiutil.GVKForObject(services, scheme.Scheme) + if err == nil { + services.GetObjectKind().SetGroupVersionKind(gvk) + } + + for i, o := range services.Items { + gvk, err := apiutil.GVKForObject(&o, scheme.Scheme) + if err == nil { + services.Items[i].GetObjectKind().SetGroupVersionKind(gvk) + services.Items[i].SetManagedFields(nil) + } + } + + b, err := yaml.Marshal(services) + if err != nil { + errorsByNamespace[namespace] = err.Error() + continue + } + + servicesByNamespace[namespace+".yaml"] = b + } + + return servicesByNamespace, errorsByNamespace +} + +func deployments(ctx context.Context, client *kubernetes.Clientset, namespaces []string) (map[string][]byte, map[string]string) { + deploymentsByNamespace := make(map[string][]byte) + errorsByNamespace := make(map[string]string) + + for _, namespace := range namespaces { + deployments, err := client.AppsV1().Deployments(namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + errorsByNamespace[namespace] = err.Error() + continue + } + + if len(deployments.Items) == 0 { + continue + } + + gvk, err := apiutil.GVKForObject(deployments, scheme.Scheme) + if err == nil { + deployments.GetObjectKind().SetGroupVersionKind(gvk) + } + + for i, o := range deployments.Items { + gvk, err := apiutil.GVKForObject(&o, scheme.Scheme) + if err == nil { + deployments.Items[i].GetObjectKind().SetGroupVersionKind(gvk) + deployments.Items[i].SetManagedFields(nil) + } + } + + b, err := yaml.Marshal(deployments) + if err != nil { + errorsByNamespace[namespace] = err.Error() + continue + } + + deploymentsByNamespace[namespace+".yaml"] = b + } + + return deploymentsByNamespace, errorsByNamespace +} + +func daemonsets(ctx context.Context, client *kubernetes.Clientset, namespaces []string) (map[string][]byte, map[string]string) { + daemonsetsByNamespace := make(map[string][]byte) + errorsByNamespace := make(map[string]string) + + for _, namespace := range namespaces { + daemonsets, err := client.AppsV1().DaemonSets(namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + errorsByNamespace[namespace] = err.Error() + continue + } + + if len(daemonsets.Items) == 0 { + continue + } + + gvk, err := apiutil.GVKForObject(daemonsets, scheme.Scheme) + if err == nil { + daemonsets.GetObjectKind().SetGroupVersionKind(gvk) + } + + for i, o := range daemonsets.Items { + gvk, err := apiutil.GVKForObject(&o, scheme.Scheme) + if err == nil { + daemonsets.Items[i].GetObjectKind().SetGroupVersionKind(gvk) + daemonsets.Items[i].SetManagedFields(nil) + } + } + + b, err := yaml.Marshal(daemonsets) + if err != nil { + errorsByNamespace[namespace] = err.Error() + continue + } + + daemonsetsByNamespace[namespace+".yaml"] = b + } + + return daemonsetsByNamespace, errorsByNamespace +} + +func jobs(ctx context.Context, client *kubernetes.Clientset, namespaces []string) (map[string][]byte, map[string]string) { + jobsByNamespace := make(map[string][]byte) + errorsByNamespace := make(map[string]string) + + for _, namespace := range namespaces { + nsJobs, err := client.BatchV1().Jobs(namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + errorsByNamespace[namespace] = err.Error() + continue + } + + if len(nsJobs.Items) == 0 { + continue + } + + gvk, err := apiutil.GVKForObject(nsJobs, scheme.Scheme) + if err == nil { + nsJobs.GetObjectKind().SetGroupVersionKind(gvk) + } + + for i, o := range nsJobs.Items { + gvk, err := apiutil.GVKForObject(&o, scheme.Scheme) + if err == nil { + nsJobs.Items[i].GetObjectKind().SetGroupVersionKind(gvk) + nsJobs.Items[i].SetManagedFields(nil) + } + } + + b, err := yaml.Marshal(nsJobs) + if err != nil { + errorsByNamespace[namespace] = err.Error() + continue + } + + jobsByNamespace[namespace+".yaml"] = b + } + + return jobsByNamespace, errorsByNamespace +} + +func configMaps(ctx context.Context, client kubernetes.Interface, namespaces []string) (map[string][]byte, map[string]string) { + configmapByNamespace := make(map[string][]byte) + errorsByNamespace := make(map[string]string) + + for _, namespace := range namespaces { + configmaps, err := client.CoreV1().ConfigMaps(namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + errorsByNamespace[namespace] = err.Error() + continue + } + + if len(configmaps.Items) == 0 { + continue + } + + gvk, err := apiutil.GVKForObject(configmaps, scheme.Scheme) + if err == nil { + configmaps.GetObjectKind().SetGroupVersionKind(gvk) + } + + for i, o := range configmaps.Items { + gvk, err := apiutil.GVKForObject(&o, scheme.Scheme) + if err == nil { + configmaps.Items[i].GetObjectKind().SetGroupVersionKind(gvk) + configmaps.Items[i].SetManagedFields(nil) + } + } + + b, err := yaml.Marshal(configmaps) + if err != nil { + errorsByNamespace[namespace] = err.Error() + continue + } + + configmapByNamespace[namespace+".yaml"] = b + } + + return configmapByNamespace, errorsByNamespace +} diff --git a/internal/utils/helm/package.go b/internal/utils/helm/package.go index 4cbf81eff4c..68d769a4bfa 100644 --- a/internal/utils/helm/package.go +++ b/internal/utils/helm/package.go @@ -93,6 +93,10 @@ func (pt *PackageTool) Setup() error { return err } + if pt.logger == nil { + pt.logger = NewPrinterForWriter(os.Stdout, false) + } + if err = pt.actionConfig.Init( kubectlFactory, ns, @@ -350,6 +354,7 @@ func (pt *PackageTool) setUninstallOptions(opts *PackageOptions) { pt.actionUninstall.KeepHistory = false pt.actionUninstall.DryRun = opts.DryRun + pt.actionUninstall.Timeout = opts.Timeout if opts.Wait { pt.actionUninstall.Wait = opts.Wait diff --git a/internal/utils/helm/package_test.go b/internal/utils/helm/package_test.go new file mode 100644 index 00000000000..ad5cf138ea8 --- /dev/null +++ b/internal/utils/helm/package_test.go @@ -0,0 +1,106 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package helm + +import ( + "testing" + "time" + + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/cli/values" +) + +func TestPackageTool_Setup(t *testing.T) { + type fields struct { + chartName string + envSettings *cli.EnvSettings + actionConfig *action.Configuration + actionInstall *action.Install + actionUninstall *action.Uninstall + valuesOpts *values.Options + logger Printer + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "shouldSetup", + fields: fields{ + chartName: "mychart", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pt := &PackageTool{ + chartName: tt.fields.chartName, + envSettings: tt.fields.envSettings, + actionConfig: tt.fields.actionConfig, + actionInstall: tt.fields.actionInstall, + actionUninstall: tt.fields.actionUninstall, + valuesOpts: tt.fields.valuesOpts, + logger: tt.fields.logger, + } + if err := pt.Setup(); (err != nil) != tt.wantErr { + t.Errorf("Setup() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestPackageTool_setInstallOptions(t *testing.T) { + type fields struct { + chartName string + envSettings *cli.EnvSettings + actionConfig *action.Configuration + actionInstall *action.Install + actionUninstall *action.Uninstall + valuesOpts *values.Options + logger Printer + } + type args struct { + opts *PackageOptions + } + tests := []struct { + name string + fields fields + args args + }{ + { + name: "shouldSetInstallOptions", + fields: fields{ + chartName: "mychart", + }, + args: args{ + opts: &PackageOptions{ + Version: "1.0.2", + Timeout: 1 * time.Second, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pt := &PackageTool{ + chartName: tt.fields.chartName, + envSettings: tt.fields.envSettings, + actionConfig: tt.fields.actionConfig, + actionInstall: tt.fields.actionInstall, + actionUninstall: tt.fields.actionUninstall, + valuesOpts: tt.fields.valuesOpts, + logger: tt.fields.logger, + } + if err := pt.Setup(); err != nil { + t.Errorf("Setup() error = %v", err) + } + pt.setInstallOptions(tt.args.opts) + }) + } +} diff --git a/internal/xds/translator/accesslog.go b/internal/xds/translator/accesslog.go index 4a22b20500b..01c448b65e9 100644 --- a/internal/xds/translator/accesslog.go +++ b/internal/xds/translator/accesslog.go @@ -6,7 +6,6 @@ package translator import ( - "encoding/json" "errors" "sort" "strings" @@ -132,6 +131,7 @@ func buildXdsAccessLog(al *ir.AccessLog, forListener bool) []*accesslog.AccessLo ConfigType: &accesslog.AccessLog_TypedConfig{ TypedConfig: accesslogAny, }, + Filter: buildAccessLogFilter(text.CELMatches, forListener), }) } // handle json file access logs @@ -174,6 +174,7 @@ func buildXdsAccessLog(al *ir.AccessLog, forListener bool) []*accesslog.AccessLo ConfigType: &accesslog.AccessLog_TypedConfig{ TypedConfig: accesslogAny, }, + Filter: buildAccessLogFilter(json.CELMatches, forListener), }) } // handle ALS access logs @@ -190,27 +191,6 @@ func buildXdsAccessLog(al *ir.AccessLog, forListener bool) []*accesslog.AccessLo TransportApiVersion: cfgcore.ApiVersion_V3, } - // include text and json format as metadata when initiating stream - md := make([]*cfgcore.HeaderValue, 0, 2) - - if als.Text != nil && *als.Text != "" { - md = append(md, &cfgcore.HeaderValue{ - Key: "x-accesslog-text", - Value: strings.ReplaceAll(strings.Trim(*als.Text, "\x00\n\r"), "\x00\n\r", " "), - }) - } - - if len(als.Attributes) > 0 { - if attr, err := json.Marshal(als.Attributes); err == nil { - md = append(md, &cfgcore.HeaderValue{ - Key: "x-accesslog-attr", - Value: string(attr), - }) - } - } - - cc.GrpcService.InitialMetadata = md - switch als.Type { case egv1a1.ALSEnvoyProxyAccessLogTypeHTTP: alCfg := &grpcaccesslog.HttpGrpcAccessLogConfig{ @@ -229,6 +209,7 @@ func buildXdsAccessLog(al *ir.AccessLog, forListener bool) []*accesslog.AccessLo ConfigType: &accesslog.AccessLog_TypedConfig{ TypedConfig: accesslogAny, }, + Filter: buildAccessLogFilter(als.CELMatches, forListener), }) case egv1a1.ALSEnvoyProxyAccessLogTypeTCP: alCfg := &grpcaccesslog.TcpGrpcAccessLogConfig{ @@ -241,6 +222,7 @@ func buildXdsAccessLog(al *ir.AccessLog, forListener bool) []*accesslog.AccessLo ConfigType: &accesslog.AccessLog_TypedConfig{ TypedConfig: accesslogAny, }, + Filter: buildAccessLogFilter(als.CELMatches, forListener), }) } } @@ -288,24 +270,10 @@ func buildXdsAccessLog(al *ir.AccessLog, forListener bool) []*accesslog.AccessLo ConfigType: &accesslog.AccessLog_TypedConfig{ TypedConfig: accesslogAny, }, + Filter: buildAccessLogFilter(otel.CELMatches, forListener), }) } - // add filter for access logs - filters := make([]*accesslog.AccessLogFilter, 0) - for _, expr := range al.CELMatches { - filters = append(filters, celAccessLogFilter(expr)) - } - if forListener { - filters = append(filters, listenerAccessLogFilter) - } - - f := buildAccessLogFilter(filters...) - - for _, log := range accessLogs { - log.Filter = f - } - return accessLogs } @@ -324,19 +292,28 @@ func celAccessLogFilter(expr string) *accesslog.AccessLogFilter { } } -func buildAccessLogFilter(f ...*accesslog.AccessLogFilter) *accesslog.AccessLogFilter { - if len(f) == 0 { +func buildAccessLogFilter(exprs []string, forListener bool) *accesslog.AccessLogFilter { + // add filter for access logs + var filters []*accesslog.AccessLogFilter + for _, expr := range exprs { + filters = append(filters, celAccessLogFilter(expr)) + } + if forListener { + filters = append(filters, listenerAccessLogFilter) + } + + if len(filters) == 0 { return nil } - if len(f) == 1 { - return f[0] + if len(filters) == 1 { + return filters[0] } return &accesslog.AccessLogFilter{ FilterSpecifier: &accesslog.AccessLogFilter_AndFilter{ AndFilter: &accesslog.AndFilter{ - Filters: f, + Filters: filters, }, }, } diff --git a/internal/xds/translator/extproc.go b/internal/xds/translator/extproc.go index 9d4779b41c7..2eeeb585590 100644 --- a/internal/xds/translator/extproc.go +++ b/internal/xds/translator/extproc.go @@ -51,7 +51,7 @@ func (*extProc) patchHCM(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTTPLi continue } - for _, ep := range route.ExtProcs { + for _, ep := range route.EnvoyExtensions.ExtProcs { if hcmContainsFilter(mgr, extProcFilterName(ep)) { continue } @@ -156,7 +156,7 @@ func routeContainsExtProc(irRoute *ir.HTTPRoute) bool { return false } - return len(irRoute.ExtProcs) > 0 + return irRoute.EnvoyExtensions != nil && len(irRoute.EnvoyExtensions.ExtProcs) > 0 } // patchResources patches the cluster resources for the external services. @@ -173,8 +173,8 @@ func (*extProc) patchResources(tCtx *types.ResourceVersionTable, continue } - for i := range route.ExtProcs { - ep := route.ExtProcs[i] + for i := range route.EnvoyExtensions.ExtProcs { + ep := route.EnvoyExtensions.ExtProcs[i] if err := createExtServiceXDSCluster( &ep.Destination, tCtx); err != nil && !errors.Is( err, ErrXdsClusterExists) { @@ -196,8 +196,11 @@ func (*extProc) patchRoute(route *routev3.Route, irRoute *ir.HTTPRoute) error { if irRoute == nil { return errors.New("ir route is nil") } + if irRoute.EnvoyExtensions == nil { + return nil + } - for _, ep := range irRoute.ExtProcs { + for _, ep := range irRoute.EnvoyExtensions.ExtProcs { filterName := extProcFilterName(ep) if err := enableFilterOnRoute(route, filterName); err != nil { return err diff --git a/internal/xds/translator/route.go b/internal/xds/translator/route.go index 139b359a15a..2b9c75a2e91 100644 --- a/internal/xds/translator/route.go +++ b/internal/xds/translator/route.go @@ -419,15 +419,6 @@ func buildXdsURLRewriteAction(destName string, urlRewrite *ir.URLRewrite, pathMa func buildXdsDirectResponseAction(res *ir.DirectResponse) *routev3.DirectResponseAction { routeAction := &routev3.DirectResponseAction{Status: res.StatusCode} - - if res.Body != nil { - routeAction.Body = &corev3.DataSource{ - Specifier: &corev3.DataSource_InlineString{ - InlineString: *res.Body, - }, - } - } - return routeAction } @@ -505,12 +496,12 @@ func buildHashPolicy(httpRoute *ir.HTTPRoute) []*routev3.RouteAction_HashPolicy hashPolicy.GetCookie().Ttl = durationpb.New(ch.Cookie.TTL.Duration) } if ch.Cookie.Attributes != nil { - attributes := make([]*routev3.RouteAction_HashPolicy_CookieAttribute, len(ch.Cookie.Attributes)) - i := 0 + attributes := make([]*routev3.RouteAction_HashPolicy_CookieAttribute, 0, len(ch.Cookie.Attributes)) for name, value := range ch.Cookie.Attributes { - attributes[i].Name = name - attributes[i].Value = value - i++ + attributes = append(attributes, &routev3.RouteAction_HashPolicy_CookieAttribute{ + Name: name, + Value: value, + }) } hashPolicy.GetCookie().Attributes = attributes } diff --git a/internal/xds/translator/testdata/in/xds-ir/accesslog-cel.yaml b/internal/xds/translator/testdata/in/xds-ir/accesslog-cel.yaml index b0623fd0842..e9cff901d3d 100644 --- a/internal/xds/translator/testdata/in/xds-ir/accesslog-cel.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/accesslog-cel.yaml @@ -1,13 +1,15 @@ name: "accesslog" accesslog: - celMatches: - - response.code >= 400 text: - path: "/dev/stdout" + celMatches: + - response.code >= 400 format: | [%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%" json: - path: "/dev/stdout" + celMatches: + - response.code >= 400 json: start_time: "%START_TIME%" method: "%REQ(:METHOD)%" @@ -22,6 +24,8 @@ accesslog: resources: "cluster_name": "cluster1" authority: "otel-collector.default.svc.cluster.local" + celMatches: + - response.code >= 400 destination: name: "accesslog-0" settings: diff --git a/internal/xds/translator/testdata/in/xds-ir/accesslog-multi-cel.yaml b/internal/xds/translator/testdata/in/xds-ir/accesslog-multi-cel.yaml index 704c19863d6..fab193fe564 100644 --- a/internal/xds/translator/testdata/in/xds-ir/accesslog-multi-cel.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/accesslog-multi-cel.yaml @@ -1,14 +1,18 @@ name: "accesslog" accesslog: - celMatches: - - response.code >= 400 - - request.url_path.contains('v1beta3') + text: - path: "/dev/stdout" + celMatches: + - response.code >= 400 + - request.url_path.contains('v1beta3') format: | [%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%" json: - path: "/dev/stdout" + celMatches: + - response.code >= 400 + - request.url_path.contains('v1beta3') json: start_time: "%START_TIME%" method: "%REQ(:METHOD)%" @@ -23,6 +27,9 @@ accesslog: resources: "cluster_name": "cluster1" authority: "otel-collector.default.svc.cluster.local" + celMatches: + - response.code >= 400 + - request.url_path.contains('v1beta3') destination: name: "accesslog-0" settings: diff --git a/internal/xds/translator/testdata/in/xds-ir/custom-filter-order.yaml b/internal/xds/translator/testdata/in/xds-ir/custom-filter-order.yaml index 4650e1ea2a3..4971328e8cb 100644 --- a/internal/xds/translator/testdata/in/xds-ir/custom-filter-order.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/custom-filter-order.yaml @@ -61,24 +61,25 @@ http: name: example2 remoteJWKS: uri: http://two.example.com/jwt/public-key/jwks.json - wasm: - - config: - parameter1: - key1: value1 - key2: value2 - parameter2: value3 - failOpen: false - httpWasmCode: - sha256: 746df05c8f3a0b07a46c0967cfbc5cbe5b9d48d0f79b6177eeedf8be6c8b34b5 - servingURL: https://envoy-gateway:18002/42d30b4a4cc631415e6e48c02d244700da327201eb273f752cacf745715b31d9.wasm - name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/0 - wasmName: wasm-filter-1 - - config: - parameter1: value1 - parameter2: value2 - failOpen: false - httpWasmCode: - sha256: a1efca12ea51069abb123bf9c77889fcc2a31cc5483fc14d115e44fdf07c7980 - servingURL: https://envoy-gateway:18002/7abf116e5cd5a20389604a5ba0f3bd04fdf76f92181fe67506b42c2ee596d3fd.wasm - name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/1 - wasmName: wasm-filter-2 + envoyExtensions: + wasms: + - config: + parameter1: + key1: value1 + key2: value2 + parameter2: value3 + failOpen: false + httpWasmCode: + sha256: 746df05c8f3a0b07a46c0967cfbc5cbe5b9d48d0f79b6177eeedf8be6c8b34b5 + servingURL: https://envoy-gateway:18002/42d30b4a4cc631415e6e48c02d244700da327201eb273f752cacf745715b31d9.wasm + name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/0 + wasmName: wasm-filter-1 + - config: + parameter1: value1 + parameter2: value2 + failOpen: false + httpWasmCode: + sha256: a1efca12ea51069abb123bf9c77889fcc2a31cc5483fc14d115e44fdf07c7980 + servingURL: https://envoy-gateway:18002/7abf116e5cd5a20389604a5ba0f3bd04fdf76f92181fe67506b42c2ee596d3fd.wasm + name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/1 + wasmName: wasm-filter-2 diff --git a/internal/xds/translator/testdata/in/xds-ir/ext-proc.yaml b/internal/xds/translator/testdata/in/xds-ir/ext-proc.yaml index 8209c2947e3..3fa4cd8bcc7 100644 --- a/internal/xds/translator/testdata/in/xds-ir/ext-proc.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/ext-proc.yaml @@ -18,30 +18,31 @@ http: port: 8080 protocol: HTTP weight: 1 - extProc: - - name: envoyextensionpolicy/default/policy-for-route-2/extproc/0 - failOpen: true - messageTimeout: 5s - requestHeaderProcessing: true - requestBodyProcessingMode: Buffered - responseBodyProcessingMode: Streamed - authority: grpc-backend-4.default:4000 - destination: - name: envoyextensionpolicy/default/policy-for-route-2/0/grpc-backend-4 - settings: - - protocol: GRPC - weight: 1 - - name: envoyextensionpolicy/default/policy-for-route-1/extproc/0 - failOpen: true - messageTimeout: 5s - responseHeaderProcessing: true - requestBodyProcessingMode: BufferedPartial - authority: grpc-backend-2.default:8000 - destination: - name: envoyextensionpolicy/default/policy-for-route-1/0/grpc-backend-2 - settings: - - protocol: GRPC - weight: 1 + envoyExtensions: + extProcs: + - name: envoyextensionpolicy/default/policy-for-route-2/extproc/0 + failOpen: true + messageTimeout: 5s + requestHeaderProcessing: true + requestBodyProcessingMode: Buffered + responseBodyProcessingMode: Streamed + authority: grpc-backend-4.default:4000 + destination: + name: envoyextensionpolicy/default/policy-for-route-2/0/grpc-backend-4 + settings: + - protocol: GRPC + weight: 1 + - name: envoyextensionpolicy/default/policy-for-route-1/extproc/0 + failOpen: true + messageTimeout: 5s + responseHeaderProcessing: true + requestBodyProcessingMode: BufferedPartial + authority: grpc-backend-2.default:8000 + destination: + name: envoyextensionpolicy/default/policy-for-route-1/0/grpc-backend-2 + settings: + - protocol: GRPC + weight: 1 hostname: gateway.envoyproxy.io isHTTP2: false name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io @@ -58,23 +59,24 @@ http: port: 8080 protocol: HTTP weight: 1 - extProc: - - name: envoyextensionpolicy/envoy-gateway/policy-for-gateway-2/extproc/0 - authority: grpc-backend-3.envoy-gateway:3000 - destination: - name: envoyextensionpolicy/envoy-gateway/policy-for-gateway-2/0/grpc-backend-3 - settings: - - protocol: GRPC - weight: 1 - - name: envoyextensionpolicy/envoy-gateway/policy-for-gateway-1/extproc/0 - failOpen: false - messageTimeout: 15s - authority: grpc-backend.envoy-gateway:9000 - destination: - name: envoyextensionpolicy/envoy-gateway/policy-for-gateway-1/0/grpc-backend - settings: - - protocol: GRPC - weight: 1 + envoyExtensions: + extProcs: + - name: envoyextensionpolicy/envoy-gateway/policy-for-gateway-2/extproc/0 + authority: grpc-backend-3.envoy-gateway:3000 + destination: + name: envoyextensionpolicy/envoy-gateway/policy-for-gateway-2/0/grpc-backend-3 + settings: + - protocol: GRPC + weight: 1 + - name: envoyextensionpolicy/envoy-gateway/policy-for-gateway-1/extproc/0 + failOpen: false + messageTimeout: 15s + authority: grpc-backend.envoy-gateway:9000 + destination: + name: envoyextensionpolicy/envoy-gateway/policy-for-gateway-1/0/grpc-backend + settings: + - protocol: GRPC + weight: 1 hostname: gateway.envoyproxy.io isHTTP2: false name: httproute/default/httproute-2/rule/0/match/0/gateway_envoyproxy_io diff --git a/internal/xds/translator/testdata/in/xds-ir/http-route-with-clientcert.yaml b/internal/xds/translator/testdata/in/xds-ir/http-route-with-clientcert.yaml new file mode 100644 index 00000000000..1cfaf827676 --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/http-route-with-clientcert.yaml @@ -0,0 +1,40 @@ +http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + name: envoy-gateway/gateway-btls/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - backendWeights: + invalid: 0 + valid: 0 + destination: + name: httproute/envoy-gateway/httproute-btls/rule/0 + settings: + - addressType: IP + endpoints: + - host: 10.244.0.11 + port: 8080 + protocol: HTTP + tls: + CACertificate: + certificate: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURKekNDQWcrZ0F3SUJBZ0lVQWw2VUtJdUttenRlODFjbGx6NVBmZE4ySWxJd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0l6RVFNQTRHQTFVRUF3d0hiWGxqYVdWdWRERVBNQTBHQTFVRUNnd0dhM1ZpWldSaU1CNFhEVEl6TVRBdwpNakExTkRFMU4xb1hEVEkwTVRBd01UQTFOREUxTjFvd0l6RVFNQTRHQTFVRUF3d0hiWGxqYVdWdWRERVBNQTBHCkExVUVDZ3dHYTNWaVpXUmlNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXdTVGMKMXlqOEhXNjJueW5rRmJYbzRWWEt2MmpDMFBNN2RQVmt5ODdGd2VaY1RLTG9XUVZQUUUycDJrTERLNk9Fc3ptTQp5eXIreHhXdHlpdmVyZW1yV3FuS2tOVFloTGZZUGhnUWtjemliN2VVYWxtRmpVYmhXZEx2SGFrYkVnQ29kbjNiCmt6NTdtSW5YMlZwaURPS2c0a3lIZml1WFdwaUJxckN4MEtOTHB4bzNERVFjRmNzUVRlVEh6aDQ3NTJHVjA0UlUKVGkvR0VXeXpJc2w0Umc3dEd0QXdtY0lQZ1VOVWZZMlEzOTBGR3FkSDRhaG4rbXcvNmFGYlczMVc2M2Q5WUpWcQppb3lPVmNhTUlwTTVCL2M3UWM4U3VoQ0kxWUdoVXlnNGNSSExFdzVWdGlraW95RTNYMDRrbmEzalFBajU0WWJSCmJwRWhjMzVhcEtMQjIxSE9VUUlEQVFBQm8xTXdVVEFkQmdOVkhRNEVGZ1FVeXZsMFZJNXZKVlN1WUZYdTdCNDgKNlBiTUVBb3dId1lEVlIwakJCZ3dGb0FVeXZsMFZJNXZKVlN1WUZYdTdCNDg2UGJNRUFvd0R3WURWUjBUQVFILwpCQVV3QXdFQi96QU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFNTHhyZ0ZWTXVOUnEyd0F3Y0J0N1NuTlI1Q2Z6CjJNdlhxNUVVbXVhd0lVaTlrYVlqd2RWaURSRUdTams3SlcxN3ZsNTc2SGpEa2RmUndpNEUyOFN5ZFJJblpmNkoKaThIWmNaN2NhSDZEeFIzMzVmZ0hWekxpNU5pVGNlL09qTkJRelEyTUpYVkRkOERCbUc1ZnlhdEppT0pRNGJXRQpBN0ZsUDBSZFAzQ08zR1dFME01aVhPQjJtMXFXa0UyZXlPNFVIdndUcU5RTGRyZEFYZ0RRbGJhbTllNEJHM0dnCmQvNnRoQWtXRGJ0L1FOVCtFSkhEQ3ZoRFJLaDFSdUdIeWcrWSsvbmViVFdXckZXc2t0UnJiT29IQ1ppQ3BYSTEKM2VYRTZudDBZa2d0RHhHMjJLcW5ocEFnOWdVU3MyaGxob3h5dmt6eUYwbXU2TmhQbHdBZ25xNysvUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + name: policy-btls/policies-ca + clientCertificates: + - name: secret-1 + # byte slice representation of "key-data" + serverCertificate: [ 99, 101, 114, 116, 45, 100, 97, 116, 97 ] + # byte slice representation of "key-data" + privateKey: [ 107, 101, 121, 45, 100, 97, 116, 97 ] + SNI: example.com + weight: 1 + hostname: '*' + name: httproute/envoy-gateway/httproute-btls/rule/0/match/0/* + pathMatch: + distinct: false + exact: /exact + name: "" diff --git a/internal/xds/translator/testdata/in/xds-ir/load-balancer.yaml b/internal/xds/translator/testdata/in/xds-ir/load-balancer.yaml index 998318fdb1d..d2b754bf16b 100644 --- a/internal/xds/translator/testdata/in/xds-ir/load-balancer.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/load-balancer.yaml @@ -117,3 +117,18 @@ http: - endpoints: - host: "1.2.3.4" port: 50000 + - name: "tenth-route" + hostname: "*" + traffic: + loadBalancer: + consistentHash: + cookie: + name: "test" + attributes: + foo: bar + destination: + name: "tenth-route-dest" + settings: + - endpoints: + - host: "1.2.3.4" + port: 50000 diff --git a/internal/xds/translator/testdata/in/xds-ir/wasm.yaml b/internal/xds/translator/testdata/in/xds-ir/wasm.yaml index faa729eec98..9afa2c97c9c 100644 --- a/internal/xds/translator/testdata/in/xds-ir/wasm.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/wasm.yaml @@ -25,20 +25,21 @@ http: distinct: false name: "" prefix: /foo - wasm: - - config: - parameter1: - key1: value1 - parameter2: - key2: - key3: value3 - failOpen: true - httpWasmCode: - servingURL: https://envoy-gateway:18002/fe571e7b1ef5dc626ceb2c2c86782a134a92989a2643485238951696ae4334c3.wasm - originalDownloadingURL: https://www.test.com/wasm-filter-4.wasm - sha256: a1f0b78b8c1320690327800e3a5de10e7dbba7b6c752e702193a395a52c727b6 - name: envoyextensionpolicy/default/policy-for-http-route/wasm/0 - wasmName: wasm-filter-4 + envoyExtensions: + wasms: + - config: + parameter1: + key1: value1 + parameter2: + key2: + key3: value3 + failOpen: true + httpWasmCode: + servingURL: https://envoy-gateway:18002/fe571e7b1ef5dc626ceb2c2c86782a134a92989a2643485238951696ae4334c3.wasm + originalDownloadingURL: https://www.test.com/wasm-filter-4.wasm + sha256: a1f0b78b8c1320690327800e3a5de10e7dbba7b6c752e702193a395a52c727b6 + name: envoyextensionpolicy/default/policy-for-http-route/wasm/0 + wasmName: wasm-filter-4 - destination: name: httproute/default/httproute-2/rule/0 settings: @@ -55,35 +56,36 @@ http: distinct: false name: "" prefix: /bar - wasm: - - config: - parameter1: - key1: value1 - key2: value2 - parameter2: value3 - failOpen: false - httpWasmCode: - servingURL: https://envoy-gateway:18002/5c90b9a82642ce00a7753923fabead306b9d9a54a7c0bd2463a1af3efcfb110b.wasm - originalDownloadingURL: https://www.example.com/wasm-filter-1.wasm - sha256: 746df05c8f3a0b07a46c0967cfbc5cbe5b9d48d0f79b6177eeedf8be6c8b34b5 - name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/wasm/0 - wasmName: wasm-filter-1 - - config: - parameter1: value1 - parameter2: value2 - failOpen: false - httpWasmCode: - servingURL: https://envoy-gateway:18002/7abf116e5cd5a20389604a5ba0f3bd04fdf76f92181fe67506b42c2ee596d3fd.wasm - originalDownloadingURL: oci://www.example.com/wasm-filter-2:v1.0.0 - sha256: 314100af781b98a8ca175d5bf90a8bf76576e20a2f397a88223404edc6ebfd46 - name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/wasm/1 - wasmName: wasm-filter-2 - rootID: my-root-id - - config: null - failOpen: false - httpWasmCode: - servingURL: https://envoy-gateway:18002/42d30b4a4cc631415e6e48c02d244700da327201eb273f752cacf745715b31d9.wasm - originalDownloadingURL: oci://www.example.com:8080/wasm-filter-3:latest - sha256: 2a19e4f337e5223d7287e7fccd933fb01905deaff804292e5257f8c681b82bee - name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/wasm/2 - wasmName: envoyextensionpolicy/envoy-gateway/policy-for-gateway/wasm/2 + envoyExtensions: + wasms: + - config: + parameter1: + key1: value1 + key2: value2 + parameter2: value3 + failOpen: false + httpWasmCode: + servingURL: https://envoy-gateway:18002/5c90b9a82642ce00a7753923fabead306b9d9a54a7c0bd2463a1af3efcfb110b.wasm + originalDownloadingURL: https://www.example.com/wasm-filter-1.wasm + sha256: 746df05c8f3a0b07a46c0967cfbc5cbe5b9d48d0f79b6177eeedf8be6c8b34b5 + name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/wasm/0 + wasmName: wasm-filter-1 + - config: + parameter1: value1 + parameter2: value2 + failOpen: false + httpWasmCode: + servingURL: https://envoy-gateway:18002/7abf116e5cd5a20389604a5ba0f3bd04fdf76f92181fe67506b42c2ee596d3fd.wasm + originalDownloadingURL: oci://www.example.com/wasm-filter-2:v1.0.0 + sha256: 314100af781b98a8ca175d5bf90a8bf76576e20a2f397a88223404edc6ebfd46 + name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/wasm/1 + wasmName: wasm-filter-2 + rootID: my-root-id + - config: null + failOpen: false + httpWasmCode: + servingURL: https://envoy-gateway:18002/42d30b4a4cc631415e6e48c02d244700da327201eb273f752cacf745715b31d9.wasm + originalDownloadingURL: oci://www.example.com:8080/wasm-filter-3:latest + sha256: 2a19e4f337e5223d7287e7fccd933fb01905deaff804292e5257f8c681b82bee + name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/wasm/2 + wasmName: envoyextensionpolicy/envoy-gateway/policy-for-gateway/wasm/2 diff --git a/internal/xds/translator/testdata/out/xds-ir/accesslog-cel.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/accesslog-cel.routes.yaml index d4a7fa5ae20..b214e8b05a3 100644 --- a/internal/xds/translator/testdata/out/xds-ir/accesslog-cel.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/accesslog-cel.routes.yaml @@ -6,8 +6,6 @@ name: first-listener/* routes: - directResponse: - body: - inlineString: 'Unknown custom filter type: UnsupportedType' status: 500 match: prefix: / diff --git a/internal/xds/translator/testdata/out/xds-ir/accesslog-endpoint-stats.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/accesslog-endpoint-stats.routes.yaml index d4a7fa5ae20..b214e8b05a3 100644 --- a/internal/xds/translator/testdata/out/xds-ir/accesslog-endpoint-stats.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/accesslog-endpoint-stats.routes.yaml @@ -6,8 +6,6 @@ name: first-listener/* routes: - directResponse: - body: - inlineString: 'Unknown custom filter type: UnsupportedType' status: 500 match: prefix: / diff --git a/internal/xds/translator/testdata/out/xds-ir/accesslog-formatters.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/accesslog-formatters.routes.yaml index d4a7fa5ae20..b214e8b05a3 100644 --- a/internal/xds/translator/testdata/out/xds-ir/accesslog-formatters.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/accesslog-formatters.routes.yaml @@ -6,8 +6,6 @@ name: first-listener/* routes: - directResponse: - body: - inlineString: 'Unknown custom filter type: UnsupportedType' status: 500 match: prefix: / diff --git a/internal/xds/translator/testdata/out/xds-ir/accesslog-multi-cel.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/accesslog-multi-cel.routes.yaml index d4a7fa5ae20..b214e8b05a3 100644 --- a/internal/xds/translator/testdata/out/xds-ir/accesslog-multi-cel.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/accesslog-multi-cel.routes.yaml @@ -6,8 +6,6 @@ name: first-listener/* routes: - directResponse: - body: - inlineString: 'Unknown custom filter type: UnsupportedType' status: 500 match: prefix: / diff --git a/internal/xds/translator/testdata/out/xds-ir/accesslog-without-format.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/accesslog-without-format.routes.yaml index d4a7fa5ae20..b214e8b05a3 100644 --- a/internal/xds/translator/testdata/out/xds-ir/accesslog-without-format.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/accesslog-without-format.routes.yaml @@ -6,8 +6,6 @@ name: first-listener/* routes: - directResponse: - body: - inlineString: 'Unknown custom filter type: UnsupportedType' status: 500 match: prefix: / diff --git a/internal/xds/translator/testdata/out/xds-ir/accesslog.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/accesslog.listeners.yaml index 21621ff674e..16609de576c 100644 --- a/internal/xds/translator/testdata/out/xds-ir/accesslog.listeners.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/accesslog.listeners.yaml @@ -43,14 +43,6 @@ grpcService: envoyGrpc: clusterName: accesslog/monitoring/envoy-als/port/9000 - initialMetadata: - - key: x-accesslog-text - value: '[%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%"' - - key: x-accesslog-attr - value: '{"attr1":"value1","attr2":"value2"}' transportApiVersion: V3 - filter: responseFlagFilter: @@ -127,15 +119,6 @@ grpcService: envoyGrpc: clusterName: accesslog/monitoring/envoy-als/port/9000 - initialMetadata: - - key: x-accesslog-text - value: '[%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%"' - - key: x-accesslog-attr - value: '{"attr1":"value1","attr2":"value2"}' transportApiVersion: V3 - name: envoy.access_loggers.open_telemetry typedConfig: diff --git a/internal/xds/translator/testdata/out/xds-ir/accesslog.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/accesslog.routes.yaml index d4a7fa5ae20..b214e8b05a3 100644 --- a/internal/xds/translator/testdata/out/xds-ir/accesslog.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/accesslog.routes.yaml @@ -6,8 +6,6 @@ name: first-listener/* routes: - directResponse: - body: - inlineString: 'Unknown custom filter type: UnsupportedType' status: 500 match: prefix: / diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-direct-response.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-direct-response.routes.yaml index d4a7fa5ae20..b214e8b05a3 100644 --- a/internal/xds/translator/testdata/out/xds-ir/http-route-direct-response.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-direct-response.routes.yaml @@ -6,8 +6,6 @@ name: first-listener/* routes: - directResponse: - body: - inlineString: 'Unknown custom filter type: UnsupportedType' status: 500 match: prefix: / diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-with-clientcert.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-with-clientcert.clusters.yaml new file mode 100644 index 00000000000..6d69b493981 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-with-clientcert.clusters.yaml @@ -0,0 +1,43 @@ +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: httproute/envoy-gateway/httproute-btls/rule/0 + lbPolicy: LEAST_REQUEST + name: httproute/envoy-gateway/httproute-btls/rule/0 + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + transportSocketMatches: + - match: + name: httproute/envoy-gateway/httproute-btls/rule/0/tls/0 + name: httproute/envoy-gateway/httproute-btls/rule/0/tls/0 + transportSocket: + name: envoy.transport_sockets.tls + typedConfig: + '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + commonTlsContext: + combinedValidationContext: + defaultValidationContext: + matchTypedSubjectAltNames: + - matcher: + exact: example.com + sanType: DNS + validationContextSdsSecretConfig: + name: policy-btls/policies-ca + sdsConfig: + ads: {} + resourceApiVersion: V3 + tlsCertificateSdsSecretConfigs: + - name: secret-1 + sdsConfig: + ads: {} + resourceApiVersion: V3 + sni: example.com + type: EDS diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-with-clientcert.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-with-clientcert.endpoints.yaml new file mode 100644 index 00000000000..3d0e8160769 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-with-clientcert.endpoints.yaml @@ -0,0 +1,16 @@ +- clusterName: httproute/envoy-gateway/httproute-btls/rule/0 + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 10.244.0.11 + portValue: 8080 + loadBalancingWeight: 1 + metadata: + filterMetadata: + envoy.transport_socket_match: + name: httproute/envoy-gateway/httproute-btls/rule/0/tls/0 + loadBalancingWeight: 1 + locality: + region: httproute/envoy-gateway/httproute-btls/rule/0/backend/0 diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-with-clientcert.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-with-clientcert.listeners.yaml new file mode 100644 index 00000000000..ff5431da747 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-with-clientcert.listeners.yaml @@ -0,0 +1,35 @@ +- address: + socketAddress: + address: 0.0.0.0 + portValue: 10080 + defaultFilterChain: + filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + commonHttpProtocolOptions: + headersWithUnderscoresAction: REJECT_REQUEST + http2ProtocolOptions: + initialConnectionWindowSize: 1048576 + initialStreamWindowSize: 65536 + maxConcurrentStreams: 100 + httpFilters: + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + suppressEnvoyHeaders: true + mergeSlashes: true + normalizePath: true + pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT + rds: + configSource: + ads: {} + resourceApiVersion: V3 + routeConfigName: envoy-gateway/gateway-btls/http + serverHeaderTransformation: PASS_THROUGH + statPrefix: http + useRemoteAddress: true + name: envoy-gateway/gateway-btls/http + drainType: MODIFY_ONLY + name: envoy-gateway/gateway-btls/http + perConnectionBufferLimitBytes: 32768 diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-with-clientcert.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-with-clientcert.routes.yaml new file mode 100644 index 00000000000..bd4f9cfe7e2 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-with-clientcert.routes.yaml @@ -0,0 +1,14 @@ +- ignorePortInHostMatching: true + name: envoy-gateway/gateway-btls/http + virtualHosts: + - domains: + - '*' + name: envoy-gateway/gateway-btls/http/* + routes: + - match: + path: /exact + name: httproute/envoy-gateway/httproute-btls/rule/0/match/0/* + route: + cluster: httproute/envoy-gateway/httproute-btls/rule/0 + upgradeConfigs: + - upgradeType: websocket diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-with-clientcert.secrets.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-with-clientcert.secrets.yaml new file mode 100644 index 00000000000..43275c11630 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-with-clientcert.secrets.yaml @@ -0,0 +1,10 @@ +- name: secret-1 + tlsCertificate: + certificateChain: + inlineBytes: Y2VydC1kYXRh + privateKey: + inlineBytes: a2V5LWRhdGE= +- name: policy-btls/policies-ca + validationContext: + trustedCa: + inlineBytes: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURKekNDQWcrZ0F3SUJBZ0lVQWw2VUtJdUttenRlODFjbGx6NVBmZE4ySWxJd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0l6RVFNQTRHQTFVRUF3d0hiWGxqYVdWdWRERVBNQTBHQTFVRUNnd0dhM1ZpWldSaU1CNFhEVEl6TVRBdwpNakExTkRFMU4xb1hEVEkwTVRBd01UQTFOREUxTjFvd0l6RVFNQTRHQTFVRUF3d0hiWGxqYVdWdWRERVBNQTBHCkExVUVDZ3dHYTNWaVpXUmlNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXdTVGMKMXlqOEhXNjJueW5rRmJYbzRWWEt2MmpDMFBNN2RQVmt5ODdGd2VaY1RLTG9XUVZQUUUycDJrTERLNk9Fc3ptTQp5eXIreHhXdHlpdmVyZW1yV3FuS2tOVFloTGZZUGhnUWtjemliN2VVYWxtRmpVYmhXZEx2SGFrYkVnQ29kbjNiCmt6NTdtSW5YMlZwaURPS2c0a3lIZml1WFdwaUJxckN4MEtOTHB4bzNERVFjRmNzUVRlVEh6aDQ3NTJHVjA0UlUKVGkvR0VXeXpJc2w0Umc3dEd0QXdtY0lQZ1VOVWZZMlEzOTBGR3FkSDRhaG4rbXcvNmFGYlczMVc2M2Q5WUpWcQppb3lPVmNhTUlwTTVCL2M3UWM4U3VoQ0kxWUdoVXlnNGNSSExFdzVWdGlraW95RTNYMDRrbmEzalFBajU0WWJSCmJwRWhjMzVhcEtMQjIxSE9VUUlEQVFBQm8xTXdVVEFkQmdOVkhRNEVGZ1FVeXZsMFZJNXZKVlN1WUZYdTdCNDgKNlBiTUVBb3dId1lEVlIwakJCZ3dGb0FVeXZsMFZJNXZKVlN1WUZYdTdCNDg2UGJNRUFvd0R3WURWUjBUQVFILwpCQVV3QXdFQi96QU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFNTHhyZ0ZWTXVOUnEyd0F3Y0J0N1NuTlI1Q2Z6CjJNdlhxNUVVbXVhd0lVaTlrYVlqd2RWaURSRUdTams3SlcxN3ZsNTc2SGpEa2RmUndpNEUyOFN5ZFJJblpmNkoKaThIWmNaN2NhSDZEeFIzMzVmZ0hWekxpNU5pVGNlL09qTkJRelEyTUpYVkRkOERCbUc1ZnlhdEppT0pRNGJXRQpBN0ZsUDBSZFAzQ08zR1dFME01aVhPQjJtMXFXa0UyZXlPNFVIdndUcU5RTGRyZEFYZ0RRbGJhbTllNEJHM0dnCmQvNnRoQWtXRGJ0L1FOVCtFSkhEQ3ZoRFJLaDFSdUdIeWcrWSsvbmViVFdXckZXc2t0UnJiT29IQ1ppQ3BYSTEKM2VYRTZudDBZa2d0RHhHMjJLcW5ocEFnOWdVU3MyaGxob3h5dmt6eUYwbXU2TmhQbHdBZ25xNysvUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K diff --git a/internal/xds/translator/testdata/out/xds-ir/load-balancer.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/load-balancer.clusters.yaml index cc88feed937..16792f24cb1 100644 --- a/internal/xds/translator/testdata/out/xds-ir/load-balancer.clusters.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/load-balancer.clusters.yaml @@ -157,3 +157,20 @@ outlierDetection: {} perConnectionBufferLimitBytes: 32768 type: EDS +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: tenth-route-dest + lbPolicy: MAGLEV + name: tenth-route-dest + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + type: EDS diff --git a/internal/xds/translator/testdata/out/xds-ir/load-balancer.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/load-balancer.endpoints.yaml index 31d55310540..0295b1c7174 100644 --- a/internal/xds/translator/testdata/out/xds-ir/load-balancer.endpoints.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/load-balancer.endpoints.yaml @@ -106,3 +106,15 @@ loadBalancingWeight: 1 locality: region: ninth-route-dest/backend/0 +- clusterName: tenth-route-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: tenth-route-dest/backend/0 diff --git a/internal/xds/translator/testdata/out/xds-ir/load-balancer.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/load-balancer.routes.yaml index 8fcdb470f81..07bf30df034 100644 --- a/internal/xds/translator/testdata/out/xds-ir/load-balancer.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/load-balancer.routes.yaml @@ -77,3 +77,16 @@ name: test upgradeConfigs: - upgradeType: websocket + - match: + prefix: / + name: tenth-route + route: + cluster: tenth-route-dest + hashPolicy: + - cookie: + attributes: + - name: foo + value: bar + name: test + upgradeConfigs: + - upgradeType: websocket diff --git a/internal/xds/translator/testdata/out/xds-ir/tracing-endpoint-stats.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/tracing-endpoint-stats.routes.yaml index d4a7fa5ae20..b214e8b05a3 100644 --- a/internal/xds/translator/testdata/out/xds-ir/tracing-endpoint-stats.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/tracing-endpoint-stats.routes.yaml @@ -6,8 +6,6 @@ name: first-listener/* routes: - directResponse: - body: - inlineString: 'Unknown custom filter type: UnsupportedType' status: 500 match: prefix: / diff --git a/internal/xds/translator/testdata/out/xds-ir/tracing-zipkin.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/tracing-zipkin.routes.yaml index d4a7fa5ae20..b214e8b05a3 100644 --- a/internal/xds/translator/testdata/out/xds-ir/tracing-zipkin.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/tracing-zipkin.routes.yaml @@ -6,8 +6,6 @@ name: first-listener/* routes: - directResponse: - body: - inlineString: 'Unknown custom filter type: UnsupportedType' status: 500 match: prefix: / diff --git a/internal/xds/translator/testdata/out/xds-ir/tracing.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/tracing.routes.yaml index d4a7fa5ae20..b214e8b05a3 100644 --- a/internal/xds/translator/testdata/out/xds-ir/tracing.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/tracing.routes.yaml @@ -6,8 +6,6 @@ name: first-listener/* routes: - directResponse: - body: - inlineString: 'Unknown custom filter type: UnsupportedType' status: 500 match: prefix: / diff --git a/internal/xds/translator/translator.go b/internal/xds/translator/translator.go index abd4942fd31..a58903e88ad 100644 --- a/internal/xds/translator/translator.go +++ b/internal/xds/translator/translator.go @@ -320,16 +320,17 @@ func (t *Translator) processHTTPListenerXdsTranslation( } } - // add http route client certs - for _, route := range httpListener.Routes { - if route.Destination != nil { - for _, st := range route.Destination.Settings { - if st.TLS != nil { - for _, cert := range st.TLS.ClientCertificates { - secret := buildXdsTLSCertSecret(cert) - if err := tCtx.AddXdsResource(resourcev3.SecretType, secret); err != nil { - errs = errors.Join(errs, err) - } + } + + // add http route client certs + for _, route := range httpListener.Routes { + if route.Destination != nil { + for _, st := range route.Destination.Settings { + if st.TLS != nil { + for _, cert := range st.TLS.ClientCertificates { + secret := buildXdsTLSCertSecret(cert) + if err := tCtx.AddXdsResource(resourcev3.SecretType, secret); err != nil { + errs = errors.Join(errs, err) } } } diff --git a/internal/xds/translator/wasm.go b/internal/xds/translator/wasm.go index 01d7411c6a2..4d6434c95d3 100644 --- a/internal/xds/translator/wasm.go +++ b/internal/xds/translator/wasm.go @@ -53,7 +53,7 @@ func (*wasm) patchHCM(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTTPListe if !routeContainsWasm(route) { continue } - for _, ep := range route.Wasms { + for _, ep := range route.EnvoyExtensions.Wasms { if hcmContainsFilter(mgr, wasmFilterName(ep)) { continue } @@ -161,7 +161,7 @@ func routeContainsWasm(irRoute *ir.HTTPRoute) bool { return false } - return len(irRoute.Wasms) > 0 + return irRoute.EnvoyExtensions != nil && len(irRoute.EnvoyExtensions.Wasms) > 0 } // patchResources patches the cluster resources for the http wasm code source. @@ -181,8 +181,11 @@ func (*wasm) patchRoute(route *routev3.Route, irRoute *ir.HTTPRoute) error { if irRoute == nil { return errors.New("ir route is nil") } + if irRoute.EnvoyExtensions == nil { + return nil + } - for _, ep := range irRoute.Wasms { + for _, ep := range irRoute.EnvoyExtensions.Wasms { filterName := wasmFilterName(ep) if err := enableFilterOnRoute(route, filterName); err != nil { return err diff --git a/release-notes/v0.2.0-rc1.yaml b/release-notes/v0.2.0-rc1.yaml index d9b58f4c377..e1b199f735e 100644 --- a/release-notes/v0.2.0-rc1.yaml +++ b/release-notes/v0.2.0-rc1.yaml @@ -8,7 +8,7 @@ changes: change: | Added the EnvoyGateway API type for configuring Envoy Gateway. Added the EnvoyProxy API type for configuring managed Envoys. -- area: ci +- area: CI change: | Added tooling to build, run, etc. Envoy Gateway. - area: providers @@ -25,10 +25,10 @@ changes: change: | Added the gatewayapi translator to translate Gateway API and associated resources to the IR and manage Gateway API status. -- area: message-service +- area: message service change: | Added infra and xds IR watchable map messages for inter-component communication. Added a Runner to each component to support pub/sub between components. -- area: infra-manager +- area: infra manager change: | Added Kubernetes Infra Manager to manage Envoy infrastructure running in a Kubernetes cluster. diff --git a/release-notes/v0.2.0-rc2.yaml b/release-notes/v0.2.0-rc2.yaml index fa2db13da10..a6d2792cb1c 100644 --- a/release-notes/v0.2.0-rc2.yaml +++ b/release-notes/v0.2.0-rc2.yaml @@ -6,7 +6,7 @@ changes: Updated and expanded developer documentation. Added `kube-demo` target to demonstrate Envoy Gateway functionality. Added developer debugging documentation. - - area: ci + - area: CI change: | Added Gateway API conformance tests. - area: providers @@ -23,9 +23,9 @@ changes: Expanded support for Gateway API status. Added support for request modifier and redirect filters. Added support to return 500 responses for invalid backends. - - area: message-service + - area: message service change: | Updated IRs to support managing multiple Envoy fleets. - - area: infra-manager + - area: infra manager change: | Separate Envoy infrastructure is created per Gateway. diff --git a/release-notes/v0.2.0.yaml b/release-notes/v0.2.0.yaml index cecac2f7693..00b148c0353 100644 --- a/release-notes/v0.2.0.yaml +++ b/release-notes/v0.2.0.yaml @@ -11,7 +11,7 @@ changes: change: | Added the EnvoyGateway API type for configuring Envoy Gateway. Added the EnvoyProxy API type for configuring managed Envoys. - - area: ci-tooling-testing + - area: CI tooling testing change: | Added tooling to build, run, etc. Envoy Gateway. Added Gateway API conformance tests. diff --git a/release-notes/v0.3.0-rc.1.yaml b/release-notes/v0.3.0-rc.1.yaml index 7cde6d6d67e..6a7ec57cc31 100644 --- a/release-notes/v0.3.0-rc.1.yaml +++ b/release-notes/v0.3.0-rc.1.yaml @@ -23,7 +23,7 @@ changes: Added Support for Routes ReferenceGrant Added Support for Namespace Server Config Type - - area: ci-tooling-testing + - area: CI tooling testing change: | Fixes Make Image Failed in Darwin Fixes Wait for Job Succeeded before conformance test diff --git a/release-notes/v0.3.0.yaml b/release-notes/v0.3.0.yaml index 53a5d2c171b..2948fe141b4 100644 --- a/release-notes/v0.3.0.yaml +++ b/release-notes/v0.3.0.yaml @@ -34,7 +34,7 @@ changes: Added Support for Namespace Server Config Type Added initial management of Envoy Proxy deployment via EnvoyProxy API - - area: ci-tooling-testing + - area: CI tooling testing change: | Fixed Make Image Failed in Darwin Fixed Wait for Job Succeeded before conformance test diff --git a/release-notes/v0.4.0-rc.1.yaml b/release-notes/v0.4.0-rc.1.yaml index d0ad17c2420..5194d2ee6ac 100644 --- a/release-notes/v0.4.0-rc.1.yaml +++ b/release-notes/v0.4.0-rc.1.yaml @@ -23,7 +23,7 @@ changes: Added Custom Envoy Gateway Extensions Framework Added Support for Service Method Match in GRPCRoute - - area: ci-tooling-testing + - area: CI tooling testing change: | Fixed CI Flakes During Helm Install Added Test To Ensure Static xDS Cluster Has Same Field Values as Dynamic Cluster diff --git a/release-notes/v0.4.0.yaml b/release-notes/v0.4.0.yaml index d05676beaa2..c4a85267e21 100644 --- a/release-notes/v0.4.0.yaml +++ b/release-notes/v0.4.0.yaml @@ -26,7 +26,7 @@ changes: Added Support for Service Method Match in GRPCRoute Fixed a Bug in the Extension Hooks for xDS Virtual Hosts and Routes - - area: ci-tooling-testing + - area: CI tooling testing change: | Fixed CI Flakes During Helm Install Added Test To Ensure Static xDS Cluster Has Same Field Values as Dynamic Cluster diff --git a/release-notes/v0.5.0-rc.1.yaml b/release-notes/v0.5.0-rc.1.yaml index 52462de8926..d99359603e8 100644 --- a/release-notes/v0.5.0-rc.1.yaml +++ b/release-notes/v0.5.0-rc.1.yaml @@ -31,7 +31,7 @@ changes: breaking-change: | Renamed field in EnvoyGateway API from Extension to ExtensionManager - - area: ci-tooling-testing + - area: CI tooling testing change: | Added Retest Github Action Added CherryPick Github Action diff --git a/release-notes/v0.5.0.yaml b/release-notes/v0.5.0.yaml index 52462de8926..d99359603e8 100644 --- a/release-notes/v0.5.0.yaml +++ b/release-notes/v0.5.0.yaml @@ -31,7 +31,7 @@ changes: breaking-change: | Renamed field in EnvoyGateway API from Extension to ExtensionManager - - area: ci-tooling-testing + - area: CI tooling testing change: | Added Retest Github Action Added CherryPick Github Action diff --git a/release-notes/v0.6.0-rc.1.yaml b/release-notes/v0.6.0-rc.1.yaml index 6e7809c6bb0..8466eb28dbd 100644 --- a/release-notes/v0.6.0-rc.1.yaml +++ b/release-notes/v0.6.0-rc.1.yaml @@ -36,7 +36,7 @@ changes: Updated the Bootstrap field within the EnvoyProxy CRD with an additional value field to specify bootstrap config - - area: ci-tooling-testing + - area: CI tooling testing change: | - area: conformance diff --git a/release-notes/v1.0.0.yaml b/release-notes/v1.0.0.yaml index 2444e87604b..cb0ed75a834 100644 --- a/release-notes/v1.0.0.yaml +++ b/release-notes/v1.0.0.yaml @@ -1,4 +1,4 @@ -date: Nov 1, 2023 +date: March 13, 2024 changes: - area: documentation diff --git a/release-notes/v1.1.0-rc.1.yaml b/release-notes/v1.1.0-rc.1.yaml index 77009689224..f7ed991997b 100644 --- a/release-notes/v1.1.0-rc.1.yaml +++ b/release-notes/v1.1.0-rc.1.yaml @@ -123,13 +123,23 @@ changes: Added Support for Loadbalancing Header Hash Policy in BackendTrafficPolicy CRD Added Support for Cluster Connection Buffer Size Limit in BackendTrafficPolicy Added Support for more Rate Limit Rules in BackendTrafficPolicy CRD - Added Support for WASM extension in EnvoyExtensionPolicy CRD + Added Support for Wasm extension in EnvoyExtensionPolicy CRD Added Support for External Processing extension in EnvoyExtensionPolicy CRD Removed Status Print Column from xPolicy CRDs breaking-change: | Gateway-API BackendTLSPolicy v1alpha3 is incompatible with previous versions of the CRD + xPolicy targetRefs can no longer specify a namespace, since Gateway-API v1.1.0 uses LocalPolicyTargetReferenceWithSectionName in Policy resources + + + deprecation: | + xPolicy targetRef is deprecated, use targetRefs instead + SecurityPolicy ExtAuth BackendRef is deprecated, use BackendRefs instead + OpenTelemetry Proxy Access Log Host and Port are deprecated, use backendRefs instead + OpenTelemetry Proxy Metrics Sink Host and Port are deprecated, use backendRefs instead + Proxy Tracing Provider Host and Port are deprecated, use backendRefs instead + Envoy Gateway Extension Server Host and Port are deprecated, use BackendEndpoint instead - area: conformance @@ -145,7 +155,7 @@ changes: Added e2e test for CEL Access Log Filter Added e2e test for GRPC Access Log Service Sink Added e2e test for XDS Metadata - Added e2e test for WASM from OCI Images and HTTP Source + Added e2e test for Wasm from OCI Images and HTTP Source Added e2e test for Service IP Routing Added e2e test for Multiple GatewayClasses Added e2e test for HTTP Full Path rewrite @@ -181,7 +191,7 @@ changes: Added Support for PolicyStatus in EnvoyPatchPolicy Added Support for Websocket upgrades in HTTP/1 Routes Added Support for custom controller name in egctl - Added Support for BackendTLSPoplicy CA Certificate reference to Secret + Added Support for BackendTLSPolicy CA Certificate reference to Secret Added names to Filter Chains Added Support extension server hooks for TCP and UDP listeners Added Support for attaching EnvoyProxy resource to Gateways diff --git a/release-notes/v1.1.0.yaml b/release-notes/v1.1.0.yaml new file mode 100644 index 00000000000..bad41982112 --- /dev/null +++ b/release-notes/v1.1.0.yaml @@ -0,0 +1,252 @@ +date: July 22, 2024 + +changes: + - area: documentation + change: | + Added Concepts Doc + Added User Guide for Wasm Extension + Added User Guide for patching Envoy Service + Added User Guide for Backend MTLS + Added User Guide for Backend TLS Parameters + Added User Guide for IP Allowlist/Denylist + Added User Guide for Extension Server + Added User Guide for building Wasm image + Added Performance Benchmarking Document + Added User Guide for Zipkin Tracing + Added User Guide for Customizing Ordering of Filters + Added User Guide for External Processing Filter in EnvoyExtensionPolicy + Added User Guide for installation of egctl with brew + Added User Guide for Client Buffer Size Limit + Added User Guide for Client Idle Timeout + Added Chinese translation for release notes, roadmap, installation, development, contribution and several User Guides + Added User Guide for Backend resource + Added GA Blog Post + Added Threat Model + Added Adopters section to docs + Added User Guide and Dashboards for Control Plane and Resource Observability + Added User Guide for Connection Limits in ClientTrafficPolicy + Added User Guide on using Private Key Provider + Added Design Doc for Authorization + Added Design Doc for XDS Metadata + Added Design Doc for Backend resource + Added Design Doc for Control Plane Observability + Added Design Doc for EnvoyExtensionPolicy + Added Design Doc for External Processing in EnvoyExtensionPolicy + Updated Access Logging User Guide to include filtering with CEL Expression + Updated Access Logging User Guide to include Metadata + Updated Development Guide to require Golang 1.22 + Updated Quickstart User Guide to fetch GATEWAY_HOST from Gateway resource + Updated Site to reflect GA status + Updated HTTP Redirect User Guide to not set a redirect port or require a BackendRef + Updated Observability User Guides to use gateway-addons-helm + Updated Gateway-API User Guide to reflect support for BackendRef filters + Updated HTTP Timeouts User Guide to highlight default Envoy timeouts + Updated Installation Guide to use server-side apply + Updated Installation Guide to refer to values.yaml docs + Updated BackendTLSPolicy User Guide to GW-API v1.1.0 + Updated User Guides to use tabs when applying yaml from file or stdin + Updated OIDC User Guide to use HTTPS redirect URLs + Updated Order of versions in Site + Updated Extensbility User Gudie to use yaml-format patches + Updated Quickstart Guide to include next steps + Updated CRD docs to include enum values + Updated Extensibility User Guide with Envoy Patch Policy examples + Updated structure of docs: rename Guides to Tasks, move Contribution + Updated Support Matrix + Updated egctl x status docs for xRoute and xPolicy + Updated egctl User Guide with Install and Uninstall commands + Updated GRPCRoute docs to use v1 instead of v1alpha2 + Fixed Rate Limiting User Guide to use correct CIDR matcher type names + Fixed User Guide for JWT-based routing + Fixed JSON Access Log Example + Use linkinator to detect dead links in docs + Use helm-docs to generate chart docs + Support Not-Implemented-Hide marker in API docs + + + - area: installation + change: | + Added startupProbe to all provisioned containers to reduce risk of restart + Added new gateway-addons-helm chart for Observability + Added support for global image settings for all images in Envoy Gateway helm chart + Added Support for PodDistruptionBudget for Envoy Gateway + Added Support for TopologySpreadConstraints for Envoy Gateway + Added Support for Tolerations for Envoy Gateway + Added Support for Ratelimit image pull secrets and pull policy + Updated ttlSecondsAfterFinished on certgen job to 30 by default + Updated Envoy Gateway ImagePullPolicy to IfNotPresent released charts + Remove envoy-gateway-metrics-service and merge its contents into envoy-gateway service + + + - area: api + change: | + Added Support for Gateway-API v1.1.0 + Added new Backend CRD + Added new EnvoyExtensionPolicy CRD + Added Support for Plural Target Refs and Target Selectors in xPolicy CRDs + Added Support for Backend CRD BackendRefs in HTTPRoute, GRPCRoute and EnvoyExtensionPolicy CRDs + Added Support for Custom Extension Server Policy CRDs in EnvoyGateway Config + Added Support for Custom ShutDownManager Image in EnvoyGateway Config + Added Support for Leader Election in EnvoyGateway Config + Added Support for Connecting to Extension Server over Unix Domain Socket in EnvoyGateway Config + Added Support for Proxy PodDisruptionBudget in EnvoyProxy CRD + Added Support for Running Envoy Proxy as a Daemonset in EnvoyProxy CRD + Added Support for Proxy Loadbalancer Source Ranges in EnvoyProxy CRD + Added Support for Proxy Prometheus Metrics Compression in EnvoyProxy CRD + Added Support for BackendRefs in Access Log, Metric and Trace Sinks in EnvoyProxy CRD + Added Support for Rate Limiting Tracing in EnvoyProxy CRD + Added Support for Routing to Service IP in EnvoyProxy CRD + Added Support for Access Log CEL filters in EnvoyProxy CRD + Added Support for Access Log Formatters for File and OpenTelemetry in EnvoyProxy CRD + Added Support for Zipkin Tracing in EnvoyProxy CRD + Added Support for using the Listener port as a the Container port in EnvoyProxy CRD + Added Support for OpenTelemtry Sink Export Settings in EnvoyProxy CRD + Added Support for Backend Client Certificate Authentication in EnvoyProxy CRD + Added Support for Backend TLS Settings in EnvoyProxy CRD + Added Support for HTTP Filter Ordering in EnvoyProxy CRD + Added Support for gRPC Access Log Service (ALS) Sink in EnvoyProxy CRD + Added Support for OpenTelelemetry Sinks as a BackendRef in EnvoyProxy CRD + Added Support for User-Provided name for generate Kubernetes resources in EnvoyProxy CRD + Added Support for Per-Endpoint stats in EnvoyProxy CRD + Added Support for Targeting SectionNames in ClientTrafficPolicy CRD + Added Support for Preserving X-Request-ID header in ClientTrafficPolicy CRD + Added Support for Using Downstream Protocol in Upstream connections in ClientTrafficPolicy CRD + Added Support for HTTP/2 settings in ClientTrafficPolicy CRD + Added Support for Connection Buffer Size Limit in ClientTrafficPolicy CRD + Added Support for HTTP Health Check in ClientTrafficPolicy CRD + Added Support for Optionally requiring a Client Certificate in ClientTrafficPolicy CRD + Added Support for Headers with Underscores CRD in ClientTrafficPolicy CRD + Added Support for XFCC header processing in ClientTrafficPolicy CRD + Added Support for TCP Listener Idle Timeout in ClientTrafficPolicy CRD + Added Support for IdleTimeout in ClientTrafficPolicy CRD + Added Support for Connection Limits in ClientTrafficPolicy CRD + Added Support for additional OIDC settings related to Resource, Token and Cookie in SecurityPolicy CRD + Added Support for Optionally requiring a JWT in SecurityPolicy CRD + Added Support for BackendRefs for Ext-Auth in SecurityPolicy CRD + Added Support for Authorization in SecurityPolicy CRD + Added Support for Ext-Auth failOpen in SecurityPolicy CRD + Added Support for Loadbalancer Cookie Consistent Hashing in BackendTrafficPolicy CRD + Added Support for Disabling X-RateLimit headers in BackendTrafficPolicy CRD + Added Support for Connection Buffer Size Limit in BackendTrafficPolicy CRD + Added Support for Loadbalancing Consistent Hash Table Size in BackendTrafficPolicy CRD + Added Support for Loadbalancing Header Hash Policy in BackendTrafficPolicy CRD + Added Support for Cluster Connection Buffer Size Limit in BackendTrafficPolicy + Added Support for more Rate Limit Rules in BackendTrafficPolicy CRD + Added Support for Wasm extension in EnvoyExtensionPolicy CRD + Added Support for External Processing extension in EnvoyExtensionPolicy CRD + Removed Status Print Column from xPolicy CRDs + + + breaking-change: | + SecurityPolicy translation failures will now cause routes referenced by the policy to return an immediate 500 response + Gateway-API BackendTLSPolicy v1alpha3 is incompatible with previous versions of the CRD + xPolicy targetRefs can no longer specify a namespace, since Gateway-API v1.1.0 uses LocalPolicyTargetReferenceWithSectionName in Policy resources + + + deprecation: | + xPolicy targetRef is deprecated, use targetRefs instead + SecurityPolicy ExtAuth BackendRef is deprecated, use BackendRefs instead + OpenTelemetry Proxy Access Log Host and Port are deprecated, use backendRefs instead + OpenTelemetry Proxy Metrics Sink Host and Port are deprecated, use backendRefs instead + Proxy Tracing Provider Host and Port are deprecated, use backendRefs instead + Envoy Gateway Extension Server Host and Port are deprecated, use BackendEndpoint instead + + + - area: conformance + change: | + Added Supported Features to Gateway Class + + + - area: testing + change: | + Added e2e test for Client MTLS + Added e2e test for Load Balancing + Added performance benchmarking test + Added e2e test for Zipking Tracing + Added e2e test for HTTP Health Checks + Added e2e test for CEL Access Log Filter + Added e2e test for GRPC Access Log Service Sink + Added e2e test for XDS Metadata + Added e2e test for Wasm from OCI Images and HTTP Source + Added e2e test for Service IP Routing + Added e2e test for Multiple GatewayClasses + Added e2e test for HTTP Full Path rewrite + Added e2e test for Backend API + Added e2e test for Backend TLS Settings + Added e2e test for disabling X-RateLimit Headers + Added e2e test for Authorization + Added e2e test for BackendRefs in Ext-Auth + Added e2e test for Using Client Protocol in Upstream Connection + Added e2e test for Backend Client Cert Authentication + Added e2e test for External Processing Filter + Added e2e test for Merge Gateways Feature + Added e2e test for Option JWT authentication + Added e2e test for Infrastructure using Server-Side Apply + Added e2e test for Connection Limits + Added e2e test for Envoy Graceful Shutdown + Updated e2e test for Limit to cover multiple listeners + Updated e2e test for CORS to not require access-control-expose-headers + Run CEL tests on all supported K8s versions + Added OSV Scanner for Golang Vulnerabilities and Licenses + Added Trivy scanner for Docker images + + + - area: translator + change: | + Added Support for BackendRef HTTP Filters + Added Support for attaching EnvoyProxy to Gateways + Added Support for cross-namespace EnvoyProxy reference from GatewayClass + Added Support for Backend Traffic Policy for UDPRoute and TCPRoute + Added Support for ClientTrafficPolicy for UDPRoute and TCPRoute + Added Support for multiple BackendRefs in TCPRoute and UDPRoute + Added Metrics related to XDS Server, Infra Manager and Controller + Added Support for PolicyStatus in EnvoyPatchPolicy + Added Support for Websocket upgrades in HTTP/1 Routes + Added Support for custom controller name in egctl + Added Support for BackendTLSPolicy CA Certificate reference to Secret + Added names to Filter Chains + Added Support extension server hooks for TCP and UDP listeners + Added Support for attaching EnvoyProxy resource to Gateways + Added Support for Exposing Prometheus Port in Rate Limiter Service + Added Support for Optional Rate Limit Backend Redis + Updated OAuth2 filter to preserve Authorization header if OIDC token forwarding is enabled + Updated Default Filter Order to have Fault filter first in the HTTP Filter Chain + Updated Ext-Auth Per-Route config to use filter-specific Config Type + Updated Overload Manager configuration according to Envoy recommendations by default + Updated Infrastructure resource management to user Server-Side Apply + Updated Reflection of Errors in Gateway Status when too many addresses are assigned + Fixed enforcement of same-namespace for BackendTLSPolicy and target + Fixed processing all listeners before returning with an error + Fixed creation of infrastructure resources if there are no listeners + Fixed use GatewayClass Name for Observability if Merge Gateways is enabled + Fixed CORS to not forward Not-Matching Preflights to Backends + Fixed BackendTLSPolicy status to fully conform with PolicyStatus + Fixed duplication of Ext-Auth, OIDC and Basic Auth Filters + Fixed Proxy Protocol Filter to always be the first Listener Filter + Fixed Translation Consistency by sorting Gateways + Fixed QUIC Listener to only Advertise HTTP/3 over ALPN + Fixed SNI matching for TCP Routes with TLS termination + Fixed Reconciliation when EnvoyProxy backendRefs changes + Fixed Reconciliation when a referenced Secret or ConfigMap changes + Fixed ReplaceFullPath not working for root path + Fixed Default Application Protocol to TCP for Zipkin Tracing + Fixed not appending well-known ports (80, 443) in rediret Location header + + - area: providers + change: | + Bumped K8s Client to v0.30.0 + + + - area: xds + change: | + Bumped go-control-plane to v0.12.1 + + + - area: cli + change: | + Added egctl x collect command + Added Support for Install and Uninstall commands to egctl + Added Support for xRoute and xPolicy in egctl x status + Added Golang version to Envoy Gateway version command + Fixed egctl x status gatewayclass example message + diff --git a/site/content/en/_index.md b/site/content/en/_index.md index 529b133e670..d3979ee492b 100644 --- a/site/content/en/_index.md +++ b/site/content/en/_index.md @@ -3,7 +3,7 @@ title: Envoy Gateway --- {{< blocks/cover title="Welcome to Envoy Gateway!" image_anchor="top" height="full" >}} - + GET STARTED diff --git a/site/content/en/contributions/DEVELOP.md b/site/content/en/contributions/DEVELOP.md index 555bf3296cc..c953048a4aa 100644 --- a/site/content/en/contributions/DEVELOP.md +++ b/site/content/en/contributions/DEVELOP.md @@ -155,18 +155,11 @@ The performance and scalability concerns come from several aspects for control-p - The consumption of memory and CPU. - The rate of configuration changes. -The benchmark test is running on [Kind][Kind] cluster, you can run the following command -to start a Kind cluster and run benchmark test on it. +The benchmark test is running on a [Kind][Kind] cluster, you can start a Kind cluster and +run benchmark test on it by executing `make benchmark-test`. -```shell -make benchmark-test -``` - -By default, a benchmark report will be generated under `test/benchmark` after test finished. - -#### Brief benchmark report - -You can browse the detailed benchmark report in `test/benchmark/benchmark-report.md`. +The benchmark report will be included in the release artifacts, you can learn more by downloading +the detailed benchmark report, namely `benchmark_report.zip`. Here are some brief benchmark reports about Envoy Gateway: diff --git a/site/content/en/contributions/RELEASING.md b/site/content/en/contributions/RELEASING.md index 76211b61e1a..de480420cc1 100644 --- a/site/content/en/contributions/RELEASING.md +++ b/site/content/en/contributions/RELEASING.md @@ -7,7 +7,6 @@ This document guides maintainers through the process of creating an Envoy Gatewa - [Release Candidate](#release-candidate) - [Prerequisites](#prerequisites) - - [Setup cherry picker action](#setup-cherry-picker-action) - [Minor Release](#minor-release) - [Prerequisites](#prerequisites-1) - [Announce the Release](#announce-the-release) @@ -74,37 +73,6 @@ export GITHUB_REMOTE=origin 18. Ensure you check the "This is a pre-release" checkbox when editing the GitHub release. 19. If you find any bugs in this process, please create an issue. -### Setup cherry picker action - -After release branch cut, RM (Release Manager) should add job [cherrypick action](https://github.com/envoyproxy/gateway/blob/main/.github/workflows/cherrypick.yaml) for target release. - -Configuration looks like following: - -```yaml - cherry_pick_release_v0_4: - runs-on: ubuntu-latest - name: Cherry pick into release-v0.4 - if: ${{ contains(github.event.pull_request.labels.*.name, 'cherrypick/release-v0.4') && github.event.pull_request.merged == true }} - steps: - - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - with: - fetch-depth: 0 - - name: Cherry pick into release/v0.4 - uses: carloscastrojumo/github-cherry-pick-action@a145da1b8142e752d3cbc11aaaa46a535690f0c5 # v1.0.9 - with: - branch: release/v0.4 - title: "[release/v0.4] {old_title}" - body: "Cherry picking #{old_pull_request_id} onto release/v0.4" - labels: | - cherrypick/release-v0.4 - # put release manager here - reviewers: | - AliceProxy -``` - -Replace `v0.4` with real branch name, and `AliceProxy` with the real name of RM. - ## Minor Release The following steps should be used for creating a minor release. diff --git a/site/content/en/contributions/design/config-api.md b/site/content/en/contributions/design/config-api.md index 1c6f3057848..89b7b0d838a 100644 --- a/site/content/en/contributions/design/config-api.md +++ b/site/content/en/contributions/design/config-api.md @@ -88,7 +88,7 @@ type Gateway struct { // defaults to "gateway.envoyproxy.io/gatewayclass-controller". See the following // for additional details: // - // https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.GatewayClass + // https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.GatewayClass // // +optional ControllerName string `json:"controllerName,omitempty"` diff --git a/site/content/en/contributions/design/envoy-extension-policy.md b/site/content/en/contributions/design/envoy-extension-policy.md index 4bda1eb85ab..67af7cd6ef1 100644 --- a/site/content/en/contributions/design/envoy-extension-policy.md +++ b/site/content/en/contributions/design/envoy-extension-policy.md @@ -113,22 +113,22 @@ spec: ## Features / API Fields Here is a list of features that can be included in this API * Network Filters: - * WASM + * Wasm * Golang * HTTP Filters: * External Processing * Lua - * WASM + * Wasm * Golang ## Design Decisions * This API will only support a single `targetRef` and can bind to a `Gateway` resource or a `HTTPRoute` or `GRPCRoute` or `TCPRoute`. -* Extensions that support both Network and HTTP filter variants (e.g. WASM, Golang) will be translated to the appropriate filter type according to the sort of route that they attach to. +* Extensions that support both Network and HTTP filter variants (e.g. Wasm, Golang) will be translated to the appropriate filter type according to the sort of route that they attach to. * Extensions that only support HTTP extensibility (Ext-Proc, LUA) can only be attached to HTTP/GRPC Routes. * A user-defined extension that is added to the request processing flow can have a significant impact on security, resilience and performance of the proxy. Gateway Operators can restrict access to the extensibility policy using K8s RBAC. * Users may need to customize the order of extension and built-in filters. This will be addressed in a separate issue. -* Gateway operators may need to include multiple extensions (e.g. WASM modules developed by different teams and distributed separately). +* Gateway operators may need to include multiple extensions (e.g. Wasm modules developed by different teams and distributed separately). This API will support attachment of multiple policies. Extension will execute in an order defined by the priority field. * This API resource MUST be part of same namespace as the targetRef resource * If the policy targets a resource but cannot attach to it, this information should be reflected diff --git a/site/content/en/contributions/design/extending-envoy-gateway.md b/site/content/en/contributions/design/extending-envoy-gateway.md index f7d3f3675c4..f3a970ef6ae 100644 --- a/site/content/en/contributions/design/extending-envoy-gateway.md +++ b/site/content/en/contributions/design/extending-envoy-gateway.md @@ -53,6 +53,10 @@ An example configuration: apiVersion: gateway.envoyproxy.io/v1alpha1 kind: EnvoyGateway extensionManager: + poliyResources: + - group: example.myextension.io + version: v1alpha1 + kind: ListenerPolicyKind resources: - group: example.myextension.io version: v2 @@ -65,15 +69,16 @@ extensionManager: - HTTPListener - Translation service: - host: my-extension.example - port: 443 + fqdn: + hostname: my-extension.example + port: 443 tls: certificateRef: name: my-secret namespace: default ``` -An extension must supply connection information in the `extension.service` field so that Envoy Gateway can communicate with the extension. The `tls` configuration is optional. +An extension must supply connection information in the `extension.service` field so that Envoy Gateway can communicate with the extension. The `tls` configuration is optional. Envoy Gateway supports connecting to an extension server either with TCP or with Unix Domain Sockets as a transport layer. If the extension wants Envoy Gateway to watch resources for it then the extension must configure the optional `extension.resources` field and supply a list of: @@ -81,6 +86,15 @@ If the extension wants Envoy Gateway to watch resources for it then the extensio - `version`: the API version of the resource - `kind`: the Kind of resource +If the extension wants Envoy Gateway to watch for policy resources then it must configure the optional `extensions.policyResources` field and supply a list of + +- `group`: the API group of the resource +- `version`: the API version of the resource +- `kind`: the Kind of resource + +Policy resources, like all Gateway-API policies, must contain `targetRef` or `targetRefs` fields in the spec which allow Envoy Gateway to identify which resources are targeted by the policy. +Policies can currently only target `Gateway` resources, and are provided as context to calls to the `HTTPListener` hook. + The extension can configure the `extensionManager.hooks` field to specify which hook points it would like to support. If a given hook is not listed here then it will not be executed even if the extension is configured properly. This allows extension developers to only opt-in to the hook points they want to make use of. @@ -93,8 +107,8 @@ Envoy Gateway manages [Envoy][] deployments, which act as the data plane that ha Gateway converts into [Envoy specific configuration (xDS)][] to send over to Envoy. Gateway API offers [ExtensionRef filters][] and [Policy Attachments][] as extension points for implementers to use. Envoy Gateway extends the Gateway API using these extension points to provide support for [rate limiting][] -and [authentication][] native to the project. The initial design of Envoy Gateway extensions will primarily focus on ExtensionRef filters so that extension developers can reference their own resources as HTTP Filters in the same way -that Envoy Gateway has native support for rate limiting and authentication filters. +and [authentication][] native to the project. The initial design of Envoy Gateway extensions will primarily focus on `ExtensionRef` filters so that extension developers can reference their own resources as HTTP Filters in the same way +that Envoy Gateway has native support for rate limiting and authentication filters, as well as policy resources which can target `Gateway`s. When Envoy Gateway encounters an [HTTPRoute][] or [GRPCRoute][] that has an `ExtensionRef` `filter` with a `group` and `kind` that Envoy Gateway does not support, it will first check the registered extension to determine if it supports the referenced object before considering it a configuration error. @@ -144,10 +158,11 @@ and provide an error to the user. the namespace of any `ExtensionRef` is the same as the namespace of the `HTTPRoute` or `GRPCRoute` it is attached to rather than treating the `name` field of an `ExtensionRef` as a `name.namespace` string. If Gateway API adds support for these fields then the design of the Envoy Gateway extensions will be updated to support them. +Similarly, any registered policy resource that targets an `HTTPListener` will be sent to the `HTTPListener` hook as context. + ## Watching New Resources -Envoy Gateway will dynamically create new watches on resources introduced by the registered Extension. It does so by using the [controller-runtime][] to create new watches on [Unstructured][] resources that match the `version`s, `group`s, and `kind`s that the -registered extension configured. When communicating with an extension, Envoy Gateway sends these Unstructured resources over to the extension. This eliminates the need for the extension to create its own watches which would have a strong chance of creating race conditions and reconciliation loops when resources change. When an extension receives the Unstructured resources from Envoy Gateway it can perform its own type validation on them. Currently we make the simplifying assumption that the registered extension's `Kinds` are filters referenced by `extensionRef` in `HTTPRouteFilter`s . Support for Policy attachments will be introduced at a later time. +Envoy Gateway will dynamically create new watches on resources introduced by the registered Extension. It does so by using the [controller-runtime][] to create new watches on [Unstructured][] resources that match the `version`s, `group`s, and `kind`s that the registered extension configured. When communicating with an extension, Envoy Gateway sends these Unstructured resources over to the extension. This eliminates the need for the extension to create its own watches which would have a strong chance of creating race conditions and reconciliation loops when resources change. When an extension receives the Unstructured resources from Envoy Gateway it can perform its own type validation on them. Currently we make the simplifying assumption that the registered extension's `Kinds` are filters referenced by `extensionRef` in `HTTPRouteFilter`s . Policy attachments which target `Gateway` resources work in the same way. ## xDS Hooks API @@ -236,7 +251,11 @@ message PostHTTPListenerModifyRequest { // Empty for now but we can add fields to the context as use-cases are discovered without // breaking any clients that use the API // additional context information can be added to this message as more use-cases are discovered -message PostHTTPListenerExtensionContext {} +message PostHTTPListenerExtensionContext { + // Resources introduced by the extension that were used as extension server + // policies targeting the listener + repeated ExtensionResource extension_resources = 1; +} // PostHTTPListenerModifyResponse is the expected response from an extension and contains a modified version of the Listener that was sent // If an extension returns a nil Listener then it will not be modified @@ -302,7 +321,7 @@ whenever Envoy Gateway creates an instance of Envoy Proxy. An extension develope ## Known Challenges -Extending Envoy Gateway by using an external extension server which makes use of hook points in Envoy Gateway does comes with a few trade-offs. One known trade-off is the impact of the time that it takes for the hook calls to be executed. Since an extension would make use of hook points in Envoy Gateway that use gRPC for communication, the time it takes to perform these requests could become a concern for some extension developers. One way to minimize the request time of the hook calls is to load the extension server as a sidecar to Envoy Gateway to minimize the impact of networking on the hook calls. +Extending Envoy Gateway by using an external extension server which makes use of hook points in Envoy Gateway does comes with a few trade-offs. One known trade-off is the impact of the time that it takes for the hook calls to be executed. Since an extension would make use of hook points in Envoy Gateway that use gRPC for communication, the time it takes to perform these requests could become a concern for some extension developers. One way to minimize the request time of the hook calls is to load the extension server as a sidecar to Envoy Gateway using the Unix Local Domain transport to minimize the impact of networking on the hook calls. [official goals]: https://github.com/envoyproxy/gateway/blob/main/GOALS.md#extensibility [ExtensionRef filters]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.LocalObjectReference diff --git a/site/content/en/contributions/design/wasm-extension.md b/site/content/en/contributions/design/wasm-extension.md new file mode 100644 index 00000000000..8700fcecbde --- /dev/null +++ b/site/content/en/contributions/design/wasm-extension.md @@ -0,0 +1,84 @@ +--- +title: "Wasm OCI Image Support" +--- + +## Motivation +Envoy Gateway (EG) should support Wasm OCI image as a remote wasm code source. +This feature will allow users to deploy Wasm modules from OCI registries, such as Docker Hub, +Google Container Registry, and Amazon Elastic Container Registry, to Envoy proxies managed by EG. +Deploying Wasm modules from OCI registries has several benefits: +* **Versioning**: Users can use the tag feature of the OCI image to manage the version of the Wasm module. +* **Security**: Users can use private registries to store the Wasm module. +* **Distribution**: Users can use the existing distribution mechanism of the OCI registry to distribute the Wasm module. + +## Goals +* Define the system components needed to support Wasm OCI images as remote Wasm code sources. + +## Architecture + +![](/img/wasm-extension.png) + +### Control Plane Wasm File Cache + +Envoy lacks native OCI image support, therefore, EG needs to download Wasm modules from their original OIC registries, +cache them locally in the file system, and serve them to Envoy over HTTP. + +#### HTTP Code Source + +For HTTP code source, we have two options: serve Wasm modules directly from their +original HTTP URLs, or cache them in EG (as with OCI images). Caching both the HTTP Wasm modules and OCI +images inside EG can make UI consistent, for example, sha256sum can be calculated on the EG side and made +optional in the API. This will also make the Envoy proxy side more efficient as it won’t have to download the +Wasm module from the original URL every time, which can be slow. + +#### Resource Consumption + +Memory: Since we cache Wasm modules in the file system, we can optimize the memory usage +of the cache and the HTTP server to avoid introducing too much memory consumption by this feature. +For example, when receiving a Wasm pulling request form the Envoy, we can open the Wasm file and write +the file content directly to response, and then close the file. There won’t be significant memory consumption involved. + +Disk: Though it's possible to mount a volume to the container for the Wasm file cache, the current implementation just +stores the Wasm files in the EG container’s file system. The disk space consumed by the cache is limited to 1GB by default, +it can be made configurable in the future. + +#### Caching Mechanism + +Cached files will be evicted based on LRU(Last recently used)algorithm. +If the image’s tag is latest, then they will be updated periodically. The cache clean and update periods +will be configurable. + +#### Restrict Access to Private Images + +* **Client Authn with mTLS:** To prevent unauthorized proxies from accessing the Wasm modules, the communication between + the Envoy and EG will be secured using mTLS. +* **User Authn with Registry Credentials:** To prevent unauthorized users from accessing the Wasm modules, the user who + creates the EEP must have the appropriate permissions to access the OCI registry. For example, if two users create EEPs + in different namespaces (ns1, ns2) accessing the same OCI image, each must also create a unique secret with registry + credentials (secret1 for user1 in ns1, secret2 for user2 in ns2) and provide it in the EEP configuration. EG will validate + the provided secret against the OCI registry before serving the Wasm module to the target HTTPRoute/Gateway of that EEP. +* **Unguessable Download URLs:** It's possible that users who have no permission to access a private OCI image could create + an EnvoyPatchPolicy to bypass the EG permission check. For example, a user could create an EPP, inject a Wasm filter, + and put the download URL of a private OCI image in the Wasm filter configuration. To prevent this, we need to make the + download URL unguessable. The download URL will be generated by EG and will be a random string that is impossible to + guess. If a user can get the config dump the Envoy Proxy, they can still get the download URL. However, they won’t be + able to do so without the permission to access the config dump of Envoy Proxy, which is a more restricted permission (usually Admin role). + +## Alternative Considered + +### Inline Bytes + +EG downloads the Wasm modules, caches them in memory, and pushes the Wasm code through xDS as inline bytes. +This could inflate the xDS, potentially causing memory issues for EG and Envoy. + +### Data Plane Agent +Mount Wasm modules in the local file system of the Envoy container. We’ll need an agent deployed in the +same pod as the Envoy for this. It would be too expensive to implement this as we’ll need to intercept +the xDS at the agent. + +### Standalone Wasm HTTP Server +Deploying the Wasm HTTP server as a standalone service. While this has no obvious benefits, it increases +operational costs. + +### Wait for Envoy OCI image support +We could wait indefinitely for Envoy to support OCI imag as a remote wasm code source. diff --git a/site/content/en/v1.0.2/_index.md b/site/content/en/docs/_index.md similarity index 100% rename from site/content/en/v1.0.2/_index.md rename to site/content/en/docs/_index.md diff --git a/site/content/en/v0.3.0/api/_index.md b/site/content/en/docs/api/_index.md similarity index 100% rename from site/content/en/v0.3.0/api/_index.md rename to site/content/en/docs/api/_index.md diff --git a/site/content/en/docs/api/extension_types.md b/site/content/en/docs/api/extension_types.md new file mode 100644 index 00000000000..4dc4ccd890c --- /dev/null +++ b/site/content/en/docs/api/extension_types.md @@ -0,0 +1,3861 @@ ++++ +title = "API Reference" ++++ + + +## Packages +- [gateway.envoyproxy.io/v1alpha1](#gatewayenvoyproxyiov1alpha1) + + +## gateway.envoyproxy.io/v1alpha1 + +Package v1alpha1 contains API schema definitions for the gateway.envoyproxy.io +API group. + + +### Resource Types +- [Backend](#backend) +- [BackendList](#backendlist) +- [BackendTrafficPolicy](#backendtrafficpolicy) +- [BackendTrafficPolicyList](#backendtrafficpolicylist) +- [ClientTrafficPolicy](#clienttrafficpolicy) +- [ClientTrafficPolicyList](#clienttrafficpolicylist) +- [EnvoyExtensionPolicy](#envoyextensionpolicy) +- [EnvoyExtensionPolicyList](#envoyextensionpolicylist) +- [EnvoyGateway](#envoygateway) +- [EnvoyPatchPolicy](#envoypatchpolicy) +- [EnvoyPatchPolicyList](#envoypatchpolicylist) +- [EnvoyProxy](#envoyproxy) +- [SecurityPolicy](#securitypolicy) +- [SecurityPolicyList](#securitypolicylist) + + + +#### ALPNProtocol + +_Underlying type:_ _string_ + +ALPNProtocol specifies the protocol to be negotiated using ALPN + +_Appears in:_ +- [BackendTLSConfig](#backendtlsconfig) +- [ClientTLSSettings](#clienttlssettings) +- [TLSSettings](#tlssettings) + +| Value | Description | +| ----- | ----------- | +| `http/1.0` | HTTPProtocolVersion1_0 specifies that HTTP/1.0 should be negotiable with ALPN
| +| `http/1.1` | HTTPProtocolVersion1_1 specifies that HTTP/1.1 should be negotiable with ALPN
| +| `h2` | HTTPProtocolVersion2 specifies that HTTP/2 should be negotiable with ALPN
| + + +#### ALSEnvoyProxyAccessLog + + + +ALSEnvoyProxyAccessLog defines the gRPC Access Log Service (ALS) sink. +The service must implement the Envoy gRPC Access Log Service streaming API: +https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/accesslog/v3/als.proto +Access log format information is passed in the form of gRPC metadata when the +stream is established. Specifically, the following metadata is passed: + + +- `x-accesslog-text` - The access log format string when a Text format is used. +- `x-accesslog-attr` - JSON encoded key/value pairs when a JSON format is used. + +_Appears in:_ +- [ProxyAccessLogSink](#proxyaccesslogsink) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `backendRefs` | _[BackendRef](#backendref) array_ | true | BackendRefs references a Kubernetes object that represents the gRPC service to which
the access logs will be sent. Currently only Service is supported. | +| `logName` | _string_ | false | LogName defines the friendly name of the access log to be returned in
StreamAccessLogsMessage.Identifier. This allows the access log server
to differentiate between different access logs coming from the same Envoy. | +| `type` | _[ALSEnvoyProxyAccessLogType](#alsenvoyproxyaccesslogtype)_ | true | Type defines the type of accesslog. Supported types are "HTTP" and "TCP". | +| `http` | _[ALSEnvoyProxyHTTPAccessLogConfig](#alsenvoyproxyhttpaccesslogconfig)_ | false | HTTP defines additional configuration specific to HTTP access logs. | + + +#### ALSEnvoyProxyAccessLogType + +_Underlying type:_ _string_ + + + +_Appears in:_ +- [ALSEnvoyProxyAccessLog](#alsenvoyproxyaccesslog) + +| Value | Description | +| ----- | ----------- | +| `HTTP` | ALSEnvoyProxyAccessLogTypeHTTP defines the HTTP access log type and will populate StreamAccessLogsMessage.http_logs.
| +| `TCP` | ALSEnvoyProxyAccessLogTypeTCP defines the TCP access log type and will populate StreamAccessLogsMessage.tcp_logs.
| + + +#### ALSEnvoyProxyHTTPAccessLogConfig + + + + + +_Appears in:_ +- [ALSEnvoyProxyAccessLog](#alsenvoyproxyaccesslog) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `requestHeaders` | _string array_ | false | RequestHeaders defines request headers to include in log entries sent to the access log service. | +| `responseHeaders` | _string array_ | false | ResponseHeaders defines response headers to include in log entries sent to the access log service. | +| `responseTrailers` | _string array_ | false | ResponseTrailers defines response trailers to include in log entries sent to the access log service. | + + +#### ActiveHealthCheck + + + +ActiveHealthCheck defines the active health check configuration. +EG supports various types of active health checking including HTTP, TCP. + +_Appears in:_ +- [HealthCheck](#healthcheck) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `timeout` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | false | Timeout defines the time to wait for a health check response. | +| `interval` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | false | Interval defines the time between active health checks. | +| `unhealthyThreshold` | _integer_ | false | UnhealthyThreshold defines the number of unhealthy health checks required before a backend host is marked unhealthy. | +| `healthyThreshold` | _integer_ | false | HealthyThreshold defines the number of healthy health checks required before a backend host is marked healthy. | +| `type` | _[ActiveHealthCheckerType](#activehealthcheckertype)_ | true | Type defines the type of health checker. | +| `http` | _[HTTPActiveHealthChecker](#httpactivehealthchecker)_ | false | HTTP defines the configuration of http health checker.
It's required while the health checker type is HTTP. | +| `tcp` | _[TCPActiveHealthChecker](#tcpactivehealthchecker)_ | false | TCP defines the configuration of tcp health checker.
It's required while the health checker type is TCP. | + + +#### ActiveHealthCheckPayload + + + +ActiveHealthCheckPayload defines the encoding of the payload bytes in the payload. + +_Appears in:_ +- [HTTPActiveHealthChecker](#httpactivehealthchecker) +- [TCPActiveHealthChecker](#tcpactivehealthchecker) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[ActiveHealthCheckPayloadType](#activehealthcheckpayloadtype)_ | true | Type defines the type of the payload. | +| `text` | _string_ | false | Text payload in plain text. | +| `binary` | _integer array_ | false | Binary payload base64 encoded. | + + +#### ActiveHealthCheckPayloadType + +_Underlying type:_ _string_ + +ActiveHealthCheckPayloadType is the type of the payload. + +_Appears in:_ +- [ActiveHealthCheckPayload](#activehealthcheckpayload) + +| Value | Description | +| ----- | ----------- | +| `Text` | ActiveHealthCheckPayloadTypeText defines the Text type payload.
| +| `Binary` | ActiveHealthCheckPayloadTypeBinary defines the Binary type payload.
| + + +#### ActiveHealthCheckerType + +_Underlying type:_ _string_ + +ActiveHealthCheckerType is the type of health checker. + +_Appears in:_ +- [ActiveHealthCheck](#activehealthcheck) + +| Value | Description | +| ----- | ----------- | +| `HTTP` | ActiveHealthCheckerTypeHTTP defines the HTTP type of health checking.
| +| `TCP` | ActiveHealthCheckerTypeTCP defines the TCP type of health checking.
| + + +#### AppProtocolType + +_Underlying type:_ _string_ + +AppProtocolType defines various backend applications protocols supported by Envoy Gateway + +_Appears in:_ +- [BackendSpec](#backendspec) + +| Value | Description | +| ----- | ----------- | +| `gateway.envoyproxy.io/h2c` | AppProtocolTypeH2C defines the HTTP/2 application protocol.
| +| `gateway.envoyproxy.io/ws` | AppProtocolTypeWS defines the WebSocket over HTTP protocol.
| +| `gateway.envoyproxy.io/wss` | AppProtocolTypeWSS defines the WebSocket over HTTPS protocol.
| + + +#### Authorization + + + +Authorization defines the authorization configuration. + + +Note: if neither `Rules` nor `DefaultAction` is specified, the default action is to deny all requests. + +_Appears in:_ +- [SecurityPolicySpec](#securitypolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `rules` | _[AuthorizationRule](#authorizationrule) array_ | false | Rules defines a list of authorization rules.
These rules are evaluated in order, the first matching rule will be applied,
and the rest will be skipped.

For example, if there are two rules: the first rule allows the request
and the second rule denies it, when a request matches both rules, it will be allowed. | +| `defaultAction` | _[AuthorizationAction](#authorizationaction)_ | false | DefaultAction defines the default action to be taken if no rules match.
If not specified, the default action is Deny. | + + +#### AuthorizationAction + +_Underlying type:_ _string_ + +AuthorizationAction defines the action to be taken if a rule matches. + +_Appears in:_ +- [Authorization](#authorization) +- [AuthorizationRule](#authorizationrule) + +| Value | Description | +| ----- | ----------- | +| `Allow` | AuthorizationActionAllow is the action to allow the request.
| +| `Deny` | AuthorizationActionDeny is the action to deny the request.
| + + +#### AuthorizationRule + + + +AuthorizationRule defines a single authorization rule. + +_Appears in:_ +- [Authorization](#authorization) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `name` | _string_ | false | Name is a user-friendly name for the rule.
If not specified, Envoy Gateway will generate a unique name for the rule.n | +| `action` | _[AuthorizationAction](#authorizationaction)_ | true | Action defines the action to be taken if the rule matches. | +| `principal` | _[Principal](#principal)_ | true | Principal specifies the client identity of a request. | + + +#### BackOffPolicy + + + + + +_Appears in:_ +- [PerRetryPolicy](#perretrypolicy) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `baseInterval` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | true | BaseInterval is the base interval between retries. | +| `maxInterval` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | false | MaxInterval is the maximum interval between retries. This parameter is optional, but must be greater than or equal to the base_interval if set.
The default is 10 times the base_interval | + + +#### Backend + + + +Backend allows the user to configure the endpoints of a backend and +the behavior of the connection from Envoy Proxy to the backend. + +_Appears in:_ +- [BackendList](#backendlist) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `apiVersion` | _string_ | |`gateway.envoyproxy.io/v1alpha1` +| `kind` | _string_ | |`Backend` +| `metadata` | _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#objectmeta-v1-meta)_ | true | Refer to Kubernetes API documentation for fields of `metadata`. | +| `spec` | _[BackendSpec](#backendspec)_ | true | Spec defines the desired state of Backend. | +| `status` | _[BackendStatus](#backendstatus)_ | true | Status defines the current status of Backend. | + + + + + + +#### BackendConnection + + + +BackendConnection allows users to configure connection-level settings of backend + +_Appears in:_ +- [BackendTrafficPolicySpec](#backendtrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `bufferLimit` | _[Quantity](#quantity)_ | false | BufferLimit Soft limit on size of the cluster’s connections read and write buffers.
If unspecified, an implementation defined default is applied (32768 bytes).
For example, 20Mi, 1Gi, 256Ki etc.
Note: that when the suffix is not provided, the value is interpreted as bytes. | + + +#### BackendEndpoint + + + +BackendEndpoint describes a backend endpoint, which can be either a fully-qualified domain name, IP address or unix domain socket +corresponding to Envoy's Address: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/address.proto#config-core-v3-address + +_Appears in:_ +- [BackendSpec](#backendspec) +- [ExtensionService](#extensionservice) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `fqdn` | _[FQDNEndpoint](#fqdnendpoint)_ | false | FQDN defines a FQDN endpoint | +| `ip` | _[IPEndpoint](#ipendpoint)_ | false | IP defines an IP endpoint. Currently, only IPv4 Addresses are supported. | +| `unix` | _[UnixSocket](#unixsocket)_ | false | Unix defines the unix domain socket endpoint | + + +#### BackendList + + + +BackendList contains a list of Backend resources. + + + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `apiVersion` | _string_ | |`gateway.envoyproxy.io/v1alpha1` +| `kind` | _string_ | |`BackendList` +| `metadata` | _[ListMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#listmeta-v1-meta)_ | true | Refer to Kubernetes API documentation for fields of `metadata`. | +| `items` | _[Backend](#backend) array_ | true | | + + +#### BackendRef + + + +BackendRef defines how an ObjectReference that is specific to BackendRef. + +_Appears in:_ +- [ALSEnvoyProxyAccessLog](#alsenvoyproxyaccesslog) +- [ExtProc](#extproc) +- [GRPCExtAuthService](#grpcextauthservice) +- [HTTPExtAuthService](#httpextauthservice) +- [OpenTelemetryEnvoyProxyAccessLog](#opentelemetryenvoyproxyaccesslog) +- [ProxyOpenTelemetrySink](#proxyopentelemetrysink) +- [TracingProvider](#tracingprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `group` | _[Group](#group)_ | false | Group is the group of the referent. For example, "gateway.networking.k8s.io".
When unspecified or empty string, core API group is inferred. | +| `kind` | _[Kind](#kind)_ | false | Kind is the Kubernetes resource kind of the referent. For example
"Service".

Defaults to "Service" when not specified.

ExternalName services can refer to CNAME DNS records that may live
outside of the cluster and as such are difficult to reason about in
terms of conformance. They also may not be safe to forward to (see
CVE-2021-25740 for more information). Implementations SHOULD NOT
support ExternalName Services.

Support: Core (Services with a type other than ExternalName)

Support: Implementation-specific (Services with type ExternalName) | +| `name` | _[ObjectName](#objectname)_ | true | Name is the name of the referent. | +| `namespace` | _[Namespace](#namespace)_ | false | Namespace is the namespace of the backend. When unspecified, the local
namespace is inferred.

Note that when a namespace different than the local namespace is specified,
a ReferenceGrant object is required in the referent namespace to allow that
namespace's owner to accept the reference. See the ReferenceGrant
documentation for details.

Support: Core | +| `port` | _[PortNumber](#portnumber)_ | false | Port specifies the destination port number to use for this resource.
Port is required when the referent is a Kubernetes Service. In this
case, the port number is the service port number, not the target port.
For other resources, destination port might be derived from the referent
resource or this field. | + + +#### BackendSpec + + + +BackendSpec describes the desired state of BackendSpec. + +_Appears in:_ +- [Backend](#backend) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `endpoints` | _[BackendEndpoint](#backendendpoint) array_ | true | Endpoints defines the endpoints to be used when connecting to the backend. | +| `appProtocols` | _[AppProtocolType](#appprotocoltype) array_ | false | AppProtocols defines the application protocols to be supported when connecting to the backend. | + + +#### BackendStatus + + + +BackendStatus defines the state of Backend + +_Appears in:_ +- [Backend](#backend) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `conditions` | _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#condition-v1-meta) array_ | false | Conditions describe the current conditions of the Backend. | + + +#### BackendTLSConfig + + + +BackendTLSConfig describes the BackendTLS configuration for Envoy Proxy. + +_Appears in:_ +- [EnvoyProxySpec](#envoyproxyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `clientCertificateRef` | _[SecretObjectReference](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.SecretObjectReference)_ | false | ClientCertificateRef defines the reference to a Kubernetes Secret that contains
the client certificate and private key for Envoy to use when connecting to
backend services and external services, such as ExtAuth, ALS, OpenTelemetry, etc.
This secret should be located within the same namespace as the Envoy proxy resource that references it. | +| `minVersion` | _[TLSVersion](#tlsversion)_ | false | Min specifies the minimal TLS protocol version to allow.
The default is TLS 1.2 if this is not specified. | +| `maxVersion` | _[TLSVersion](#tlsversion)_ | false | Max specifies the maximal TLS protocol version to allow
The default is TLS 1.3 if this is not specified. | +| `ciphers` | _string array_ | false | Ciphers specifies the set of cipher suites supported when
negotiating TLS 1.0 - 1.2. This setting has no effect for TLS 1.3.
In non-FIPS Envoy Proxy builds the default cipher 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
In builds using BoringSSL FIPS the default cipher list is:
- ECDHE-ECDSA-AES128-GCM-SHA256
- ECDHE-RSA-AES128-GCM-SHA256
- ECDHE-ECDSA-AES256-GCM-SHA384
- ECDHE-RSA-AES256-GCM-SHA384 | +| `ecdhCurves` | _string array_ | false | ECDHCurves specifies the set of supported ECDH curves.
In non-FIPS Envoy Proxy builds the default curves are:
- X25519
- P-256
In builds using BoringSSL FIPS the default curve is:
- P-256 | +| `signatureAlgorithms` | _string array_ | false | SignatureAlgorithms specifies which signature algorithms the listener should
support. | +| `alpnProtocols` | _[ALPNProtocol](#alpnprotocol) array_ | false | ALPNProtocols supplies the list of ALPN protocols that should be
exposed by the listener. By default h2 and http/1.1 are enabled.
Supported values are:
- http/1.0
- http/1.1
- h2 | + + +#### BackendTrafficPolicy + + + +BackendTrafficPolicy allows the user to configure the behavior of the connection +between the Envoy Proxy listener and the backend service. + +_Appears in:_ +- [BackendTrafficPolicyList](#backendtrafficpolicylist) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `apiVersion` | _string_ | |`gateway.envoyproxy.io/v1alpha1` +| `kind` | _string_ | |`BackendTrafficPolicy` +| `metadata` | _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#objectmeta-v1-meta)_ | true | Refer to Kubernetes API documentation for fields of `metadata`. | +| `spec` | _[BackendTrafficPolicySpec](#backendtrafficpolicyspec)_ | true | spec defines the desired state of BackendTrafficPolicy. | +| `status` | _[PolicyStatus](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.PolicyStatus)_ | true | status defines the current status of BackendTrafficPolicy. | + + + + +#### BackendTrafficPolicyList + + + +BackendTrafficPolicyList contains a list of BackendTrafficPolicy resources. + + + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `apiVersion` | _string_ | |`gateway.envoyproxy.io/v1alpha1` +| `kind` | _string_ | |`BackendTrafficPolicyList` +| `metadata` | _[ListMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#listmeta-v1-meta)_ | true | Refer to Kubernetes API documentation for fields of `metadata`. | +| `items` | _[BackendTrafficPolicy](#backendtrafficpolicy) array_ | true | | + + +#### BackendTrafficPolicySpec + + + +BackendTrafficPolicySpec defines the desired state of BackendTrafficPolicy. + +_Appears in:_ +- [BackendTrafficPolicy](#backendtrafficpolicy) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `targetRef` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName)_ | true | TargetRef is the name of the resource this policy is being attached to.
This policy and the TargetRef MUST be in the same namespace for this
Policy to have effect

Deprecated: use targetRefs/targetSelectors instead | +| `targetRefs` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName) array_ | true | TargetRefs are the names of the Gateway resources this policy
is being attached to. | +| `targetSelectors` | _[TargetSelector](#targetselector) array_ | true | TargetSelectors allow targeting resources for this policy based on labels | +| `rateLimit` | _[RateLimitSpec](#ratelimitspec)_ | false | RateLimit allows the user to limit the number of incoming requests
to a predefined value based on attributes within the traffic flow. | +| `loadBalancer` | _[LoadBalancer](#loadbalancer)_ | false | LoadBalancer policy to apply when routing traffic from the gateway to
the backend endpoints | +| `proxyProtocol` | _[ProxyProtocol](#proxyprotocol)_ | false | ProxyProtocol enables the Proxy Protocol when communicating with the backend. | +| `tcpKeepalive` | _[TCPKeepalive](#tcpkeepalive)_ | false | TcpKeepalive settings associated with the upstream client connection.
Disabled by default. | +| `healthCheck` | _[HealthCheck](#healthcheck)_ | false | HealthCheck allows gateway to perform active health checking on backends. | +| `faultInjection` | _[FaultInjection](#faultinjection)_ | false | FaultInjection defines the fault injection policy to be applied. This configuration can be used to
inject delays and abort requests to mimic failure scenarios such as service failures and overloads | +| `circuitBreaker` | _[CircuitBreaker](#circuitbreaker)_ | false | Circuit Breaker settings for the upstream connections and requests.
If not set, circuit breakers will be enabled with the default thresholds | +| `retry` | _[Retry](#retry)_ | false | Retry provides more advanced usage, allowing users to customize the number of retries, retry fallback strategy, and retry triggering conditions.
If not set, retry will be disabled. | +| `useClientProtocol` | _boolean_ | false | UseClientProtocol configures Envoy to prefer sending requests to backends using
the same HTTP protocol that the incoming request used. Defaults to false, which means
that Envoy will use the protocol indicated by the attached BackendRef. | +| `timeout` | _[Timeout](#timeout)_ | false | Timeout settings for the backend connections. | +| `connection` | _[BackendConnection](#backendconnection)_ | false | Connection includes backend connection settings. | + + +#### BasicAuth + + + +BasicAuth defines the configuration for the HTTP Basic Authentication. + +_Appears in:_ +- [SecurityPolicySpec](#securitypolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `users` | _[SecretObjectReference](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.SecretObjectReference)_ | true | The Kubernetes secret which contains the username-password pairs in
htpasswd format, used to verify user credentials in the "Authorization"
header.

This is an Opaque secret. The username-password pairs should be stored in
the key ".htpasswd". As the key name indicates, the value needs to be the
htpasswd format, for example: "user1:{SHA}hashed_user1_password".
Right now, only SHA hash algorithm is supported.
Reference to https://httpd.apache.org/docs/2.4/programs/htpasswd.html
for more details.

Note: The secret must be in the same namespace as the SecurityPolicy. | + + +#### BootstrapType + +_Underlying type:_ _string_ + +BootstrapType defines the types of bootstrap supported by Envoy Gateway. + +_Appears in:_ +- [ProxyBootstrap](#proxybootstrap) + +| Value | Description | +| ----- | ----------- | +| `Merge` | Merge merges the provided bootstrap with the default one. The provided bootstrap can add or override a value
within a map, or add a new value to a list.
Please note that the provided bootstrap can't override a value within a list.
| +| `Replace` | Replace replaces the default bootstrap with the provided one.
| + + +#### CIDR + +_Underlying type:_ _string_ + +CIDR defines a CIDR Address range. +A CIDR can be an IPv4 address range such as "192.168.1.0/24" or an IPv6 address range such as "2001:0db8:11a3:09d7::/64". + +_Appears in:_ +- [Principal](#principal) + + + +#### CORS + + + +CORS defines the configuration for Cross-Origin Resource Sharing (CORS). + +_Appears in:_ +- [SecurityPolicySpec](#securitypolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `allowOrigins` | _[Origin](#origin) array_ | true | AllowOrigins defines the origins that are allowed to make requests. | +| `allowMethods` | _string array_ | true | AllowMethods defines the methods that are allowed to make requests. | +| `allowHeaders` | _string array_ | true | AllowHeaders defines the headers that are allowed to be sent with requests. | +| `exposeHeaders` | _string array_ | true | ExposeHeaders defines the headers that can be exposed in the responses. | +| `maxAge` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | true | MaxAge defines how long the results of a preflight request can be cached. | +| `allowCredentials` | _boolean_ | true | AllowCredentials indicates whether a request can include user credentials
like cookies, authentication headers, or TLS client certificates. | + + +#### CircuitBreaker + + + +CircuitBreaker defines the Circuit Breaker configuration. + +_Appears in:_ +- [BackendTrafficPolicySpec](#backendtrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `maxConnections` | _integer_ | false | The maximum number of connections that Envoy will establish to the referenced backend defined within a xRoute rule. | +| `maxPendingRequests` | _integer_ | false | The maximum number of pending requests that Envoy will queue to the referenced backend defined within a xRoute rule. | +| `maxParallelRequests` | _integer_ | false | The maximum number of parallel requests that Envoy will make to the referenced backend defined within a xRoute rule. | +| `maxParallelRetries` | _integer_ | false | The maximum number of parallel retries that Envoy will make to the referenced backend defined within a xRoute rule. | +| `maxRequestsPerConnection` | _integer_ | false | The maximum number of requests that Envoy will make over a single connection to the referenced backend defined within a xRoute rule.
Default: unlimited. | + + +#### ClaimToHeader + + + +ClaimToHeader defines a configuration to convert JWT claims into HTTP headers + +_Appears in:_ +- [JWTProvider](#jwtprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `header` | _string_ | true | Header defines the name of the HTTP request header that the JWT Claim will be saved into. | +| `claim` | _string_ | true | Claim is the JWT Claim that should be saved into the header : it can be a nested claim of type
(eg. "claim.nested.key", "sub"). The nested claim name must use dot "."
to separate the JSON name path. | + + +#### ClientConnection + + + +ClientConnection allows users to configure connection-level settings of client + +_Appears in:_ +- [ClientTrafficPolicySpec](#clienttrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `connectionLimit` | _[ConnectionLimit](#connectionlimit)_ | false | ConnectionLimit defines limits related to connections | +| `bufferLimit` | _[Quantity](#quantity)_ | false | BufferLimit provides configuration for the maximum buffer size in bytes for each incoming connection.
For example, 20Mi, 1Gi, 256Ki etc.
Note that when the suffix is not provided, the value is interpreted as bytes.
Default: 32768 bytes. | + + +#### ClientIPDetectionSettings + + + +ClientIPDetectionSettings provides configuration for determining the original client IP address for requests. + +_Appears in:_ +- [ClientTrafficPolicySpec](#clienttrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `xForwardedFor` | _[XForwardedForSettings](#xforwardedforsettings)_ | false | XForwardedForSettings provides configuration for using X-Forwarded-For headers for determining the client IP address. | +| `customHeader` | _[CustomHeaderExtensionSettings](#customheaderextensionsettings)_ | false | CustomHeader provides configuration for determining the client IP address for a request based on
a trusted custom HTTP header. This uses the custom_header original IP detection extension.
Refer to https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/http/original_ip_detection/custom_header/v3/custom_header.proto
for more details. | + + +#### ClientTLSSettings + + + + + +_Appears in:_ +- [ClientTrafficPolicySpec](#clienttrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `clientValidation` | _[ClientValidationContext](#clientvalidationcontext)_ | false | ClientValidation specifies the configuration to validate the client
initiating the TLS connection to the Gateway listener. | +| `minVersion` | _[TLSVersion](#tlsversion)_ | false | Min specifies the minimal TLS protocol version to allow.
The default is TLS 1.2 if this is not specified. | +| `maxVersion` | _[TLSVersion](#tlsversion)_ | false | Max specifies the maximal TLS protocol version to allow
The default is TLS 1.3 if this is not specified. | +| `ciphers` | _string array_ | false | Ciphers specifies the set of cipher suites supported when
negotiating TLS 1.0 - 1.2. This setting has no effect for TLS 1.3.
In non-FIPS Envoy Proxy builds the default cipher 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
In builds using BoringSSL FIPS the default cipher list is:
- ECDHE-ECDSA-AES128-GCM-SHA256
- ECDHE-RSA-AES128-GCM-SHA256
- ECDHE-ECDSA-AES256-GCM-SHA384
- ECDHE-RSA-AES256-GCM-SHA384 | +| `ecdhCurves` | _string array_ | false | ECDHCurves specifies the set of supported ECDH curves.
In non-FIPS Envoy Proxy builds the default curves are:
- X25519
- P-256
In builds using BoringSSL FIPS the default curve is:
- P-256 | +| `signatureAlgorithms` | _string array_ | false | SignatureAlgorithms specifies which signature algorithms the listener should
support. | +| `alpnProtocols` | _[ALPNProtocol](#alpnprotocol) array_ | false | ALPNProtocols supplies the list of ALPN protocols that should be
exposed by the listener. By default h2 and http/1.1 are enabled.
Supported values are:
- http/1.0
- http/1.1
- h2 | + + +#### ClientTimeout + + + + + +_Appears in:_ +- [ClientTrafficPolicySpec](#clienttrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `tcp` | _[TCPClientTimeout](#tcpclienttimeout)_ | false | Timeout settings for TCP. | +| `http` | _[HTTPClientTimeout](#httpclienttimeout)_ | false | Timeout settings for HTTP. | + + +#### ClientTrafficPolicy + + + +ClientTrafficPolicy allows the user to configure the behavior of the connection +between the downstream client and Envoy Proxy listener. + +_Appears in:_ +- [ClientTrafficPolicyList](#clienttrafficpolicylist) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `apiVersion` | _string_ | |`gateway.envoyproxy.io/v1alpha1` +| `kind` | _string_ | |`ClientTrafficPolicy` +| `metadata` | _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#objectmeta-v1-meta)_ | true | Refer to Kubernetes API documentation for fields of `metadata`. | +| `spec` | _[ClientTrafficPolicySpec](#clienttrafficpolicyspec)_ | true | Spec defines the desired state of ClientTrafficPolicy. | +| `status` | _[PolicyStatus](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.PolicyStatus)_ | true | Status defines the current status of ClientTrafficPolicy. | + + +#### ClientTrafficPolicyList + + + +ClientTrafficPolicyList contains a list of ClientTrafficPolicy resources. + + + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `apiVersion` | _string_ | |`gateway.envoyproxy.io/v1alpha1` +| `kind` | _string_ | |`ClientTrafficPolicyList` +| `metadata` | _[ListMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#listmeta-v1-meta)_ | true | Refer to Kubernetes API documentation for fields of `metadata`. | +| `items` | _[ClientTrafficPolicy](#clienttrafficpolicy) array_ | true | | + + +#### ClientTrafficPolicySpec + + + +ClientTrafficPolicySpec defines the desired state of ClientTrafficPolicy. + +_Appears in:_ +- [ClientTrafficPolicy](#clienttrafficpolicy) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `targetRef` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName)_ | true | TargetRef is the name of the resource this policy is being attached to.
This policy and the TargetRef MUST be in the same namespace for this
Policy to have effect

Deprecated: use targetRefs/targetSelectors instead | +| `targetRefs` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName) array_ | true | TargetRefs are the names of the Gateway resources this policy
is being attached to. | +| `targetSelectors` | _[TargetSelector](#targetselector) array_ | true | TargetSelectors allow targeting resources for this policy based on labels | +| `tcpKeepalive` | _[TCPKeepalive](#tcpkeepalive)_ | false | TcpKeepalive settings associated with the downstream client connection.
If defined, sets SO_KEEPALIVE on the listener socket to enable TCP Keepalives.
Disabled by default. | +| `enableProxyProtocol` | _boolean_ | false | EnableProxyProtocol interprets the ProxyProtocol header and adds the
Client Address into the X-Forwarded-For header.
Note Proxy Protocol must be present when this field is set, else the connection
is closed. | +| `clientIPDetection` | _[ClientIPDetectionSettings](#clientipdetectionsettings)_ | false | ClientIPDetectionSettings provides configuration for determining the original client IP address for requests. | +| `tls` | _[ClientTLSSettings](#clienttlssettings)_ | false | TLS settings configure TLS termination settings with the downstream client. | +| `path` | _[PathSettings](#pathsettings)_ | false | Path enables managing how the incoming path set by clients can be normalized. | +| `headers` | _[HeaderSettings](#headersettings)_ | false | HeaderSettings provides configuration for header management. | +| `timeout` | _[ClientTimeout](#clienttimeout)_ | false | Timeout settings for the client connections. | +| `connection` | _[ClientConnection](#clientconnection)_ | false | Connection includes client connection settings. | +| `http1` | _[HTTP1Settings](#http1settings)_ | false | HTTP1 provides HTTP/1 configuration on the listener. | +| `http2` | _[HTTP2Settings](#http2settings)_ | false | HTTP2 provides HTTP/2 configuration on the listener. | +| `http3` | _[HTTP3Settings](#http3settings)_ | false | HTTP3 provides HTTP/3 configuration on the listener. | +| `healthCheck` | _[HealthCheckSettings](#healthchecksettings)_ | false | HealthCheck provides configuration for determining whether the HTTP/HTTPS listener is healthy. | + + +#### ClientValidationContext + + + +ClientValidationContext holds configuration that can be used to validate the client initiating the TLS connection +to the Gateway. +By default, no client specific configuration is validated. + +_Appears in:_ +- [ClientTLSSettings](#clienttlssettings) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `optional` | _boolean_ | false | Optional set to true accepts connections even when a client doesn't present a certificate.
Defaults to false, which rejects connections without a valid client certificate. | +| `caCertificateRefs` | _[SecretObjectReference](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.SecretObjectReference) array_ | false | CACertificateRefs contains one or more references to
Kubernetes objects that contain TLS certificates of
the Certificate Authorities that can be used
as a trust anchor to validate the certificates presented by the client.

A single reference to a Kubernetes ConfigMap or a Kubernetes Secret,
with the CA certificate in a key named `ca.crt` is currently supported.

References to a resource in different namespace are invalid UNLESS there
is a ReferenceGrant in the target namespace that allows the certificate
to be attached. | + + +#### Compression + + + +Compression defines the config of enabling compression. +This can help reduce the bandwidth at the expense of higher CPU. + +_Appears in:_ +- [BackendTrafficPolicySpec](#backendtrafficpolicyspec) +- [ProxyPrometheusProvider](#proxyprometheusprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[CompressorType](#compressortype)_ | true | CompressorType defines the compressor type to use for compression. | +| `gzip` | _[GzipCompressor](#gzipcompressor)_ | false | The configuration for GZIP compressor. | + + +#### CompressorType + +_Underlying type:_ _string_ + +CompressorType defines the types of compressor library supported by Envoy Gateway. + +_Appears in:_ +- [Compression](#compression) + + + +#### ConnectionLimit + + + + + +_Appears in:_ +- [ClientConnection](#clientconnection) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `value` | _integer_ | true | Value of the maximum concurrent connections limit.
When the limit is reached, incoming connections will be closed after the CloseDelay duration.
Default: unlimited. | +| `closeDelay` | _[Duration](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Duration)_ | false | CloseDelay defines the delay to use before closing connections that are rejected
once the limit value is reached.
Default: none. | + + +#### ConsistentHash + + + +ConsistentHash defines the configuration related to the consistent hash +load balancer policy. + +_Appears in:_ +- [LoadBalancer](#loadbalancer) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[ConsistentHashType](#consistenthashtype)_ | true | ConsistentHashType defines the type of input to hash on. Valid Type values are
"SourceIP",
"Header",
"Cookie". | +| `header` | _[Header](#header)_ | false | Header configures the header hash policy when the consistent hash type is set to Header. | +| `cookie` | _[Cookie](#cookie)_ | false | Cookie configures the cookie hash policy when the consistent hash type is set to Cookie. | +| `tableSize` | _integer_ | false | The table size for consistent hashing, must be prime number limited to 5000011. | + + +#### ConsistentHashType + +_Underlying type:_ _string_ + +ConsistentHashType defines the type of input to hash on. + +_Appears in:_ +- [ConsistentHash](#consistenthash) + +| Value | Description | +| ----- | ----------- | +| `SourceIP` | SourceIPConsistentHashType hashes based on the source IP address.
| +| `Header` | HeaderConsistentHashType hashes based on a request header.
| +| `Cookie` | CookieConsistentHashType hashes based on a cookie.
| + + +#### Cookie + + + +Cookie defines the cookie hashing configuration for consistent hash based +load balancing. + +_Appears in:_ +- [ConsistentHash](#consistenthash) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `name` | _string_ | true | Name of the cookie to hash.
If this cookie does not exist in the request, Envoy will generate a cookie and set
the TTL on the response back to the client based on Layer 4
attributes of the backend endpoint, to ensure that these future requests
go to the same backend endpoint. Make sure to set the TTL field for this case. | +| `ttl` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | false | TTL of the generated cookie if the cookie is not present. This value sets the
Max-Age attribute value. | +| `attributes` | _object (keys:string, values:string)_ | false | Additional Attributes to set for the generated cookie. | + + +#### CustomHeaderExtensionSettings + + + +CustomHeaderExtensionSettings provides configuration for determining the client IP address for a request based on +a trusted custom HTTP header. This uses the the custom_header original IP detection extension. +Refer to https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/http/original_ip_detection/custom_header/v3/custom_header.proto +for more details. + +_Appears in:_ +- [ClientIPDetectionSettings](#clientipdetectionsettings) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `name` | _string_ | true | Name of the header containing the original downstream remote address, if present. | +| `failClosed` | _boolean_ | false | FailClosed is a switch used to control the flow of traffic when client IP detection
fails. If set to true, the listener will respond with 403 Forbidden when the client
IP address cannot be determined. | + + +#### CustomTag + + + + + +_Appears in:_ +- [ProxyTracing](#proxytracing) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[CustomTagType](#customtagtype)_ | true | Type defines the type of custom tag. | +| `literal` | _[LiteralCustomTag](#literalcustomtag)_ | true | Literal adds hard-coded value to each span.
It's required when the type is "Literal". | +| `environment` | _[EnvironmentCustomTag](#environmentcustomtag)_ | true | Environment adds value from environment variable to each span.
It's required when the type is "Environment". | +| `requestHeader` | _[RequestHeaderCustomTag](#requestheadercustomtag)_ | true | RequestHeader adds value from request header to each span.
It's required when the type is "RequestHeader". | + + +#### CustomTagType + +_Underlying type:_ _string_ + + + +_Appears in:_ +- [CustomTag](#customtag) + +| Value | Description | +| ----- | ----------- | +| `Literal` | CustomTagTypeLiteral adds hard-coded value to each span.
| +| `Environment` | CustomTagTypeEnvironment adds value from environment variable to each span.
| +| `RequestHeader` | CustomTagTypeRequestHeader adds value from request header to each span.
| + + +#### EnvironmentCustomTag + + + +EnvironmentCustomTag adds value from environment variable to each span. + +_Appears in:_ +- [CustomTag](#customtag) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `name` | _string_ | true | Name defines the name of the environment variable which to extract the value from. | +| `defaultValue` | _string_ | false | DefaultValue defines the default value to use if the environment variable is not set. | + + +#### EnvoyExtensionPolicy + + + +EnvoyExtensionPolicy allows the user to configure various envoy extensibility options for the Gateway. + +_Appears in:_ +- [EnvoyExtensionPolicyList](#envoyextensionpolicylist) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `apiVersion` | _string_ | |`gateway.envoyproxy.io/v1alpha1` +| `kind` | _string_ | |`EnvoyExtensionPolicy` +| `metadata` | _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#objectmeta-v1-meta)_ | true | Refer to Kubernetes API documentation for fields of `metadata`. | +| `spec` | _[EnvoyExtensionPolicySpec](#envoyextensionpolicyspec)_ | true | Spec defines the desired state of EnvoyExtensionPolicy. | +| `status` | _[PolicyStatus](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.PolicyStatus)_ | true | Status defines the current status of EnvoyExtensionPolicy. | + + +#### EnvoyExtensionPolicyList + + + +EnvoyExtensionPolicyList contains a list of EnvoyExtensionPolicy resources. + + + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `apiVersion` | _string_ | |`gateway.envoyproxy.io/v1alpha1` +| `kind` | _string_ | |`EnvoyExtensionPolicyList` +| `metadata` | _[ListMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#listmeta-v1-meta)_ | true | Refer to Kubernetes API documentation for fields of `metadata`. | +| `items` | _[EnvoyExtensionPolicy](#envoyextensionpolicy) array_ | true | | + + +#### EnvoyExtensionPolicySpec + + + +EnvoyExtensionPolicySpec defines the desired state of EnvoyExtensionPolicy. + +_Appears in:_ +- [EnvoyExtensionPolicy](#envoyextensionpolicy) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `targetRef` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName)_ | true | TargetRef is the name of the resource this policy is being attached to.
This policy and the TargetRef MUST be in the same namespace for this
Policy to have effect

Deprecated: use targetRefs/targetSelectors instead | +| `targetRefs` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName) array_ | true | TargetRefs are the names of the Gateway resources this policy
is being attached to. | +| `targetSelectors` | _[TargetSelector](#targetselector) array_ | true | TargetSelectors allow targeting resources for this policy based on labels | +| `wasm` | _[Wasm](#wasm) array_ | false | Wasm is a list of Wasm extensions to be loaded by the Gateway.
Order matters, as the extensions will be loaded in the order they are
defined in this list. | +| `extProc` | _[ExtProc](#extproc) array_ | false | ExtProc is an ordered list of external processing filters
that should added to the envoy filter chain | + + +#### EnvoyFilter + +_Underlying type:_ _string_ + +EnvoyFilter defines the type of Envoy HTTP filter. + +_Appears in:_ +- [FilterPosition](#filterposition) + +| Value | Description | +| ----- | ----------- | +| `envoy.filters.http.fault` | EnvoyFilterFault defines the Envoy HTTP fault filter.
| +| `envoy.filters.http.cors` | EnvoyFilterCORS defines the Envoy HTTP CORS filter.
| +| `envoy.filters.http.ext_authz` | EnvoyFilterExtAuthz defines the Envoy HTTP external authorization filter.
| +| `envoy.filters.http.basic_authn` | EnvoyFilterBasicAuthn defines the Envoy HTTP basic authentication filter.
| +| `envoy.filters.http.oauth2` | EnvoyFilterOAuth2 defines the Envoy HTTP OAuth2 filter.
| +| `envoy.filters.http.jwt_authn` | EnvoyFilterJWTAuthn defines the Envoy HTTP JWT authentication filter.
| +| `envoy.filters.http.ext_proc` | EnvoyFilterExtProc defines the Envoy HTTP external process filter.
| +| `envoy.filters.http.wasm` | EnvoyFilterWasm defines the Envoy HTTP WebAssembly filter.
| +| `envoy.filters.http.local_ratelimit` | EnvoyFilterLocalRateLimit defines the Envoy HTTP local rate limit filter.
| +| `envoy.filters.http.ratelimit` | EnvoyFilterRateLimit defines the Envoy HTTP rate limit filter.
| +| `envoy.filters.http.rbac` | EnvoyFilterRBAC defines the Envoy RBAC filter.
| +| `envoy.filters.http.router` | EnvoyFilterRouter defines the Envoy HTTP router filter.
| + + +#### EnvoyGateway + + + +EnvoyGateway is the schema for the envoygateways API. + + + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `apiVersion` | _string_ | |`gateway.envoyproxy.io/v1alpha1` +| `kind` | _string_ | |`EnvoyGateway` +| `gateway` | _[Gateway](#gateway)_ | false | Gateway defines desired Gateway API specific configuration. If unset,
default configuration parameters will apply. | +| `provider` | _[EnvoyGatewayProvider](#envoygatewayprovider)_ | false | Provider defines the desired provider and provider-specific configuration.
If unspecified, the Kubernetes provider is used with default configuration
parameters. | +| `logging` | _[EnvoyGatewayLogging](#envoygatewaylogging)_ | false | Logging defines logging parameters for Envoy Gateway. | +| `admin` | _[EnvoyGatewayAdmin](#envoygatewayadmin)_ | false | Admin defines the desired admin related abilities.
If unspecified, the Admin is used with default configuration
parameters. | +| `telemetry` | _[EnvoyGatewayTelemetry](#envoygatewaytelemetry)_ | false | Telemetry defines the desired control plane telemetry related abilities.
If unspecified, the telemetry is used with default configuration. | +| `rateLimit` | _[RateLimit](#ratelimit)_ | false | RateLimit defines the configuration associated with the Rate Limit service
deployed by Envoy Gateway required to implement the Global Rate limiting
functionality. The specific rate limit service used here is the reference
implementation in Envoy. For more details visit https://github.com/envoyproxy/ratelimit.
This configuration is unneeded for "Local" rate limiting. | +| `extensionManager` | _[ExtensionManager](#extensionmanager)_ | false | ExtensionManager defines an extension manager to register for the Envoy Gateway Control Plane. | +| `extensionApis` | _[ExtensionAPISettings](#extensionapisettings)_ | false | ExtensionAPIs defines the settings related to specific Gateway API Extensions
implemented by Envoy Gateway | + + +#### EnvoyGatewayAdmin + + + +EnvoyGatewayAdmin defines the Envoy Gateway Admin configuration. + +_Appears in:_ +- [EnvoyGateway](#envoygateway) +- [EnvoyGatewaySpec](#envoygatewayspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `address` | _[EnvoyGatewayAdminAddress](#envoygatewayadminaddress)_ | false | Address defines the address of Envoy Gateway Admin Server. | +| `enableDumpConfig` | _boolean_ | false | EnableDumpConfig defines if enable dump config in Envoy Gateway logs. | +| `enablePprof` | _boolean_ | false | EnablePprof defines if enable pprof in Envoy Gateway Admin Server. | + + +#### EnvoyGatewayAdminAddress + + + +EnvoyGatewayAdminAddress defines the Envoy Gateway Admin Address configuration. + +_Appears in:_ +- [EnvoyGatewayAdmin](#envoygatewayadmin) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `port` | _integer_ | false | Port defines the port the admin server is exposed on. | +| `host` | _string_ | false | Host defines the admin server hostname. | + + +#### EnvoyGatewayCustomProvider + + + +EnvoyGatewayCustomProvider defines configuration for the Custom provider. + +_Appears in:_ +- [EnvoyGatewayProvider](#envoygatewayprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `resource` | _[EnvoyGatewayResourceProvider](#envoygatewayresourceprovider)_ | true | Resource defines the desired resource provider.
This provider is used to specify the provider to be used
to retrieve the resource configurations such as Gateway API
resources | +| `infrastructure` | _[EnvoyGatewayInfrastructureProvider](#envoygatewayinfrastructureprovider)_ | true | Infrastructure defines the desired infrastructure provider.
This provider is used to specify the provider to be used
to provide an environment to deploy the out resources like
the Envoy Proxy data plane. | + + +#### EnvoyGatewayFileResourceProvider + + + +EnvoyGatewayFileResourceProvider defines configuration for the File Resource provider. + +_Appears in:_ +- [EnvoyGatewayResourceProvider](#envoygatewayresourceprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `paths` | _string array_ | true | Paths are the paths to a directory or file containing the resource configuration.
Recursive sub directories are not currently supported. | + + +#### EnvoyGatewayHostInfrastructureProvider + + + +EnvoyGatewayHostInfrastructureProvider defines configuration for the Host Infrastructure provider. + +_Appears in:_ +- [EnvoyGatewayInfrastructureProvider](#envoygatewayinfrastructureprovider) + + + +#### EnvoyGatewayInfrastructureProvider + + + +EnvoyGatewayInfrastructureProvider defines configuration for the Custom Infrastructure provider. + +_Appears in:_ +- [EnvoyGatewayCustomProvider](#envoygatewaycustomprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[InfrastructureProviderType](#infrastructureprovidertype)_ | true | Type is the type of infrastructure providers to use. Supported types are "Host". | +| `host` | _[EnvoyGatewayHostInfrastructureProvider](#envoygatewayhostinfrastructureprovider)_ | false | Host defines the configuration of the Host provider. Host provides runtime
deployment of the data plane as a child process on the host environment. | + + +#### EnvoyGatewayKubernetesProvider + + + +EnvoyGatewayKubernetesProvider defines configuration for the Kubernetes provider. + +_Appears in:_ +- [EnvoyGatewayProvider](#envoygatewayprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `rateLimitDeployment` | _[KubernetesDeploymentSpec](#kubernetesdeploymentspec)_ | false | RateLimitDeployment defines the desired state of the Envoy ratelimit deployment resource.
If unspecified, default settings for the managed Envoy ratelimit deployment resource
are applied. | +| `watch` | _[KubernetesWatchMode](#kuberneteswatchmode)_ | false | Watch holds configuration of which input resources should be watched and reconciled. | +| `deploy` | _[KubernetesDeployMode](#kubernetesdeploymode)_ | false | Deploy holds configuration of how output managed resources such as the Envoy Proxy data plane
should be deployed | +| `overwriteControlPlaneCerts` | _boolean_ | false | OverwriteControlPlaneCerts updates the secrets containing the control plane certs, when set. | +| `leaderElection` | _[LeaderElection](#leaderelection)_ | false | LeaderElection specifies the configuration for leader election.
If it's not set up, leader election will be active by default, using Kubernetes' standard settings. | +| `shutdownManager` | _[ShutdownManager](#shutdownmanager)_ | false | ShutdownManager defines the configuration for the shutdown manager. | + + +#### EnvoyGatewayLogComponent + +_Underlying type:_ _string_ + +EnvoyGatewayLogComponent defines a component that supports a configured logging level. + +_Appears in:_ +- [EnvoyGatewayLogging](#envoygatewaylogging) + +| Value | Description | +| ----- | ----------- | +| `default` | LogComponentGatewayDefault defines the "default"-wide logging component. When specified,
all other logging components are ignored.
| +| `provider` | LogComponentProviderRunner defines the "provider" runner component.
| +| `gateway-api` | LogComponentGatewayAPIRunner defines the "gateway-api" runner component.
| +| `xds-translator` | LogComponentXdsTranslatorRunner defines the "xds-translator" runner component.
| +| `xds-server` | LogComponentXdsServerRunner defines the "xds-server" runner component.
| +| `infrastructure` | LogComponentInfrastructureRunner defines the "infrastructure" runner component.
| +| `global-ratelimit` | LogComponentGlobalRateLimitRunner defines the "global-ratelimit" runner component.
| + + +#### EnvoyGatewayLogging + + + +EnvoyGatewayLogging defines logging for Envoy Gateway. + +_Appears in:_ +- [EnvoyGateway](#envoygateway) +- [EnvoyGatewaySpec](#envoygatewayspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `level` | _object (keys:[EnvoyGatewayLogComponent](#envoygatewaylogcomponent), values:[LogLevel](#loglevel))_ | true | Level is the logging level. If unspecified, defaults to "info".
EnvoyGatewayLogComponent options: default/provider/gateway-api/xds-translator/xds-server/infrastructure/global-ratelimit.
LogLevel options: debug/info/error/warn. | + + +#### EnvoyGatewayMetricSink + + + +EnvoyGatewayMetricSink defines control plane +metric sinks where metrics are sent to. + +_Appears in:_ +- [EnvoyGatewayMetrics](#envoygatewaymetrics) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[MetricSinkType](#metricsinktype)_ | true | Type defines the metric sink type.
EG control plane currently supports OpenTelemetry. | +| `openTelemetry` | _[EnvoyGatewayOpenTelemetrySink](#envoygatewayopentelemetrysink)_ | true | OpenTelemetry defines the configuration for OpenTelemetry sink.
It's required if the sink type is OpenTelemetry. | + + +#### EnvoyGatewayMetrics + + + +EnvoyGatewayMetrics defines control plane push/pull metrics configurations. + +_Appears in:_ +- [EnvoyGatewayTelemetry](#envoygatewaytelemetry) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `sinks` | _[EnvoyGatewayMetricSink](#envoygatewaymetricsink) array_ | true | Sinks defines the metric sinks where metrics are sent to. | +| `prometheus` | _[EnvoyGatewayPrometheusProvider](#envoygatewayprometheusprovider)_ | true | Prometheus defines the configuration for prometheus endpoint. | + + +#### EnvoyGatewayOpenTelemetrySink + + + + + +_Appears in:_ +- [EnvoyGatewayMetricSink](#envoygatewaymetricsink) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `host` | _string_ | true | Host define the sink service hostname. | +| `protocol` | _string_ | true | Protocol define the sink service protocol. | +| `port` | _integer_ | false | Port defines the port the sink service is exposed on. | +| `exportInterval` | _[Duration](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Duration)_ | true | ExportInterval configures the intervening time between exports for a
Sink. This option overrides any value set for the
OTEL_METRIC_EXPORT_INTERVAL environment variable.
If ExportInterval is less than or equal to zero, 60 seconds
is used as the default. | +| `exportTimeout` | _[Duration](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Duration)_ | true | ExportTimeout configures the time a Sink waits for an export to
complete before canceling it. This option overrides any value set for the
OTEL_METRIC_EXPORT_TIMEOUT environment variable.
If ExportTimeout is less than or equal to zero, 30 seconds
is used as the default. | + + +#### EnvoyGatewayPrometheusProvider + + + +EnvoyGatewayPrometheusProvider will expose prometheus endpoint in pull mode. + +_Appears in:_ +- [EnvoyGatewayMetrics](#envoygatewaymetrics) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `disable` | _boolean_ | true | Disable defines if disables the prometheus metrics in pull mode. | + + +#### EnvoyGatewayProvider + + + +EnvoyGatewayProvider defines the desired configuration of a provider. + +_Appears in:_ +- [EnvoyGateway](#envoygateway) +- [EnvoyGatewaySpec](#envoygatewayspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[ProviderType](#providertype)_ | true | Type is the type of provider to use. Supported types are "Kubernetes". | +| `kubernetes` | _[EnvoyGatewayKubernetesProvider](#envoygatewaykubernetesprovider)_ | false | Kubernetes defines the configuration of the Kubernetes provider. Kubernetes
provides runtime configuration via the Kubernetes API. | +| `custom` | _[EnvoyGatewayCustomProvider](#envoygatewaycustomprovider)_ | false | Custom defines the configuration for the Custom provider. This provider
allows you to define a specific resource provider and a infrastructure
provider. | + + +#### EnvoyGatewayResourceProvider + + + +EnvoyGatewayResourceProvider defines configuration for the Custom Resource provider. + +_Appears in:_ +- [EnvoyGatewayCustomProvider](#envoygatewaycustomprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[ResourceProviderType](#resourceprovidertype)_ | true | Type is the type of resource provider to use. Supported types are "File". | +| `file` | _[EnvoyGatewayFileResourceProvider](#envoygatewayfileresourceprovider)_ | false | File defines the configuration of the File provider. File provides runtime
configuration defined by one or more files. | + + +#### EnvoyGatewaySpec + + + +EnvoyGatewaySpec defines the desired state of Envoy Gateway. + +_Appears in:_ +- [EnvoyGateway](#envoygateway) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `gateway` | _[Gateway](#gateway)_ | false | Gateway defines desired Gateway API specific configuration. If unset,
default configuration parameters will apply. | +| `provider` | _[EnvoyGatewayProvider](#envoygatewayprovider)_ | false | Provider defines the desired provider and provider-specific configuration.
If unspecified, the Kubernetes provider is used with default configuration
parameters. | +| `logging` | _[EnvoyGatewayLogging](#envoygatewaylogging)_ | false | Logging defines logging parameters for Envoy Gateway. | +| `admin` | _[EnvoyGatewayAdmin](#envoygatewayadmin)_ | false | Admin defines the desired admin related abilities.
If unspecified, the Admin is used with default configuration
parameters. | +| `telemetry` | _[EnvoyGatewayTelemetry](#envoygatewaytelemetry)_ | false | Telemetry defines the desired control plane telemetry related abilities.
If unspecified, the telemetry is used with default configuration. | +| `rateLimit` | _[RateLimit](#ratelimit)_ | false | RateLimit defines the configuration associated with the Rate Limit service
deployed by Envoy Gateway required to implement the Global Rate limiting
functionality. The specific rate limit service used here is the reference
implementation in Envoy. For more details visit https://github.com/envoyproxy/ratelimit.
This configuration is unneeded for "Local" rate limiting. | +| `extensionManager` | _[ExtensionManager](#extensionmanager)_ | false | ExtensionManager defines an extension manager to register for the Envoy Gateway Control Plane. | +| `extensionApis` | _[ExtensionAPISettings](#extensionapisettings)_ | false | ExtensionAPIs defines the settings related to specific Gateway API Extensions
implemented by Envoy Gateway | + + +#### EnvoyGatewayTelemetry + + + +EnvoyGatewayTelemetry defines telemetry configurations for envoy gateway control plane. +Control plane will focus on metrics observability telemetry and tracing telemetry later. + +_Appears in:_ +- [EnvoyGateway](#envoygateway) +- [EnvoyGatewaySpec](#envoygatewayspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `metrics` | _[EnvoyGatewayMetrics](#envoygatewaymetrics)_ | true | Metrics defines metrics configuration for envoy gateway. | + + +#### EnvoyJSONPatchConfig + + + +EnvoyJSONPatchConfig defines the configuration for patching a Envoy xDS Resource +using JSONPatch semantic + +_Appears in:_ +- [EnvoyPatchPolicySpec](#envoypatchpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[EnvoyResourceType](#envoyresourcetype)_ | true | Type is the typed URL of the Envoy xDS Resource | +| `name` | _string_ | true | Name is the name of the resource | +| `operation` | _[JSONPatchOperation](#jsonpatchoperation)_ | true | Patch defines the JSON Patch Operation | + + +#### EnvoyPatchPolicy + + + +EnvoyPatchPolicy allows the user to modify the generated Envoy xDS +resources by Envoy Gateway using this patch API + +_Appears in:_ +- [EnvoyPatchPolicyList](#envoypatchpolicylist) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `apiVersion` | _string_ | |`gateway.envoyproxy.io/v1alpha1` +| `kind` | _string_ | |`EnvoyPatchPolicy` +| `metadata` | _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#objectmeta-v1-meta)_ | true | Refer to Kubernetes API documentation for fields of `metadata`. | +| `spec` | _[EnvoyPatchPolicySpec](#envoypatchpolicyspec)_ | true | Spec defines the desired state of EnvoyPatchPolicy. | +| `status` | _[PolicyStatus](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.PolicyStatus)_ | true | Status defines the current status of EnvoyPatchPolicy. | + + +#### EnvoyPatchPolicyList + + + +EnvoyPatchPolicyList contains a list of EnvoyPatchPolicy resources. + + + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `apiVersion` | _string_ | |`gateway.envoyproxy.io/v1alpha1` +| `kind` | _string_ | |`EnvoyPatchPolicyList` +| `metadata` | _[ListMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#listmeta-v1-meta)_ | true | Refer to Kubernetes API documentation for fields of `metadata`. | +| `items` | _[EnvoyPatchPolicy](#envoypatchpolicy) array_ | true | | + + +#### EnvoyPatchPolicySpec + + + +EnvoyPatchPolicySpec defines the desired state of EnvoyPatchPolicy. + +_Appears in:_ +- [EnvoyPatchPolicy](#envoypatchpolicy) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[EnvoyPatchType](#envoypatchtype)_ | true | Type decides the type of patch.
Valid EnvoyPatchType values are "JSONPatch". | +| `jsonPatches` | _[EnvoyJSONPatchConfig](#envoyjsonpatchconfig) array_ | false | JSONPatch defines the JSONPatch configuration. | +| `targetRef` | _[LocalPolicyTargetReference](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReference)_ | true | TargetRef is the name of the Gateway API resource this policy
is being attached to.
By default, attaching to Gateway is supported and
when mergeGateways is enabled it should attach to GatewayClass.
This Policy and the TargetRef MUST be in the same namespace
for this Policy to have effect and be applied to the Gateway
TargetRef | +| `priority` | _integer_ | true | Priority of the EnvoyPatchPolicy.
If multiple EnvoyPatchPolicies are applied to the same
TargetRef, they will be applied in the ascending order of
the priority i.e. int32.min has the highest priority and
int32.max has the lowest priority.
Defaults to 0. | + + +#### EnvoyPatchType + +_Underlying type:_ _string_ + +EnvoyPatchType specifies the types of Envoy patching mechanisms. + +_Appears in:_ +- [EnvoyPatchPolicySpec](#envoypatchpolicyspec) + +| Value | Description | +| ----- | ----------- | +| `JSONPatch` | JSONPatchEnvoyPatchType allows the user to patch the generated xDS resources using JSONPatch semantics.
For more details on the semantics, please refer to https://datatracker.ietf.org/doc/html/rfc6902
| + + +#### EnvoyProxy + + + +EnvoyProxy is the schema for the envoyproxies API. + + + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `apiVersion` | _string_ | |`gateway.envoyproxy.io/v1alpha1` +| `kind` | _string_ | |`EnvoyProxy` +| `metadata` | _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#objectmeta-v1-meta)_ | true | Refer to Kubernetes API documentation for fields of `metadata`. | +| `spec` | _[EnvoyProxySpec](#envoyproxyspec)_ | true | EnvoyProxySpec defines the desired state of EnvoyProxy. | +| `status` | _[EnvoyProxyStatus](#envoyproxystatus)_ | true | EnvoyProxyStatus defines the actual state of EnvoyProxy. | + + +#### EnvoyProxyKubernetesProvider + + + +EnvoyProxyKubernetesProvider defines configuration for the Kubernetes resource +provider. + +_Appears in:_ +- [EnvoyProxyProvider](#envoyproxyprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `envoyDeployment` | _[KubernetesDeploymentSpec](#kubernetesdeploymentspec)_ | false | EnvoyDeployment defines the desired state of the Envoy deployment resource.
If unspecified, default settings for the managed Envoy deployment resource
are applied. | +| `envoyDaemonSet` | _[KubernetesDaemonSetSpec](#kubernetesdaemonsetspec)_ | false | EnvoyDaemonSet defines the desired state of the Envoy daemonset resource.
Disabled by default, a deployment resource is used instead to provision the Envoy Proxy fleet | +| `envoyService` | _[KubernetesServiceSpec](#kubernetesservicespec)_ | false | EnvoyService defines the desired state of the Envoy service resource.
If unspecified, default settings for the managed Envoy service resource
are applied. | +| `envoyHpa` | _[KubernetesHorizontalPodAutoscalerSpec](#kuberneteshorizontalpodautoscalerspec)_ | false | EnvoyHpa defines the Horizontal Pod Autoscaler settings for Envoy Proxy Deployment.
Once the HPA is being set, Replicas field from EnvoyDeployment will be ignored. | +| `useListenerPortAsContainerPort` | _boolean_ | false | UseListenerPortAsContainerPort disables the port shifting feature in the Envoy Proxy.
When set to false (default value), if the service port is a privileged port (1-1023), add a constant to the value converting it into an ephemeral port.
This allows the container to bind to the port without needing a CAP_NET_BIND_SERVICE capability. | +| `envoyPDB` | _[KubernetesPodDisruptionBudgetSpec](#kubernetespoddisruptionbudgetspec)_ | false | EnvoyPDB allows to control the pod disruption budget of an Envoy Proxy. | + + +#### EnvoyProxyProvider + + + +EnvoyProxyProvider defines the desired state of a resource provider. + +_Appears in:_ +- [EnvoyProxySpec](#envoyproxyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[ProviderType](#providertype)_ | true | Type is the type of resource provider to use. A resource provider provides
infrastructure resources for running the data plane, e.g. Envoy proxy, and
optional auxiliary control planes. Supported types are "Kubernetes". | +| `kubernetes` | _[EnvoyProxyKubernetesProvider](#envoyproxykubernetesprovider)_ | false | Kubernetes defines the desired state of the Kubernetes resource provider.
Kubernetes provides infrastructure resources for running the data plane,
e.g. Envoy proxy. If unspecified and type is "Kubernetes", default settings
for managed Kubernetes resources are applied. | + + +#### EnvoyProxySpec + + + +EnvoyProxySpec defines the desired state of EnvoyProxy. + +_Appears in:_ +- [EnvoyProxy](#envoyproxy) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `provider` | _[EnvoyProxyProvider](#envoyproxyprovider)_ | false | Provider defines the desired resource provider and provider-specific configuration.
If unspecified, the "Kubernetes" resource provider is used with default configuration
parameters. | +| `logging` | _[ProxyLogging](#proxylogging)_ | true | Logging defines logging parameters for managed proxies. | +| `telemetry` | _[ProxyTelemetry](#proxytelemetry)_ | false | Telemetry defines telemetry parameters for managed proxies. | +| `bootstrap` | _[ProxyBootstrap](#proxybootstrap)_ | false | Bootstrap defines the Envoy Bootstrap as a YAML string.
Visit https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/bootstrap/v3/bootstrap.proto#envoy-v3-api-msg-config-bootstrap-v3-bootstrap
to learn more about the syntax.
If set, this is the Bootstrap configuration used for the managed Envoy Proxy fleet instead of the default Bootstrap configuration
set by Envoy Gateway.
Some fields within the Bootstrap that are required to communicate with the xDS Server (Envoy Gateway) and receive xDS resources
from it are not configurable and will result in the `EnvoyProxy` resource being rejected.
Backward compatibility across minor versions is not guaranteed.
We strongly recommend using `egctl x translate` to generate a `EnvoyProxy` resource with the `Bootstrap` field set to the default
Bootstrap configuration used. You can edit this configuration, and rerun `egctl x translate` to ensure there are no validation errors. | +| `concurrency` | _integer_ | false | Concurrency defines the number of worker threads to run. If unset, it defaults to
the number of cpuset threads on the platform. | +| `routingType` | _[RoutingType](#routingtype)_ | false | RoutingType can be set to "Service" to use the Service Cluster IP for routing to the backend,
or it can be set to "Endpoint" to use Endpoint routing. The default is "Endpoint". | +| `extraArgs` | _string array_ | false | ExtraArgs defines additional command line options that are provided to Envoy.
More info: https://www.envoyproxy.io/docs/envoy/latest/operations/cli#command-line-options
Note: some command line options are used internally(e.g. --log-level) so they cannot be provided here. | +| `mergeGateways` | _boolean_ | false | MergeGateways defines if Gateway resources should be merged onto the same Envoy Proxy Infrastructure.
Setting this field to true would merge all Gateway Listeners under the parent Gateway Class.
This means that the port, protocol and hostname tuple must be unique for every listener.
If a duplicate listener is detected, the newer listener (based on timestamp) will be rejected and its status will be updated with a "Accepted=False" condition. | +| `shutdown` | _[ShutdownConfig](#shutdownconfig)_ | false | Shutdown defines configuration for graceful envoy shutdown process. | +| `filterOrder` | _[FilterPosition](#filterposition) array_ | false | FilterOrder defines the order of filters in the Envoy proxy's HTTP filter chain.
The FilterPosition in the list will be applied in the order they are defined.
If unspecified, the default filter order is applied.
Default filter order is:

- envoy.filters.http.fault

- envoy.filters.http.cors

- envoy.filters.http.ext_authz

- envoy.filters.http.basic_authn

- envoy.filters.http.oauth2

- envoy.filters.http.jwt_authn

- envoy.filters.http.ext_proc

- envoy.filters.http.wasm

- envoy.filters.http.rbac

- envoy.filters.http.local_ratelimit

- envoy.filters.http.ratelimit

- envoy.filters.http.router | +| `backendTLS` | _[BackendTLSConfig](#backendtlsconfig)_ | false | BackendTLS is the TLS configuration for the Envoy proxy to use when connecting to backends.
These settings are applied on backends for which TLS policies are specified. | + + +#### EnvoyProxyStatus + + + +EnvoyProxyStatus defines the observed state of EnvoyProxy. This type is not implemented +until https://github.com/envoyproxy/gateway/issues/1007 is fixed. + +_Appears in:_ +- [EnvoyProxy](#envoyproxy) + + + +#### EnvoyResourceType + +_Underlying type:_ _string_ + +EnvoyResourceType specifies the type URL of the Envoy resource. + +_Appears in:_ +- [EnvoyJSONPatchConfig](#envoyjsonpatchconfig) + +| Value | Description | +| ----- | ----------- | +| `type.googleapis.com/envoy.config.listener.v3.Listener` | ListenerEnvoyResourceType defines the Type URL of the Listener resource
| +| `type.googleapis.com/envoy.config.route.v3.RouteConfiguration` | RouteConfigurationEnvoyResourceType defines the Type URL of the RouteConfiguration resource
| +| `type.googleapis.com/envoy.config.cluster.v3.Cluster` | ClusterEnvoyResourceType defines the Type URL of the Cluster resource
| +| `type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment` | ClusterLoadAssignmentEnvoyResourceType defines the Type URL of the ClusterLoadAssignment resource
| + + +#### ExtAuth + + + +ExtAuth defines the configuration for External Authorization. + +_Appears in:_ +- [SecurityPolicySpec](#securitypolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `grpc` | _[GRPCExtAuthService](#grpcextauthservice)_ | true | GRPC defines the gRPC External Authorization service.
Either GRPCService or HTTPService must be specified,
and only one of them can be provided. | +| `http` | _[HTTPExtAuthService](#httpextauthservice)_ | true | HTTP defines the HTTP External Authorization service.
Either GRPCService or HTTPService must be specified,
and only one of them can be provided. | +| `headersToExtAuth` | _string array_ | false | HeadersToExtAuth defines the client request headers that will be included
in the request to the external authorization service.
Note: If not specified, the default behavior for gRPC and HTTP external
authorization services is different due to backward compatibility reasons.
All headers will be included in the check request to a gRPC authorization server.
Only the following headers will be included in the check request to an HTTP
authorization server: Host, Method, Path, Content-Length, and Authorization.
And these headers will always be included to the check request to an HTTP
authorization server by default, no matter whether they are specified
in HeadersToExtAuth or not. | +| `failOpen` | _boolean_ | false | FailOpen is a switch used to control the behavior when a response from the External Authorization service cannot be obtained.
If FailOpen is set to true, the system allows the traffic to pass through.
Otherwise, if it is set to false or not set (defaulting to false),
the system blocks the traffic and returns a HTTP 5xx error, reflecting a fail-closed approach.
This setting determines whether to prioritize accessibility over strict security in case of authorization service failure. | + + +#### ExtProc + + + +ExtProc defines the configuration for External Processing filter. + +_Appears in:_ +- [EnvoyExtensionPolicySpec](#envoyextensionpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `backendRefs` | _[BackendRef](#backendref) array_ | true | BackendRefs defines the configuration of the external processing service | +| `messageTimeout` | _[Duration](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Duration)_ | false | MessageTimeout is the timeout for a response to be returned from the external processor
Default: 200ms | +| `failOpen` | _boolean_ | false | FailOpen defines if requests or responses that cannot be processed due to connectivity to the
external processor are terminated or passed-through.
Default: false | +| `processingMode` | _[ExtProcProcessingMode](#extprocprocessingmode)_ | false | ProcessingMode defines how request and response body is processed
Default: header and body are not sent to the external processor | + + +#### ExtProcBodyProcessingMode + +_Underlying type:_ _string_ + + + +_Appears in:_ +- [ProcessingModeOptions](#processingmodeoptions) + +| Value | Description | +| ----- | ----------- | +| `Streamed` | StreamedExtProcBodyProcessingMode will stream the body to the server in pieces as they arrive at the proxy.
| +| `Buffered` | BufferedExtProcBodyProcessingMode will buffer the message body in memory and send the entire body at once. If the body exceeds the configured buffer limit, then the downstream system will receive an error.
| +| `BufferedPartial` | BufferedPartialExtBodyHeaderProcessingMode will buffer the message body in memory and send the entire body in one chunk. If the body exceeds the configured buffer limit, then the body contents up to the buffer limit will be sent.
| + + +#### ExtProcProcessingMode + + + +ExtProcProcessingMode defines if and how headers and bodies are sent to the service. +https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/ext_proc/v3/processing_mode.proto#envoy-v3-api-msg-extensions-filters-http-ext-proc-v3-processingmode + +_Appears in:_ +- [ExtProc](#extproc) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `request` | _[ProcessingModeOptions](#processingmodeoptions)_ | false | Defines processing mode for requests. If present, request headers are sent. Request body is processed according
to the specified mode. | +| `response` | _[ProcessingModeOptions](#processingmodeoptions)_ | false | Defines processing mode for responses. If present, response headers are sent. Response body is processed according
to the specified mode. | + + +#### ExtensionAPISettings + + + +ExtensionAPISettings defines the settings specific to Gateway API Extensions. + +_Appears in:_ +- [EnvoyGateway](#envoygateway) +- [EnvoyGatewaySpec](#envoygatewayspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `enableEnvoyPatchPolicy` | _boolean_ | true | EnableEnvoyPatchPolicy enables Envoy Gateway to
reconcile and implement the EnvoyPatchPolicy resources. | +| `enableBackend` | _boolean_ | true | EnableBackend enables Envoy Gateway to
reconcile and implement the Backend resources. | + + +#### ExtensionHooks + + + +ExtensionHooks defines extension hooks across all supported runners + +_Appears in:_ +- [ExtensionManager](#extensionmanager) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `xdsTranslator` | _[XDSTranslatorHooks](#xdstranslatorhooks)_ | true | XDSTranslator defines all the supported extension hooks for the xds-translator runner | + + +#### ExtensionManager + + + +ExtensionManager defines the configuration for registering an extension manager to +the Envoy Gateway control plane. + +_Appears in:_ +- [EnvoyGateway](#envoygateway) +- [EnvoyGatewaySpec](#envoygatewayspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `resources` | _[GroupVersionKind](#groupversionkind) array_ | false | Resources defines the set of K8s resources the extension will handle as route
filter resources | +| `policyResources` | _[GroupVersionKind](#groupversionkind) array_ | false | PolicyResources defines the set of K8S resources the extension server will handle
as directly attached GatewayAPI policies | +| `hooks` | _[ExtensionHooks](#extensionhooks)_ | true | Hooks defines the set of hooks the extension supports | +| `service` | _[ExtensionService](#extensionservice)_ | true | Service defines the configuration of the extension service that the Envoy
Gateway Control Plane will call through extension hooks. | + + +#### ExtensionService + + + +ExtensionService defines the configuration for connecting to a registered extension service. + +_Appears in:_ +- [ExtensionManager](#extensionmanager) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `fqdn` | _[FQDNEndpoint](#fqdnendpoint)_ | false | FQDN defines a FQDN endpoint | +| `ip` | _[IPEndpoint](#ipendpoint)_ | false | IP defines an IP endpoint. Currently, only IPv4 Addresses are supported. | +| `unix` | _[UnixSocket](#unixsocket)_ | false | Unix defines the unix domain socket endpoint | +| `host` | _string_ | false | Host define the extension service hostname.
Deprecated: use the appropriate transport attribute instead (FQDN,IP,Unix) | +| `port` | _integer_ | false | Port defines the port the extension service is exposed on.
Deprecated: use the appropriate transport attribute instead (FQDN,IP,Unix) | +| `tls` | _[ExtensionTLS](#extensiontls)_ | false | TLS defines TLS configuration for communication between Envoy Gateway and
the extension service. | + + +#### ExtensionTLS + + + +ExtensionTLS defines the TLS configuration when connecting to an extension service + +_Appears in:_ +- [ExtensionService](#extensionservice) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `certificateRef` | _[SecretObjectReference](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.SecretObjectReference)_ | true | CertificateRef contains a references to objects (Kubernetes objects or otherwise) that
contains a TLS certificate and private keys. These certificates are used to
establish a TLS handshake to the extension server.

CertificateRef can only reference a Kubernetes Secret at this time. | + + +#### FQDNEndpoint + + + +FQDNEndpoint describes TCP/UDP socket address, corresponding to Envoy's Socket Address +https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/address.proto#config-core-v3-socketaddress + +_Appears in:_ +- [BackendEndpoint](#backendendpoint) +- [ExtensionService](#extensionservice) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `hostname` | _string_ | true | Hostname defines the FQDN hostname of the backend endpoint. | +| `port` | _integer_ | true | Port defines the port of the backend endpoint. | + + +#### FaultInjection + + + +FaultInjection defines the fault injection policy to be applied. This configuration can be used to +inject delays and abort requests to mimic failure scenarios such as service failures and overloads + +_Appears in:_ +- [BackendTrafficPolicySpec](#backendtrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `delay` | _[FaultInjectionDelay](#faultinjectiondelay)_ | false | If specified, a delay will be injected into the request. | +| `abort` | _[FaultInjectionAbort](#faultinjectionabort)_ | false | If specified, the request will be aborted if it meets the configuration criteria. | + + +#### FaultInjectionAbort + + + +FaultInjectionAbort defines the abort fault injection configuration + +_Appears in:_ +- [FaultInjection](#faultinjection) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `httpStatus` | _integer_ | false | StatusCode specifies the HTTP status code to be returned | +| `grpcStatus` | _integer_ | false | GrpcStatus specifies the GRPC status code to be returned | +| `percentage` | _float_ | false | Percentage specifies the percentage of requests to be aborted. Default 100%, if set 0, no requests will be aborted. Accuracy to 0.0001%. | + + +#### FaultInjectionDelay + + + +FaultInjectionDelay defines the delay fault injection configuration + +_Appears in:_ +- [FaultInjection](#faultinjection) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `fixedDelay` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | true | FixedDelay specifies the fixed delay duration | +| `percentage` | _float_ | false | Percentage specifies the percentage of requests to be delayed. Default 100%, if set 0, no requests will be delayed. Accuracy to 0.0001%. | + + +#### FileEnvoyProxyAccessLog + + + + + +_Appears in:_ +- [ProxyAccessLogSink](#proxyaccesslogsink) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `path` | _string_ | true | Path defines the file path used to expose envoy access log(e.g. /dev/stdout). | + + +#### FilterPosition + + + +FilterPosition defines the position of an Envoy HTTP filter in the filter chain. + +_Appears in:_ +- [EnvoyProxySpec](#envoyproxyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `name` | _[EnvoyFilter](#envoyfilter)_ | true | Name of the filter. | +| `before` | _[EnvoyFilter](#envoyfilter)_ | true | Before defines the filter that should come before the filter.
Only one of Before or After must be set. | +| `after` | _[EnvoyFilter](#envoyfilter)_ | true | After defines the filter that should come after the filter.
Only one of Before or After must be set. | + + +#### GRPCExtAuthService + + + +GRPCExtAuthService defines the gRPC External Authorization service +The authorization request message is defined in +https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/auth/v3/external_auth.proto + +_Appears in:_ +- [ExtAuth](#extauth) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `backendRef` | _[BackendObjectReference](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.BackendObjectReference)_ | true | BackendRef references a Kubernetes object that represents the
backend server to which the authorization request will be sent.
Only Service kind is supported for now.

Deprecated: Use BackendRefs instead. | +| `backendRefs` | _[BackendRef](#backendref) array_ | false | BackendRefs references a Kubernetes object that represents the
backend server to which the authorization request will be sent.
Only Service kind is supported for now. | + + +#### Gateway + + + +Gateway defines the desired Gateway API configuration of Envoy Gateway. + +_Appears in:_ +- [EnvoyGateway](#envoygateway) +- [EnvoyGatewaySpec](#envoygatewayspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `controllerName` | _string_ | false | ControllerName defines the name of the Gateway API controller. If unspecified,
defaults to "gateway.envoyproxy.io/gatewayclass-controller". See the following
for additional details:
https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.GatewayClass | + + +#### GlobalRateLimit + + + +GlobalRateLimit defines global rate limit configuration. + +_Appears in:_ +- [RateLimitSpec](#ratelimitspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `rules` | _[RateLimitRule](#ratelimitrule) array_ | true | Rules are a list of RateLimit selectors and limits. Each rule and its
associated limit is applied in a mutually exclusive way. If a request
matches multiple rules, each of their associated limits get applied, so a
single request might increase the rate limit counters for multiple rules
if selected. The rate limit service will return a logical OR of the individual
rate limit decisions of all matching rules. For example, if a request
matches two rules, one rate limited and one not, the final decision will be
to rate limit the request. | + + +#### GroupVersionKind + + + +GroupVersionKind unambiguously identifies a Kind. +It can be converted to k8s.io/apimachinery/pkg/runtime/schema.GroupVersionKind + +_Appears in:_ +- [ExtensionManager](#extensionmanager) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `group` | _string_ | true | | +| `version` | _string_ | true | | +| `kind` | _string_ | true | | + + +#### GzipCompressor + + + +GzipCompressor defines the config for the Gzip compressor. +The default values can be found here: +https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/compression/gzip/compressor/v3/gzip.proto#extension-envoy-compression-gzip-compressor + +_Appears in:_ +- [Compression](#compression) + + + +#### HTTP10Settings + + + +HTTP10Settings provides HTTP/1.0 configuration on the listener. + +_Appears in:_ +- [HTTP1Settings](#http1settings) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `useDefaultHost` | _boolean_ | false | UseDefaultHost defines if the HTTP/1.0 request is missing the Host header,
then the hostname associated with the listener should be injected into the
request.
If this is not set and an HTTP/1.0 request arrives without a host, then
it will be rejected. | + + +#### HTTP1Settings + + + +HTTP1Settings provides HTTP/1 configuration on the listener. + +_Appears in:_ +- [ClientTrafficPolicySpec](#clienttrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `enableTrailers` | _boolean_ | false | EnableTrailers defines if HTTP/1 trailers should be proxied by Envoy. | +| `preserveHeaderCase` | _boolean_ | false | PreserveHeaderCase defines if Envoy should preserve the letter case of headers.
By default, Envoy will lowercase all the headers. | +| `http10` | _[HTTP10Settings](#http10settings)_ | false | HTTP10 turns on support for HTTP/1.0 and HTTP/0.9 requests. | + + +#### HTTP2Settings + + + +HTTP2Settings provides HTTP/2 configuration on the listener. + +_Appears in:_ +- [ClientTrafficPolicySpec](#clienttrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `initialStreamWindowSize` | _[Quantity](#quantity)_ | false | InitialStreamWindowSize sets the initial window size for HTTP/2 streams.
If not set, the default value is 64 KiB(64*1024). | +| `initialConnectionWindowSize` | _[Quantity](#quantity)_ | false | InitialConnectionWindowSize sets the initial window size for HTTP/2 connections.
If not set, the default value is 1 MiB. | +| `maxConcurrentStreams` | _integer_ | false | MaxConcurrentStreams sets the maximum number of concurrent streams allowed per connection.
If not set, the default value is 100. | + + +#### HTTP3Settings + + + +HTTP3Settings provides HTTP/3 configuration on the listener. + +_Appears in:_ +- [ClientTrafficPolicySpec](#clienttrafficpolicyspec) + + + +#### HTTPActiveHealthChecker + + + +HTTPActiveHealthChecker defines the settings of http health check. + +_Appears in:_ +- [ActiveHealthCheck](#activehealthcheck) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `path` | _string_ | true | Path defines the HTTP path that will be requested during health checking. | +| `method` | _string_ | false | Method defines the HTTP method used for health checking.
Defaults to GET | +| `expectedStatuses` | _[HTTPStatus](#httpstatus) array_ | false | ExpectedStatuses defines a list of HTTP response statuses considered healthy.
Defaults to 200 only | +| `expectedResponse` | _[ActiveHealthCheckPayload](#activehealthcheckpayload)_ | false | ExpectedResponse defines a list of HTTP expected responses to match. | + + +#### HTTPClientTimeout + + + + + +_Appears in:_ +- [ClientTimeout](#clienttimeout) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `requestReceivedTimeout` | _[Duration](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Duration)_ | false | RequestReceivedTimeout is the duration envoy waits for the complete request reception. This timer starts upon request
initiation and stops when either the last byte of the request is sent upstream or when the response begins. | +| `idleTimeout` | _[Duration](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Duration)_ | false | IdleTimeout for an HTTP connection. Idle time is defined as a period in which there are no active requests in the connection.
Default: 1 hour. | + + +#### HTTPExtAuthService + + + +HTTPExtAuthService defines the HTTP External Authorization service + +_Appears in:_ +- [ExtAuth](#extauth) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `backendRef` | _[BackendObjectReference](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.BackendObjectReference)_ | true | BackendRef references a Kubernetes object that represents the
backend server to which the authorization request will be sent.
Only Service kind is supported for now.

Deprecated: Use BackendRefs instead. | +| `backendRefs` | _[BackendRef](#backendref) array_ | false | BackendRefs references a Kubernetes object that represents the
backend server to which the authorization request will be sent.
Only Service kind is supported for now. | +| `path` | _string_ | true | Path is the path of the HTTP External Authorization service.
If path is specified, the authorization request will be sent to that path,
or else the authorization request will be sent to the root path. | +| `headersToBackend` | _string array_ | false | HeadersToBackend are the authorization response headers that will be added
to the original client request before sending it to the backend server.
Note that coexisting headers will be overridden.
If not specified, no authorization response headers will be added to the
original client request. | + + +#### HTTPStatus + +_Underlying type:_ _integer_ + +HTTPStatus defines the http status code. + +_Appears in:_ +- [HTTPActiveHealthChecker](#httpactivehealthchecker) +- [RetryOn](#retryon) + + + +#### HTTPTimeout + + + + + +_Appears in:_ +- [Timeout](#timeout) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `connectionIdleTimeout` | _[Duration](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Duration)_ | false | The idle timeout for an HTTP connection. Idle time is defined as a period in which there are no active requests in the connection.
Default: 1 hour. | +| `maxConnectionDuration` | _[Duration](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Duration)_ | false | The maximum duration of an HTTP connection.
Default: unlimited. | + + +#### HTTPWasmCodeSource + + + +HTTPWasmCodeSource defines the HTTP URL containing the Wasm code. + +_Appears in:_ +- [WasmCodeSource](#wasmcodesource) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `url` | _string_ | true | URL is the URL containing the Wasm code. | +| `sha256` | _string_ | false | SHA256 checksum that will be used to verify the Wasm code.

If not specified, Envoy Gateway will not verify the downloaded Wasm code.
kubebuilder:validation:Pattern=`^[a-f0-9]{64}$` | + + +#### Header + + + +Header defines the header hashing configuration for consistent hash based +load balancing. + +_Appears in:_ +- [ConsistentHash](#consistenthash) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `name` | _string_ | true | Name of the header to hash. | + + + + +#### HeaderMatchType + +_Underlying type:_ _string_ + +HeaderMatchType specifies the semantics of how HTTP header values should be compared. +Valid HeaderMatchType values are "Exact", "RegularExpression", and "Distinct". + +_Appears in:_ +- [HeaderMatch](#headermatch) + +| Value | Description | +| ----- | ----------- | +| `Exact` | HeaderMatchExact matches the exact value of the Value field against the value of
the specified HTTP Header.
| +| `RegularExpression` | HeaderMatchRegularExpression matches a regular expression against the value of the
specified HTTP Header. The regex string must adhere to the syntax documented in
https://github.com/google/re2/wiki/Syntax.
| +| `Distinct` | HeaderMatchDistinct matches any and all possible unique values encountered in the
specified HTTP Header. Note that each unique value will receive its own rate limit
bucket.
Note: This is only supported for Global Rate Limits.
| + + +#### HeaderSettings + + + +HeaderSettings provides configuration options for headers on the listener. + +_Appears in:_ +- [ClientTrafficPolicySpec](#clienttrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `enableEnvoyHeaders` | _boolean_ | false | EnableEnvoyHeaders configures Envoy Proxy to add the "X-Envoy-" headers to requests
and responses. | +| `disableRateLimitHeaders` | _boolean_ | false | DisableRateLimitHeaders configures Envoy Proxy to omit the "X-RateLimit-" response headers
when rate limiting is enabled. | +| `xForwardedClientCert` | _[XForwardedClientCert](#xforwardedclientcert)_ | false | XForwardedClientCert configures how Envoy Proxy handle the x-forwarded-client-cert (XFCC) HTTP header.

x-forwarded-client-cert (XFCC) is an HTTP header used to forward the certificate
information of part or all of the clients or proxies that a request has flowed through,
on its way from the client to the server.

Envoy proxy may choose to sanitize/append/forward the XFCC header before proxying the request.

If not set, the default behavior is sanitizing the XFCC header. | +| `withUnderscoresAction` | _[WithUnderscoresAction](#withunderscoresaction)_ | false | WithUnderscoresAction configures the action to take when an HTTP header with underscores
is encountered. The default action is to reject the request. | +| `preserveXRequestID` | _boolean_ | false | PreserveXRequestID configures Envoy to keep the X-Request-ID header if passed for a request that is edge
(Edge request is the request from external clients to front Envoy) and not reset it, which is the current Envoy behaviour.
It defaults to false. | + + +#### HealthCheck + + + +HealthCheck configuration to decide which endpoints +are healthy and can be used for routing. + +_Appears in:_ +- [BackendTrafficPolicySpec](#backendtrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `active` | _[ActiveHealthCheck](#activehealthcheck)_ | false | Active health check configuration | +| `passive` | _[PassiveHealthCheck](#passivehealthcheck)_ | false | Passive passive check configuration | + + +#### HealthCheckSettings + + + +HealthCheckSettings provides HealthCheck configuration on the HTTP/HTTPS listener. + +_Appears in:_ +- [ClientTrafficPolicySpec](#clienttrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `path` | _string_ | true | Path specifies the HTTP path to match on for health check requests. | + + +#### IPEndpoint + + + +IPEndpoint describes TCP/UDP socket address, corresponding to Envoy's Socket Address +https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/address.proto#config-core-v3-socketaddress + +_Appears in:_ +- [BackendEndpoint](#backendendpoint) +- [ExtensionService](#extensionservice) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `address` | _string_ | true | Address defines the IP address of the backend endpoint. | +| `port` | _integer_ | true | Port defines the port of the backend endpoint. | + + +#### ImagePullPolicy + +_Underlying type:_ _string_ + +ImagePullPolicy defines the policy to use when pulling an OIC image. + +_Appears in:_ +- [WasmCodeSource](#wasmcodesource) + +| Value | Description | +| ----- | ----------- | +| `IfNotPresent` | ImagePullPolicyIfNotPresent will only pull the image if it does not already exist in the EG cache.
| +| `Always` | ImagePullPolicyAlways will pull the image when the EnvoyExtension resource version changes.
Note: EG does not update the Wasm module every time an Envoy proxy requests the Wasm module.
| + + +#### ImageWasmCodeSource + + + +ImageWasmCodeSource defines the OCI image containing the Wasm code. + +_Appears in:_ +- [WasmCodeSource](#wasmcodesource) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `url` | _string_ | true | URL is the URL of the OCI image.
URL can be in the format of `registry/image:tag` or `registry/image@sha256:digest`. | +| `sha256` | _string_ | false | SHA256 checksum that will be used to verify the OCI image.

It must match the digest of the OCI image.

If not specified, Envoy Gateway will not verify the downloaded OCI image.
kubebuilder:validation:Pattern=`^[a-f0-9]{64}$` | +| `pullSecretRef` | _[SecretObjectReference](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.SecretObjectReference)_ | false | PullSecretRef is a reference to the secret containing the credentials to pull the image.
Only support Kubernetes Secret resource from the same namespace. | + + +#### InfrastructureProviderType + +_Underlying type:_ _string_ + +InfrastructureProviderType defines the types of custom infrastructure providers supported by Envoy Gateway. + +_Appears in:_ +- [EnvoyGatewayInfrastructureProvider](#envoygatewayinfrastructureprovider) + +| Value | Description | +| ----- | ----------- | +| `Host` | InfrastructureProviderTypeHost defines the "Host" provider.
| + + +#### JSONPatchOperation + + + +JSONPatchOperation defines the JSON Patch Operation as defined in +https://datatracker.ietf.org/doc/html/rfc6902 + +_Appears in:_ +- [EnvoyJSONPatchConfig](#envoyjsonpatchconfig) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `op` | _[JSONPatchOperationType](#jsonpatchoperationtype)_ | true | Op is the type of operation to perform | +| `path` | _string_ | true | Path is the location of the target document/field where the operation will be performed
Refer to https://datatracker.ietf.org/doc/html/rfc6901 for more details. | +| `from` | _string_ | false | From is the source location of the value to be copied or moved. Only valid
for move or copy operations
Refer to https://datatracker.ietf.org/doc/html/rfc6901 for more details. | +| `value` | _[JSON](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#json-v1-apiextensions-k8s-io)_ | false | Value is the new value of the path location. The value is only used by
the `add` and `replace` operations. | + + +#### JSONPatchOperationType + +_Underlying type:_ _string_ + +JSONPatchOperationType specifies the JSON Patch operations that can be performed. + +_Appears in:_ +- [JSONPatchOperation](#jsonpatchoperation) + + + +#### JWT + + + +JWT defines the configuration for JSON Web Token (JWT) authentication. + +_Appears in:_ +- [SecurityPolicySpec](#securitypolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `optional` | _boolean_ | true | Optional determines whether a missing JWT is acceptable, defaulting to false if not specified.
Note: Even if optional is set to true, JWT authentication will still fail if an invalid JWT is presented. | +| `providers` | _[JWTProvider](#jwtprovider) array_ | true | Providers defines the JSON Web Token (JWT) authentication provider type.
When multiple JWT providers are specified, the JWT is considered valid if
any of the providers successfully validate the JWT. For additional details,
see https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/jwt_authn_filter.html. | + + +#### JWTExtractor + + + +JWTExtractor defines a custom JWT token extraction from HTTP request. +If specified, Envoy will extract the JWT token from the listed extractors (headers, cookies, or params) and validate each of them. +If any value extracted is found to be an invalid JWT, a 401 error will be returned. + +_Appears in:_ +- [JWTProvider](#jwtprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `headers` | _[JWTHeaderExtractor](#jwtheaderextractor) array_ | false | Headers represents a list of HTTP request headers to extract the JWT token from. | +| `cookies` | _string array_ | false | Cookies represents a list of cookie names to extract the JWT token from. | +| `params` | _string array_ | false | Params represents a list of query parameters to extract the JWT token from. | + + +#### JWTHeaderExtractor + + + +JWTHeaderExtractor defines an HTTP header location to extract JWT token + +_Appears in:_ +- [JWTExtractor](#jwtextractor) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `name` | _string_ | true | Name is the HTTP header name to retrieve the token | +| `valuePrefix` | _string_ | false | ValuePrefix is the prefix that should be stripped before extracting the token.
The format would be used by Envoy like "{ValuePrefix}".
For example, "Authorization: Bearer ", then the ValuePrefix="Bearer " with a space at the end. | + + +#### JWTProvider + + + +JWTProvider defines how a JSON Web Token (JWT) can be verified. + +_Appears in:_ +- [JWT](#jwt) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `name` | _string_ | true | Name defines a unique name for the JWT provider. A name can have a variety of forms,
including RFC1123 subdomains, RFC 1123 labels, or RFC 1035 labels. | +| `issuer` | _string_ | false | Issuer is the principal that issued the JWT and takes the form of a URL or email address.
For additional details, see https://tools.ietf.org/html/rfc7519#section-4.1.1 for
URL format and https://rfc-editor.org/rfc/rfc5322.html for email format. If not provided,
the JWT issuer is not checked. | +| `audiences` | _string array_ | false | Audiences is a list of JWT audiences allowed access. For additional details, see
https://tools.ietf.org/html/rfc7519#section-4.1.3. If not provided, JWT audiences
are not checked. | +| `remoteJWKS` | _[RemoteJWKS](#remotejwks)_ | true | RemoteJWKS defines how to fetch and cache JSON Web Key Sets (JWKS) from a remote
HTTP/HTTPS endpoint. | +| `claimToHeaders` | _[ClaimToHeader](#claimtoheader) array_ | false | ClaimToHeaders is a list of JWT claims that must be extracted into HTTP request headers
For examples, following config:
The claim must be of type; string, int, double, bool. Array type claims are not supported | +| `recomputeRoute` | _boolean_ | false | RecomputeRoute clears the route cache and recalculates the routing decision.
This field must be enabled if the headers generated from the claim are used for
route matching decisions. If the recomputation selects a new route, features targeting
the new matched route will be applied. | +| `extractFrom` | _[JWTExtractor](#jwtextractor)_ | false | ExtractFrom defines different ways to extract the JWT token from HTTP request.
If empty, it defaults to extract JWT token from the Authorization HTTP request header using Bearer schema
or access_token from query parameters. | + + +#### KubernetesContainerSpec + + + +KubernetesContainerSpec defines the desired state of the Kubernetes container resource. + +_Appears in:_ +- [KubernetesDaemonSetSpec](#kubernetesdaemonsetspec) +- [KubernetesDeploymentSpec](#kubernetesdeploymentspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `env` | _[EnvVar](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#envvar-v1-core) array_ | false | List of environment variables to set in the container. | +| `resources` | _[ResourceRequirements](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#resourcerequirements-v1-core)_ | false | Resources required by this container.
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ | +| `securityContext` | _[SecurityContext](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#securitycontext-v1-core)_ | false | SecurityContext defines the security options the container should be run with.
If set, the fields of SecurityContext override the equivalent fields of PodSecurityContext.
More info: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ | +| `image` | _string_ | false | Image specifies the EnvoyProxy container image to be used, instead of the default image. | +| `volumeMounts` | _[VolumeMount](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#volumemount-v1-core) array_ | false | VolumeMounts are volumes to mount into the container's filesystem.
Cannot be updated. | + + +#### KubernetesDaemonSetSpec + + + +KubernetesDaemonsetSpec defines the desired state of the Kubernetes daemonset resource. + +_Appears in:_ +- [EnvoyProxyKubernetesProvider](#envoyproxykubernetesprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `patch` | _[KubernetesPatchSpec](#kubernetespatchspec)_ | false | Patch defines how to perform the patch operation to daemonset | +| `strategy` | _[DaemonSetUpdateStrategy](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#daemonsetupdatestrategy-v1-apps)_ | false | The daemonset strategy to use to replace existing pods with new ones. | +| `pod` | _[KubernetesPodSpec](#kubernetespodspec)_ | false | Pod defines the desired specification of pod. | +| `container` | _[KubernetesContainerSpec](#kubernetescontainerspec)_ | false | Container defines the desired specification of main container. | +| `name` | _string_ | false | Name of the daemonSet.
When unset, this defaults to an autogenerated name. | + + +#### KubernetesDeployMode + + + +KubernetesDeployMode holds configuration for how to deploy managed resources such as the Envoy Proxy +data plane fleet. + +_Appears in:_ +- [EnvoyGatewayKubernetesProvider](#envoygatewaykubernetesprovider) + + + +#### KubernetesDeploymentSpec + + + +KubernetesDeploymentSpec defines the desired state of the Kubernetes deployment resource. + +_Appears in:_ +- [EnvoyGatewayKubernetesProvider](#envoygatewaykubernetesprovider) +- [EnvoyProxyKubernetesProvider](#envoyproxykubernetesprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `patch` | _[KubernetesPatchSpec](#kubernetespatchspec)_ | false | Patch defines how to perform the patch operation to deployment | +| `replicas` | _integer_ | false | Replicas is the number of desired pods. Defaults to 1. | +| `strategy` | _[DeploymentStrategy](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#deploymentstrategy-v1-apps)_ | false | The deployment strategy to use to replace existing pods with new ones. | +| `pod` | _[KubernetesPodSpec](#kubernetespodspec)_ | false | Pod defines the desired specification of pod. | +| `container` | _[KubernetesContainerSpec](#kubernetescontainerspec)_ | false | Container defines the desired specification of main container. | +| `initContainers` | _[Container](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#container-v1-core) array_ | false | List of initialization containers belonging to the pod.
More info: https://kubernetes.io/docs/concepts/workloads/pods/init-containers/ | +| `name` | _string_ | false | Name of the deployment.
When unset, this defaults to an autogenerated name. | + + +#### KubernetesHorizontalPodAutoscalerSpec + + + +KubernetesHorizontalPodAutoscalerSpec defines Kubernetes Horizontal Pod Autoscaler settings of Envoy Proxy Deployment. +When HPA is enabled, it is recommended that the value in `KubernetesDeploymentSpec.replicas` be removed, otherwise +Envoy Gateway will revert back to this value every time reconciliation occurs. +See k8s.io.autoscaling.v2.HorizontalPodAutoScalerSpec. + +_Appears in:_ +- [EnvoyProxyKubernetesProvider](#envoyproxykubernetesprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `minReplicas` | _integer_ | false | minReplicas is the lower limit for the number of replicas to which the autoscaler
can scale down. It defaults to 1 replica. | +| `maxReplicas` | _integer_ | true | maxReplicas is the upper limit for the number of replicas to which the autoscaler can scale up.
It cannot be less that minReplicas. | +| `metrics` | _[MetricSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#metricspec-v2-autoscaling) array_ | false | metrics contains the specifications for which to use to calculate the
desired replica count (the maximum replica count across all metrics will
be used).
If left empty, it defaults to being based on CPU utilization with average on 80% usage. | +| `behavior` | _[HorizontalPodAutoscalerBehavior](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#horizontalpodautoscalerbehavior-v2-autoscaling)_ | false | behavior configures the scaling behavior of the target
in both Up and Down directions (scaleUp and scaleDown fields respectively).
If not set, the default HPAScalingRules for scale up and scale down are used.
See k8s.io.autoscaling.v2.HorizontalPodAutoScalerBehavior. | + + +#### KubernetesPatchSpec + + + +KubernetesPatchSpec defines how to perform the patch operation + +_Appears in:_ +- [KubernetesDaemonSetSpec](#kubernetesdaemonsetspec) +- [KubernetesDeploymentSpec](#kubernetesdeploymentspec) +- [KubernetesServiceSpec](#kubernetesservicespec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[MergeType](#mergetype)_ | false | Type is the type of merge operation to perform

By default, StrategicMerge is used as the patch type. | +| `value` | _[JSON](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#json-v1-apiextensions-k8s-io)_ | true | Object contains the raw configuration for merged object | + + +#### KubernetesPodDisruptionBudgetSpec + + + +KubernetesPodDisruptionBudgetSpec defines Kubernetes PodDisruptionBudget settings of Envoy Proxy Deployment. + +_Appears in:_ +- [EnvoyProxyKubernetesProvider](#envoyproxykubernetesprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `minAvailable` | _integer_ | false | MinAvailable specifies the minimum number of pods that must be available at all times during voluntary disruptions,
such as node drains or updates. This setting ensures that your envoy proxy maintains a certain level of availability
and resilience during maintenance operations. | + + +#### KubernetesPodSpec + + + +KubernetesPodSpec defines the desired state of the Kubernetes pod resource. + +_Appears in:_ +- [KubernetesDaemonSetSpec](#kubernetesdaemonsetspec) +- [KubernetesDeploymentSpec](#kubernetesdeploymentspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `annotations` | _object (keys:string, values:string)_ | false | Annotations are the annotations that should be appended to the pods.
By default, no pod annotations are appended. | +| `labels` | _object (keys:string, values:string)_ | false | Labels are the additional labels that should be tagged to the pods.
By default, no additional pod labels are tagged. | +| `securityContext` | _[PodSecurityContext](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#podsecuritycontext-v1-core)_ | false | SecurityContext holds pod-level security attributes and common container settings.
Optional: Defaults to empty. See type description for default values of each field. | +| `affinity` | _[Affinity](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#affinity-v1-core)_ | false | If specified, the pod's scheduling constraints. | +| `tolerations` | _[Toleration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#toleration-v1-core) array_ | false | If specified, the pod's tolerations. | +| `volumes` | _[Volume](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#volume-v1-core) array_ | false | Volumes that can be mounted by containers belonging to the pod.
More info: https://kubernetes.io/docs/concepts/storage/volumes | +| `imagePullSecrets` | _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#localobjectreference-v1-core) array_ | false | ImagePullSecrets is an optional list of references to secrets
in the same namespace to use for pulling any of the images used by this PodSpec.
If specified, these secrets will be passed to individual puller implementations for them to use.
More info: https://kubernetes.io/docs/concepts/containers/images#specifying-imagepullsecrets-on-a-pod | +| `nodeSelector` | _object (keys:string, values:string)_ | false | NodeSelector is a selector which must be true for the pod to fit on a node.
Selector which must match a node's labels for the pod to be scheduled on that node.
More info: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/ | +| `topologySpreadConstraints` | _[TopologySpreadConstraint](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#topologyspreadconstraint-v1-core) array_ | false | TopologySpreadConstraints describes how a group of pods ought to spread across topology
domains. Scheduler will schedule pods in a way which abides by the constraints.
All topologySpreadConstraints are ANDed. | + + +#### KubernetesServiceSpec + + + +KubernetesServiceSpec defines the desired state of the Kubernetes service resource. + +_Appears in:_ +- [EnvoyProxyKubernetesProvider](#envoyproxykubernetesprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `annotations` | _object (keys:string, values:string)_ | false | Annotations that should be appended to the service.
By default, no annotations are appended. | +| `type` | _[ServiceType](#servicetype)_ | false | Type determines how the Service is exposed. Defaults to LoadBalancer.
Valid options are ClusterIP, LoadBalancer and NodePort.
"LoadBalancer" means a service will be exposed via an external load balancer (if the cloud provider supports it).
"ClusterIP" means a service will only be accessible inside the cluster, via the cluster IP.
"NodePort" means a service will be exposed on a static Port on all Nodes of the cluster. | +| `loadBalancerClass` | _string_ | false | LoadBalancerClass, when specified, allows for choosing the LoadBalancer provider
implementation if more than one are available or is otherwise expected to be specified | +| `allocateLoadBalancerNodePorts` | _boolean_ | false | AllocateLoadBalancerNodePorts defines if NodePorts will be automatically allocated for
services with type LoadBalancer. Default is "true". It may be set to "false" if the cluster
load-balancer does not rely on NodePorts. If the caller requests specific NodePorts (by specifying a
value), those requests will be respected, regardless of this field. This field may only be set for
services with type LoadBalancer and will be cleared if the type is changed to any other type. | +| `loadBalancerSourceRanges` | _string array_ | false | LoadBalancerSourceRanges defines a list of allowed IP addresses which will be configured as
firewall rules on the platform providers load balancer. This is not guaranteed to be working as
it happens outside of kubernetes and has to be supported and handled by the platform provider.
This field may only be set for services with type LoadBalancer and will be cleared if the type
is changed to any other type. | +| `loadBalancerIP` | _string_ | false | LoadBalancerIP defines the IP Address of the underlying load balancer service. This field
may be ignored if the load balancer provider does not support this feature.
This field has been deprecated in Kubernetes, but it is still used for setting the IP Address in some cloud
providers such as GCP. | +| `externalTrafficPolicy` | _[ServiceExternalTrafficPolicy](#serviceexternaltrafficpolicy)_ | false | ExternalTrafficPolicy determines the externalTrafficPolicy for the Envoy Service. Valid options
are Local and Cluster. Default is "Local". "Local" means traffic will only go to pods on the node
receiving the traffic. "Cluster" means connections are loadbalanced to all pods in the cluster. | +| `patch` | _[KubernetesPatchSpec](#kubernetespatchspec)_ | false | Patch defines how to perform the patch operation to the service | +| `name` | _string_ | false | Name of the service.
When unset, this defaults to an autogenerated name. | + + +#### KubernetesWatchMode + + + +KubernetesWatchMode holds the configuration for which input resources to watch and reconcile. + +_Appears in:_ +- [EnvoyGatewayKubernetesProvider](#envoygatewaykubernetesprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[KubernetesWatchModeType](#kuberneteswatchmodetype)_ | true | Type indicates what watch mode to use. KubernetesWatchModeTypeNamespaces and
KubernetesWatchModeTypeNamespaceSelector are currently supported
By default, when this field is unset or empty, Envoy Gateway will watch for input namespaced resources
from all namespaces. | +| `namespaces` | _string array_ | true | Namespaces holds the list of namespaces that Envoy Gateway will watch for namespaced scoped
resources such as Gateway, HTTPRoute and Service.
Note that Envoy Gateway will continue to reconcile relevant cluster scoped resources such as
GatewayClass that it is linked to. Precisely one of Namespaces and NamespaceSelector must be set. | +| `namespaceSelector` | _[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#labelselector-v1-meta)_ | true | NamespaceSelector holds the label selector used to dynamically select namespaces.
Envoy Gateway will watch for namespaces matching the specified label selector.
Precisely one of Namespaces and NamespaceSelector must be set. | + + +#### KubernetesWatchModeType + +_Underlying type:_ _string_ + +KubernetesWatchModeType defines the type of KubernetesWatchMode + +_Appears in:_ +- [KubernetesWatchMode](#kuberneteswatchmode) + + + +#### LeaderElection + + + +LeaderElection defines the desired leader election settings. + +_Appears in:_ +- [EnvoyGatewayKubernetesProvider](#envoygatewaykubernetesprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `leaseDuration` | _[Duration](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Duration)_ | true | LeaseDuration defines the time non-leader contenders will wait before attempting to claim leadership.
It's based on the timestamp of the last acknowledged signal. The default setting is 15 seconds. | +| `renewDeadline` | _[Duration](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Duration)_ | true | RenewDeadline represents the time frame within which the current leader will attempt to renew its leadership
status before relinquishing its position. The default setting is 10 seconds. | +| `retryPeriod` | _[Duration](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Duration)_ | true | RetryPeriod denotes the interval at which LeaderElector clients should perform action retries.
The default setting is 2 seconds. | +| `disable` | _boolean_ | true | Disable provides the option to turn off leader election, which is enabled by default. | + + +#### LiteralCustomTag + + + +LiteralCustomTag adds hard-coded value to each span. + +_Appears in:_ +- [CustomTag](#customtag) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `value` | _string_ | true | Value defines the hard-coded value to add to each span. | + + +#### LoadBalancer + + + +LoadBalancer defines the load balancer policy to be applied. + +_Appears in:_ +- [BackendTrafficPolicySpec](#backendtrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[LoadBalancerType](#loadbalancertype)_ | true | Type decides the type of Load Balancer policy.
Valid LoadBalancerType values are
"ConsistentHash",
"LeastRequest",
"Random",
"RoundRobin". | +| `consistentHash` | _[ConsistentHash](#consistenthash)_ | false | ConsistentHash defines the configuration when the load balancer type is
set to ConsistentHash | +| `slowStart` | _[SlowStart](#slowstart)_ | false | SlowStart defines the configuration related to the slow start load balancer policy.
If set, during slow start window, traffic sent to the newly added hosts will gradually increase.
Currently this is only supported for RoundRobin and LeastRequest load balancers | + + +#### LoadBalancerType + +_Underlying type:_ _string_ + +LoadBalancerType specifies the types of LoadBalancer. + +_Appears in:_ +- [LoadBalancer](#loadbalancer) + +| Value | Description | +| ----- | ----------- | +| `ConsistentHash` | ConsistentHashLoadBalancerType load balancer policy.
| +| `LeastRequest` | LeastRequestLoadBalancerType load balancer policy.
| +| `Random` | RandomLoadBalancerType load balancer policy.
| +| `RoundRobin` | RoundRobinLoadBalancerType load balancer policy.
| + + +#### LocalRateLimit + + + +LocalRateLimit defines local rate limit configuration. + +_Appears in:_ +- [RateLimitSpec](#ratelimitspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `rules` | _[RateLimitRule](#ratelimitrule) array_ | false | Rules are a list of RateLimit selectors and limits. If a request matches
multiple rules, the strictest limit is applied. For example, if a request
matches two rules, one with 10rps and one with 20rps, the final limit will
be based on the rule with 10rps. | + + +#### LogLevel + +_Underlying type:_ _string_ + +LogLevel defines a log level for Envoy Gateway and EnvoyProxy system logs. + +_Appears in:_ +- [EnvoyGatewayLogging](#envoygatewaylogging) +- [ProxyLogging](#proxylogging) + +| Value | Description | +| ----- | ----------- | +| `debug` | LogLevelDebug defines the "debug" logging level.
| +| `info` | LogLevelInfo defines the "Info" logging level.
| +| `warn` | LogLevelWarn defines the "Warn" logging level.
| +| `error` | LogLevelError defines the "Error" logging level.
| + + + + +#### MetricSinkType + +_Underlying type:_ _string_ + + + +_Appears in:_ +- [EnvoyGatewayMetricSink](#envoygatewaymetricsink) +- [ProxyMetricSink](#proxymetricsink) + +| Value | Description | +| ----- | ----------- | +| `OpenTelemetry` | | + + +#### OIDC + + + +OIDC defines the configuration for the OpenID Connect (OIDC) authentication. + +_Appears in:_ +- [SecurityPolicySpec](#securitypolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `provider` | _[OIDCProvider](#oidcprovider)_ | true | The OIDC Provider configuration. | +| `clientID` | _string_ | true | The client ID to be used in the OIDC
[Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). | +| `clientSecret` | _[SecretObjectReference](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.SecretObjectReference)_ | true | The Kubernetes secret which contains the OIDC client secret to be used in the
[Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest).

This is an Opaque secret. The client secret should be stored in the key
"client-secret". | +| `cookieNames` | _[OIDCCookieNames](#oidccookienames)_ | false | The optional cookie name overrides to be used for Bearer and IdToken cookies in the
[Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest).
If not specified, uses a randomly generated suffix | +| `scopes` | _string array_ | false | The OIDC scopes to be used in the
[Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest).
The "openid" scope is always added to the list of scopes if not already
specified. | +| `resources` | _string array_ | false | The OIDC resources to be used in the
[Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). | +| `redirectURL` | _string_ | true | The redirect URL to be used in the OIDC
[Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest).
If not specified, uses the default redirect URI "%REQ(x-forwarded-proto)%://%REQ(:authority)%/oauth2/callback" | +| `logoutPath` | _string_ | true | The path to log a user out, clearing their credential cookies.

If not specified, uses a default logout path "/logout" | +| `forwardAccessToken` | _boolean_ | false | ForwardAccessToken indicates whether the Envoy should forward the access token
via the Authorization header Bearer scheme to the upstream.
If not specified, defaults to false. | +| `defaultTokenTTL` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | false | DefaultTokenTTL is the default lifetime of the id token and access token.
Please note that Envoy will always use the expiry time from the response
of the authorization server if it is provided. This field is only used when
the expiry time is not provided by the authorization.

If not specified, defaults to 0. In this case, the "expires_in" field in
the authorization response must be set by the authorization server, or the
OAuth flow will fail. | +| `refreshToken` | _boolean_ | false | RefreshToken indicates whether the Envoy should automatically refresh the
id token and access token when they expire.
When set to true, the Envoy will use the refresh token to get a new id token
and access token when they expire.

If not specified, defaults to false. | +| `defaultRefreshTokenTTL` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | false | DefaultRefreshTokenTTL is the default lifetime of the refresh token.
This field is only used when the exp (expiration time) claim is omitted in
the refresh token or the refresh token is not JWT.

If not specified, defaults to 604800s (one week).
Note: this field is only applicable when the "refreshToken" field is set to true. | + + +#### OIDCCookieNames + + + +OIDCCookieNames defines the names of cookies to use in the Envoy OIDC filter. + +_Appears in:_ +- [OIDC](#oidc) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `accessToken` | _string_ | false | The name of the cookie used to store the AccessToken in the
[Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest).
If not specified, defaults to "AccessToken-(randomly generated uid)" | +| `idToken` | _string_ | false | The name of the cookie used to store the IdToken in the
[Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest).
If not specified, defaults to "IdToken-(randomly generated uid)" | + + +#### OIDCProvider + + + +OIDCProvider defines the OIDC Provider configuration. + +_Appears in:_ +- [OIDC](#oidc) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `issuer` | _string_ | true | The OIDC Provider's [issuer identifier](https://openid.net/specs/openid-connect-discovery-1_0.html#IssuerDiscovery).
Issuer MUST be a URI RFC 3986 [RFC3986] with a scheme component that MUST
be https, a host component, and optionally, port and path components and
no query or fragment components. | +| `authorizationEndpoint` | _string_ | false | The OIDC Provider's [authorization endpoint](https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint).
If not provided, EG will try to discover it from the provider's [Well-Known Configuration Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse). | +| `tokenEndpoint` | _string_ | false | The OIDC Provider's [token endpoint](https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint).
If not provided, EG will try to discover it from the provider's [Well-Known Configuration Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse). | + + +#### OpenTelemetryEnvoyProxyAccessLog + + + +OpenTelemetryEnvoyProxyAccessLog defines the OpenTelemetry access log sink. + +_Appears in:_ +- [ProxyAccessLogSink](#proxyaccesslogsink) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `host` | _string_ | false | Host define the extension service hostname.
Deprecated: Use BackendRefs instead. | +| `port` | _integer_ | false | Port defines the port the extension service is exposed on.
Deprecated: Use BackendRefs instead. | +| `backendRefs` | _[BackendRef](#backendref) array_ | false | BackendRefs references a Kubernetes object that represents the
backend server to which the access log will be sent.
Only Service kind is supported for now. | +| `resources` | _object (keys:string, values:string)_ | false | Resources is a set of labels that describe the source of a log entry, including envoy node info.
It's recommended to follow [semantic conventions](https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/). | + + +#### Origin + +_Underlying type:_ _string_ + +Origin is defined by the scheme (protocol), hostname (domain), and port of +the URL used to access it. The hostname can be "precise" which is just the +domain name or "wildcard" which is a domain name prefixed with a single +wildcard label such as "*.example.com". +In addition to that a single wildcard (with or without scheme) can be +configured to match any origin. + + +For example, the following are valid origins: +- https://foo.example.com +- https://*.example.com +- http://foo.example.com:8080 +- http://*.example.com:8080 +- https://* + +_Appears in:_ +- [CORS](#cors) + + + +#### PassiveHealthCheck + + + +PassiveHealthCheck defines the configuration for passive health checks in the context of Envoy's Outlier Detection, +see https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/outlier + +_Appears in:_ +- [HealthCheck](#healthcheck) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `splitExternalLocalOriginErrors` | _boolean_ | false | SplitExternalLocalOriginErrors enables splitting of errors between external and local origin. | +| `interval` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | false | Interval defines the time between passive health checks. | +| `consecutiveLocalOriginFailures` | _integer_ | false | ConsecutiveLocalOriginFailures sets the number of consecutive local origin failures triggering ejection.
Parameter takes effect only when split_external_local_origin_errors is set to true. | +| `consecutiveGatewayErrors` | _integer_ | false | ConsecutiveGatewayErrors sets the number of consecutive gateway errors triggering ejection. | +| `consecutive5XxErrors` | _integer_ | false | Consecutive5xxErrors sets the number of consecutive 5xx errors triggering ejection. | +| `baseEjectionTime` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | false | BaseEjectionTime defines the base duration for which a host will be ejected on consecutive failures. | +| `maxEjectionPercent` | _integer_ | false | MaxEjectionPercent sets the maximum percentage of hosts in a cluster that can be ejected. | + + +#### PathEscapedSlashAction + +_Underlying type:_ _string_ + +PathEscapedSlashAction determines the action for requests that contain %2F, %2f, %5C, or %5c +sequences in the URI path. + +_Appears in:_ +- [PathSettings](#pathsettings) + +| Value | Description | +| ----- | ----------- | +| `KeepUnchanged` | KeepUnchangedAction keeps escaped slashes as they arrive without changes
| +| `RejectRequest` | RejectRequestAction rejects client requests containing escaped slashes
with a 400 status. gRPC requests will be rejected with the INTERNAL (13)
error code.
The "httpN.downstream_rq_failed_path_normalization" counter is incremented
for each rejected request.
| +| `UnescapeAndRedirect` | UnescapeAndRedirect unescapes %2F and %5C sequences and redirects to the new path
if these sequences were present.
Redirect occurs after path normalization and merge slashes transformations if
they were configured. gRPC requests will be rejected with the INTERNAL (13)
error code.
This option minimizes possibility of path confusion exploits by forcing request
with unescaped slashes to traverse all parties: downstream client, intermediate
proxies, Envoy and upstream server.
The “httpN.downstream_rq_redirected_with_normalized_path” counter is incremented
for each redirected request.
| +| `UnescapeAndForward` | UnescapeAndForward unescapes %2F and %5C sequences and forwards the request.
Note: this option should not be enabled if intermediaries perform path based access
control as it may lead to path confusion vulnerabilities.
| + + +#### PathSettings + + + +PathSettings provides settings that managing how the incoming path set by clients is handled. + +_Appears in:_ +- [ClientTrafficPolicySpec](#clienttrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `escapedSlashesAction` | _[PathEscapedSlashAction](#pathescapedslashaction)_ | false | EscapedSlashesAction determines how %2f, %2F, %5c, or %5C sequences in the path URI
should be handled.
The default is UnescapeAndRedirect. | +| `disableMergeSlashes` | _boolean_ | false | DisableMergeSlashes allows disabling the default configuration of merging adjacent
slashes in the path.
Note that slash merging is not part of the HTTP spec and is provided for convenience. | + + +#### PerRetryPolicy + + + + + +_Appears in:_ +- [Retry](#retry) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `timeout` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | false | Timeout is the timeout per retry attempt. | +| `backOff` | _[BackOffPolicy](#backoffpolicy)_ | false | Backoff is the backoff policy to be applied per retry attempt. gateway uses a fully jittered exponential
back-off algorithm for retries. For additional details,
see https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/router_filter#config-http-filters-router-x-envoy-max-retries | + + +#### PolicyTargetReferences + + + + + +_Appears in:_ +- [BackendTrafficPolicySpec](#backendtrafficpolicyspec) +- [ClientTrafficPolicySpec](#clienttrafficpolicyspec) +- [EnvoyExtensionPolicySpec](#envoyextensionpolicyspec) +- [SecurityPolicySpec](#securitypolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `targetRef` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName)_ | true | TargetRef is the name of the resource this policy is being attached to.
This policy and the TargetRef MUST be in the same namespace for this
Policy to have effect

Deprecated: use targetRefs/targetSelectors instead | +| `targetRefs` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName) array_ | true | TargetRefs are the names of the Gateway resources this policy
is being attached to. | +| `targetSelectors` | _[TargetSelector](#targetselector) array_ | true | TargetSelectors allow targeting resources for this policy based on labels | + + +#### Principal + + + +Principal specifies the client identity of a request. +A client identity can be a client IP, a JWT claim, username from the Authorization header, +or any other identity that can be extracted from a custom header. +Currently, only the client IP is supported. + +_Appears in:_ +- [AuthorizationRule](#authorizationrule) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `clientCIDRs` | _[CIDR](#cidr) array_ | true | ClientCIDRs are the IP CIDR ranges of the client.
Valid examples are "192.168.1.0/24" or "2001:db8::/64"

The client IP is inferred from the X-Forwarded-For header, a custom header,
or the proxy protocol.
You can use the `ClientIPDetection` or the `EnableProxyProtocol` field in
the `ClientTrafficPolicy` to configure how the client IP is detected. | + + +#### ProcessingModeOptions + + + +ProcessingModeOptions defines if headers or body should be processed by the external service + +_Appears in:_ +- [ExtProcProcessingMode](#extprocprocessingmode) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `body` | _[ExtProcBodyProcessingMode](#extprocbodyprocessingmode)_ | false | Defines body processing mode | + + +#### ProviderType + +_Underlying type:_ _string_ + +ProviderType defines the types of providers supported by Envoy Gateway. + +_Appears in:_ +- [EnvoyGatewayProvider](#envoygatewayprovider) +- [EnvoyProxyProvider](#envoyproxyprovider) + +| Value | Description | +| ----- | ----------- | +| `Kubernetes` | ProviderTypeKubernetes defines the "Kubernetes" provider.
| +| `File` | ProviderTypeFile defines the "File" provider. This type is not implemented
until https://github.com/envoyproxy/gateway/issues/1001 is fixed.
| + + +#### ProxyAccessLog + + + + + +_Appears in:_ +- [ProxyTelemetry](#proxytelemetry) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `disable` | _boolean_ | true | Disable disables access logging for managed proxies if set to true. | +| `settings` | _[ProxyAccessLogSetting](#proxyaccesslogsetting) array_ | false | Settings defines accesslog settings for managed proxies.
If unspecified, will send default format to stdout. | + + +#### ProxyAccessLogFormat + + + +ProxyAccessLogFormat defines the format of accesslog. +By default accesslogs are written to standard output. + +_Appears in:_ +- [ProxyAccessLogSetting](#proxyaccesslogsetting) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[ProxyAccessLogFormatType](#proxyaccesslogformattype)_ | true | Type defines the type of accesslog format. | +| `text` | _string_ | false | Text defines the text accesslog format, following Envoy accesslog formatting,
It's required when the format type is "Text".
Envoy [command operators](https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#command-operators) may be used in the format.
The [format string documentation](https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#config-access-log-format-strings) provides more information. | +| `json` | _object (keys:string, values:string)_ | false | JSON is additional attributes that describe the specific event occurrence.
Structured format for the envoy access logs. Envoy [command operators](https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#command-operators)
can be used as values for fields within the Struct.
It's required when the format type is "JSON". | + + +#### ProxyAccessLogFormatType + +_Underlying type:_ _string_ + + + +_Appears in:_ +- [ProxyAccessLogFormat](#proxyaccesslogformat) + +| Value | Description | +| ----- | ----------- | +| `Text` | ProxyAccessLogFormatTypeText defines the text accesslog format.
| +| `JSON` | ProxyAccessLogFormatTypeJSON defines the JSON accesslog format.
| + + +#### ProxyAccessLogSetting + + + + + +_Appears in:_ +- [ProxyAccessLog](#proxyaccesslog) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `format` | _[ProxyAccessLogFormat](#proxyaccesslogformat)_ | false | Format defines the format of accesslog.
This will be ignored if sink type is ALS. | +| `sinks` | _[ProxyAccessLogSink](#proxyaccesslogsink) array_ | true | Sinks defines the sinks of accesslog. | + + +#### ProxyAccessLogSink + + + +ProxyAccessLogSink defines the sink of accesslog. + +_Appears in:_ +- [ProxyAccessLogSetting](#proxyaccesslogsetting) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[ProxyAccessLogSinkType](#proxyaccesslogsinktype)_ | true | Type defines the type of accesslog sink. | +| `als` | _[ALSEnvoyProxyAccessLog](#alsenvoyproxyaccesslog)_ | false | ALS defines the gRPC Access Log Service (ALS) sink. | +| `file` | _[FileEnvoyProxyAccessLog](#fileenvoyproxyaccesslog)_ | false | File defines the file accesslog sink. | +| `openTelemetry` | _[OpenTelemetryEnvoyProxyAccessLog](#opentelemetryenvoyproxyaccesslog)_ | false | OpenTelemetry defines the OpenTelemetry accesslog sink. | + + +#### ProxyAccessLogSinkType + +_Underlying type:_ _string_ + + + +_Appears in:_ +- [ProxyAccessLogSink](#proxyaccesslogsink) + +| Value | Description | +| ----- | ----------- | +| `ALS` | ProxyAccessLogSinkTypeALS defines the gRPC Access Log Service (ALS) sink.
The service must implement the Envoy gRPC Access Log Service streaming API:
https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/accesslog/v3/als.proto
| +| `File` | ProxyAccessLogSinkTypeFile defines the file accesslog sink.
| +| `OpenTelemetry` | ProxyAccessLogSinkTypeOpenTelemetry defines the OpenTelemetry accesslog sink.
When the provider is Kubernetes, EnvoyGateway always sends `k8s.namespace.name`
and `k8s.pod.name` as additional attributes.
| + + +#### ProxyBootstrap + + + +ProxyBootstrap defines Envoy Bootstrap configuration. + +_Appears in:_ +- [EnvoyProxySpec](#envoyproxyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[BootstrapType](#bootstraptype)_ | false | Type is the type of the bootstrap configuration, it should be either Replace or Merge.
If unspecified, it defaults to Replace. | +| `value` | _string_ | true | Value is a YAML string of the bootstrap. | + + +#### ProxyLogComponent + +_Underlying type:_ _string_ + +ProxyLogComponent defines a component that supports a configured logging level. + +_Appears in:_ +- [ProxyLogging](#proxylogging) + +| Value | Description | +| ----- | ----------- | +| `default` | LogComponentDefault defines the default logging component.
See more details: https://www.envoyproxy.io/docs/envoy/latest/operations/cli#cmdoption-l
| +| `upstream` | LogComponentUpstream defines the "upstream" logging component.
| +| `http` | LogComponentHTTP defines the "http" logging component.
| +| `connection` | LogComponentConnection defines the "connection" logging component.
| +| `admin` | LogComponentAdmin defines the "admin" logging component.
| +| `client` | LogComponentClient defines the "client" logging component.
| +| `filter` | LogComponentFilter defines the "filter" logging component.
| +| `main` | LogComponentMain defines the "main" logging component.
| +| `router` | LogComponentRouter defines the "router" logging component.
| +| `runtime` | LogComponentRuntime defines the "runtime" logging component.
| + + +#### ProxyLogging + + + +ProxyLogging defines logging parameters for managed proxies. + +_Appears in:_ +- [EnvoyProxySpec](#envoyproxyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `level` | _object (keys:[ProxyLogComponent](#proxylogcomponent), values:[LogLevel](#loglevel))_ | true | Level is a map of logging level per component, where the component is the key
and the log level is the value. If unspecified, defaults to "default: warn". | + + +#### ProxyMetricSink + + + +ProxyMetricSink defines the sink of metrics. +Default metrics sink is OpenTelemetry. + +_Appears in:_ +- [ProxyMetrics](#proxymetrics) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[MetricSinkType](#metricsinktype)_ | true | Type defines the metric sink type.
EG currently only supports OpenTelemetry. | +| `openTelemetry` | _[ProxyOpenTelemetrySink](#proxyopentelemetrysink)_ | false | OpenTelemetry defines the configuration for OpenTelemetry sink.
It's required if the sink type is OpenTelemetry. | + + +#### ProxyMetrics + + + + + +_Appears in:_ +- [ProxyTelemetry](#proxytelemetry) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `prometheus` | _[ProxyPrometheusProvider](#proxyprometheusprovider)_ | true | Prometheus defines the configuration for Admin endpoint `/stats/prometheus`. | +| `sinks` | _[ProxyMetricSink](#proxymetricsink) array_ | true | Sinks defines the metric sinks where metrics are sent to. | +| `matches` | _[StringMatch](#stringmatch) array_ | true | Matches defines configuration for selecting specific metrics instead of generating all metrics stats
that are enabled by default. This helps reduce CPU and memory overhead in Envoy, but eliminating some stats
may after critical functionality. Here are the stats that we strongly recommend not disabling:
`cluster_manager.warming_clusters`, `cluster..membership_total`,`cluster..membership_healthy`,
`cluster..membership_degraded`,reference https://github.com/envoyproxy/envoy/issues/9856,
https://github.com/envoyproxy/envoy/issues/14610 | +| `enableVirtualHostStats` | _boolean_ | true | EnableVirtualHostStats enables envoy stat metrics for virtual hosts. | +| `enablePerEndpointStats` | _boolean_ | true | EnablePerEndpointStats enables per endpoint envoy stats metrics.
Please use with caution. | + + +#### ProxyOpenTelemetrySink + + + +ProxyOpenTelemetrySink defines the configuration for OpenTelemetry sink. + +_Appears in:_ +- [ProxyMetricSink](#proxymetricsink) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `host` | _string_ | false | Host define the service hostname.
Deprecated: Use BackendRefs instead. | +| `port` | _integer_ | false | Port defines the port the service is exposed on.
Deprecated: Use BackendRefs instead. | +| `backendRefs` | _[BackendRef](#backendref) array_ | false | BackendRefs references a Kubernetes object that represents the
backend server to which the metric will be sent.
Only Service kind is supported for now. | + + +#### ProxyPrometheusProvider + + + + + +_Appears in:_ +- [ProxyMetrics](#proxymetrics) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `disable` | _boolean_ | true | Disable the Prometheus endpoint. | +| `compression` | _[Compression](#compression)_ | false | Configure the compression on Prometheus endpoint. Compression is useful in situations when bandwidth is scarce and large payloads can be effectively compressed at the expense of higher CPU load. | + + +#### ProxyProtocol + + + +ProxyProtocol defines the configuration related to the proxy protocol +when communicating with the backend. + +_Appears in:_ +- [BackendTrafficPolicySpec](#backendtrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `version` | _[ProxyProtocolVersion](#proxyprotocolversion)_ | true | Version of ProxyProtol
Valid ProxyProtocolVersion values are
"V1"
"V2" | + + +#### ProxyProtocolVersion + +_Underlying type:_ _string_ + +ProxyProtocolVersion defines the version of the Proxy Protocol to use. + +_Appears in:_ +- [ProxyProtocol](#proxyprotocol) + +| Value | Description | +| ----- | ----------- | +| `V1` | ProxyProtocolVersionV1 is the PROXY protocol version 1 (human readable format).
| +| `V2` | ProxyProtocolVersionV2 is the PROXY protocol version 2 (binary format).
| + + +#### ProxyTelemetry + + + + + +_Appears in:_ +- [EnvoyProxySpec](#envoyproxyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `accessLog` | _[ProxyAccessLog](#proxyaccesslog)_ | false | AccessLogs defines accesslog parameters for managed proxies.
If unspecified, will send default format to stdout. | +| `tracing` | _[ProxyTracing](#proxytracing)_ | false | Tracing defines tracing configuration for managed proxies.
If unspecified, will not send tracing data. | +| `metrics` | _[ProxyMetrics](#proxymetrics)_ | true | Metrics defines metrics configuration for managed proxies. | + + +#### ProxyTracing + + + + + +_Appears in:_ +- [ProxyTelemetry](#proxytelemetry) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `samplingRate` | _integer_ | false | SamplingRate controls the rate at which traffic will be
selected for tracing if no prior sampling decision has been made.
Defaults to 100, valid values [0-100]. 100 indicates 100% sampling. | +| `customTags` | _object (keys:string, values:[CustomTag](#customtag))_ | true | CustomTags defines the custom tags to add to each span.
If provider is kubernetes, pod name and namespace are added by default. | +| `provider` | _[TracingProvider](#tracingprovider)_ | true | Provider defines the tracing provider. | + + +#### RateLimit + + + +RateLimit defines the configuration associated with the Rate Limit Service +used for Global Rate Limiting. + +_Appears in:_ +- [EnvoyGateway](#envoygateway) +- [EnvoyGatewaySpec](#envoygatewayspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `backend` | _[RateLimitDatabaseBackend](#ratelimitdatabasebackend)_ | true | Backend holds the configuration associated with the
database backend used by the rate limit service to store
state associated with global ratelimiting. | +| `timeout` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | false | Timeout specifies the timeout period for the proxy to access the ratelimit server
If not set, timeout is 20ms. | +| `failClosed` | _boolean_ | true | FailClosed is a switch used to control the flow of traffic
when the response from the ratelimit server cannot be obtained.
If FailClosed is false, let the traffic pass,
otherwise, don't let the traffic pass and return 500.
If not set, FailClosed is False. | +| `telemetry` | _[RateLimitTelemetry](#ratelimittelemetry)_ | false | Telemetry defines telemetry configuration for RateLimit. | + + +#### RateLimitDatabaseBackend + + + +RateLimitDatabaseBackend defines the configuration associated with +the database backend used by the rate limit service. + +_Appears in:_ +- [RateLimit](#ratelimit) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[RateLimitDatabaseBackendType](#ratelimitdatabasebackendtype)_ | true | Type is the type of database backend to use. Supported types are:
* Redis: Connects to a Redis database. | +| `redis` | _[RateLimitRedisSettings](#ratelimitredissettings)_ | false | Redis defines the settings needed to connect to a Redis database. | + + +#### RateLimitDatabaseBackendType + +_Underlying type:_ _string_ + +RateLimitDatabaseBackendType specifies the types of database backend +to be used by the rate limit service. + +_Appears in:_ +- [RateLimitDatabaseBackend](#ratelimitdatabasebackend) + +| Value | Description | +| ----- | ----------- | +| `Redis` | RedisBackendType uses a redis database for the rate limit service.
| + + +#### RateLimitMetrics + + + + + +_Appears in:_ +- [RateLimitTelemetry](#ratelimittelemetry) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `prometheus` | _[RateLimitMetricsPrometheusProvider](#ratelimitmetricsprometheusprovider)_ | true | Prometheus defines the configuration for prometheus endpoint. | + + +#### RateLimitMetricsPrometheusProvider + + + + + +_Appears in:_ +- [RateLimitMetrics](#ratelimitmetrics) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `disable` | _boolean_ | true | Disable the Prometheus endpoint. | + + +#### RateLimitRedisSettings + + + +RateLimitRedisSettings defines the configuration for connecting to redis database. + +_Appears in:_ +- [RateLimitDatabaseBackend](#ratelimitdatabasebackend) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `url` | _string_ | true | URL of the Redis Database. | +| `tls` | _[RedisTLSSettings](#redistlssettings)_ | false | TLS defines TLS configuration for connecting to redis database. | + + +#### RateLimitRule + + + +RateLimitRule defines the semantics for matching attributes +from the incoming requests, and setting limits for them. + +_Appears in:_ +- [GlobalRateLimit](#globalratelimit) +- [LocalRateLimit](#localratelimit) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `clientSelectors` | _[RateLimitSelectCondition](#ratelimitselectcondition) array_ | false | ClientSelectors holds the list of select conditions to select
specific clients using attributes from the traffic flow.
All individual select conditions must hold True for this rule
and its limit to be applied.

If no client selectors are specified, the rule applies to all traffic of
the targeted Route.

If the policy targets a Gateway, the rule applies to each Route of the Gateway.
Please note that each Route has its own rate limit counters. For example,
if a Gateway has two Routes, and the policy has a rule with limit 10rps,
each Route will have its own 10rps limit. | +| `limit` | _[RateLimitValue](#ratelimitvalue)_ | true | Limit holds the rate limit values.
This limit is applied for traffic flows when the selectors
compute to True, causing the request to be counted towards the limit.
The limit is enforced and the request is ratelimited, i.e. a response with
429 HTTP status code is sent back to the client when
the selected requests have reached the limit. | + + +#### RateLimitSelectCondition + + + +RateLimitSelectCondition specifies the attributes within the traffic flow that can +be used to select a subset of clients to be ratelimited. +All the individual conditions must hold True for the overall condition to hold True. + +_Appears in:_ +- [RateLimitRule](#ratelimitrule) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `headers` | _[HeaderMatch](#headermatch) array_ | false | Headers is a list of request headers to match. Multiple header values are ANDed together,
meaning, a request MUST match all the specified headers.
At least one of headers or sourceCIDR condition must be specified. | +| `sourceCIDR` | _[SourceMatch](#sourcematch)_ | false | SourceCIDR is the client IP Address range to match on.
At least one of headers or sourceCIDR condition must be specified. | + + +#### RateLimitSpec + + + +RateLimitSpec defines the desired state of RateLimitSpec. + +_Appears in:_ +- [BackendTrafficPolicySpec](#backendtrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[RateLimitType](#ratelimittype)_ | true | Type decides the scope for the RateLimits.
Valid RateLimitType values are "Global" or "Local". | +| `global` | _[GlobalRateLimit](#globalratelimit)_ | false | Global defines global rate limit configuration. | +| `local` | _[LocalRateLimit](#localratelimit)_ | false | Local defines local rate limit configuration. | + + +#### RateLimitTelemetry + + + + + +_Appears in:_ +- [RateLimit](#ratelimit) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `metrics` | _[RateLimitMetrics](#ratelimitmetrics)_ | true | Metrics defines metrics configuration for RateLimit. | +| `tracing` | _[RateLimitTracing](#ratelimittracing)_ | true | Tracing defines traces configuration for RateLimit. | + + +#### RateLimitTracing + + + + + +_Appears in:_ +- [RateLimitTelemetry](#ratelimittelemetry) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `samplingRate` | _integer_ | false | SamplingRate controls the rate at which traffic will be
selected for tracing if no prior sampling decision has been made.
Defaults to 100, valid values [0-100]. 100 indicates 100% sampling. | +| `provider` | _[RateLimitTracingProvider](#ratelimittracingprovider)_ | true | Provider defines the rateLimit tracing provider.
Only OpenTelemetry is supported currently. | + + +#### RateLimitTracingProvider + + + +RateLimitTracingProvider defines the tracing provider configuration of RateLimit + +_Appears in:_ +- [RateLimitTracing](#ratelimittracing) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[RateLimitTracingProviderType](#ratelimittracingprovidertype)_ | true | Type defines the tracing provider type.
Since to RateLimit Exporter currently using OpenTelemetry, only OpenTelemetry is supported | +| `url` | _string_ | true | URL is the endpoint of the trace collector that supports the OTLP protocol | + + + + +#### RateLimitType + +_Underlying type:_ _string_ + +RateLimitType specifies the types of RateLimiting. + +_Appears in:_ +- [RateLimitSpec](#ratelimitspec) + +| Value | Description | +| ----- | ----------- | +| `Global` | GlobalRateLimitType allows the rate limits to be applied across all Envoy
proxy instances.
| +| `Local` | LocalRateLimitType allows the rate limits to be applied on a per Envoy
proxy instance basis.
| + + +#### RateLimitUnit + +_Underlying type:_ _string_ + +RateLimitUnit specifies the intervals for setting rate limits. +Valid RateLimitUnit values are "Second", "Minute", "Hour", and "Day". + +_Appears in:_ +- [RateLimitValue](#ratelimitvalue) + +| Value | Description | +| ----- | ----------- | +| `Second` | RateLimitUnitSecond specifies the rate limit interval to be 1 second.
| +| `Minute` | RateLimitUnitMinute specifies the rate limit interval to be 1 minute.
| +| `Hour` | RateLimitUnitHour specifies the rate limit interval to be 1 hour.
| +| `Day` | RateLimitUnitDay specifies the rate limit interval to be 1 day.
| + + +#### RateLimitValue + + + +RateLimitValue defines the limits for rate limiting. + +_Appears in:_ +- [RateLimitRule](#ratelimitrule) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `requests` | _integer_ | true | | +| `unit` | _[RateLimitUnit](#ratelimitunit)_ | true | | + + +#### RedisTLSSettings + + + +RedisTLSSettings defines the TLS configuration for connecting to redis database. + +_Appears in:_ +- [RateLimitRedisSettings](#ratelimitredissettings) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `certificateRef` | _[SecretObjectReference](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.SecretObjectReference)_ | false | CertificateRef defines the client certificate reference for TLS connections.
Currently only a Kubernetes Secret of type TLS is supported. | + + +#### RemoteJWKS + + + +RemoteJWKS defines how to fetch and cache JSON Web Key Sets (JWKS) from a remote +HTTP/HTTPS endpoint. + +_Appears in:_ +- [JWTProvider](#jwtprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `uri` | _string_ | true | URI is the HTTPS URI to fetch the JWKS. Envoy's system trust bundle is used to
validate the server certificate. | + + +#### RequestHeaderCustomTag + + + +RequestHeaderCustomTag adds value from request header to each span. + +_Appears in:_ +- [CustomTag](#customtag) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `name` | _string_ | true | Name defines the name of the request header which to extract the value from. | +| `defaultValue` | _string_ | false | DefaultValue defines the default value to use if the request header is not set. | + + +#### ResourceProviderType + +_Underlying type:_ _string_ + +ResourceProviderType defines the types of custom resource providers supported by Envoy Gateway. + +_Appears in:_ +- [EnvoyGatewayResourceProvider](#envoygatewayresourceprovider) + +| Value | Description | +| ----- | ----------- | +| `File` | ResourceProviderTypeFile defines the "File" provider.
| + + +#### Retry + + + +Retry defines the retry strategy to be applied. + +_Appears in:_ +- [BackendTrafficPolicySpec](#backendtrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `numRetries` | _integer_ | false | NumRetries is the number of retries to be attempted. Defaults to 2. | +| `retryOn` | _[RetryOn](#retryon)_ | false | RetryOn specifies the retry trigger condition.

If not specified, the default is to retry on connect-failure,refused-stream,unavailable,cancelled,retriable-status-codes(503). | +| `perRetry` | _[PerRetryPolicy](#perretrypolicy)_ | false | PerRetry is the retry policy to be applied per retry attempt. | + + +#### RetryOn + + + + + +_Appears in:_ +- [Retry](#retry) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `triggers` | _[TriggerEnum](#triggerenum) array_ | false | Triggers specifies the retry trigger condition(Http/Grpc). | +| `httpStatusCodes` | _[HTTPStatus](#httpstatus) array_ | false | HttpStatusCodes specifies the http status codes to be retried.
The retriable-status-codes trigger must also be configured for these status codes to trigger a retry. | + + +#### RoutingType + +_Underlying type:_ _string_ + +RoutingType defines the type of routing of this Envoy proxy. + +_Appears in:_ +- [EnvoyProxySpec](#envoyproxyspec) + +| Value | Description | +| ----- | ----------- | +| `Service` | ServiceRoutingType is the RoutingType for Service Cluster IP routing.
| +| `Endpoint` | EndpointRoutingType is the RoutingType for Endpoint routing.
| + + +#### SecurityPolicy + + + +SecurityPolicy allows the user to configure various security settings for a +Gateway. + +_Appears in:_ +- [SecurityPolicyList](#securitypolicylist) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `apiVersion` | _string_ | |`gateway.envoyproxy.io/v1alpha1` +| `kind` | _string_ | |`SecurityPolicy` +| `metadata` | _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#objectmeta-v1-meta)_ | true | Refer to Kubernetes API documentation for fields of `metadata`. | +| `spec` | _[SecurityPolicySpec](#securitypolicyspec)_ | true | Spec defines the desired state of SecurityPolicy. | +| `status` | _[PolicyStatus](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.PolicyStatus)_ | true | Status defines the current status of SecurityPolicy. | + + +#### SecurityPolicyList + + + +SecurityPolicyList contains a list of SecurityPolicy resources. + + + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `apiVersion` | _string_ | |`gateway.envoyproxy.io/v1alpha1` +| `kind` | _string_ | |`SecurityPolicyList` +| `metadata` | _[ListMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#listmeta-v1-meta)_ | true | Refer to Kubernetes API documentation for fields of `metadata`. | +| `items` | _[SecurityPolicy](#securitypolicy) array_ | true | | + + +#### SecurityPolicySpec + + + +SecurityPolicySpec defines the desired state of SecurityPolicy. + +_Appears in:_ +- [SecurityPolicy](#securitypolicy) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `targetRef` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName)_ | true | TargetRef is the name of the resource this policy is being attached to.
This policy and the TargetRef MUST be in the same namespace for this
Policy to have effect

Deprecated: use targetRefs/targetSelectors instead | +| `targetRefs` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName) array_ | true | TargetRefs are the names of the Gateway resources this policy
is being attached to. | +| `targetSelectors` | _[TargetSelector](#targetselector) array_ | true | TargetSelectors allow targeting resources for this policy based on labels | +| `cors` | _[CORS](#cors)_ | false | CORS defines the configuration for Cross-Origin Resource Sharing (CORS). | +| `basicAuth` | _[BasicAuth](#basicauth)_ | false | BasicAuth defines the configuration for the HTTP Basic Authentication. | +| `jwt` | _[JWT](#jwt)_ | false | JWT defines the configuration for JSON Web Token (JWT) authentication. | +| `oidc` | _[OIDC](#oidc)_ | false | OIDC defines the configuration for the OpenID Connect (OIDC) authentication. | +| `extAuth` | _[ExtAuth](#extauth)_ | false | ExtAuth defines the configuration for External Authorization. | + + +#### ServiceExternalTrafficPolicy + +_Underlying type:_ _string_ + +ServiceExternalTrafficPolicy describes how nodes distribute service traffic they +receive on one of the Service's "externally-facing" addresses (NodePorts, ExternalIPs, +and LoadBalancer IPs. + +_Appears in:_ +- [KubernetesServiceSpec](#kubernetesservicespec) + +| Value | Description | +| ----- | ----------- | +| `Cluster` | ServiceExternalTrafficPolicyCluster routes traffic to all endpoints.
| +| `Local` | ServiceExternalTrafficPolicyLocal preserves the source IP of the traffic by
routing only to endpoints on the same node as the traffic was received on
(dropping the traffic if there are no local endpoints).
| + + +#### ServiceType + +_Underlying type:_ _string_ + +ServiceType string describes ingress methods for a service + +_Appears in:_ +- [KubernetesServiceSpec](#kubernetesservicespec) + +| Value | Description | +| ----- | ----------- | +| `ClusterIP` | ServiceTypeClusterIP means a service will only be accessible inside the
cluster, via the cluster IP.
| +| `LoadBalancer` | ServiceTypeLoadBalancer means a service will be exposed via an
external load balancer (if the cloud provider supports it).
| +| `NodePort` | ServiceTypeNodePort means a service will be exposed on each Kubernetes Node
at a static Port, common across all Nodes.
| + + +#### ShutdownConfig + + + +ShutdownConfig defines configuration for graceful envoy shutdown process. + +_Appears in:_ +- [EnvoyProxySpec](#envoyproxyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `drainTimeout` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | false | DrainTimeout defines the graceful drain timeout. This should be less than the pod's terminationGracePeriodSeconds.
If unspecified, defaults to 600 seconds. | +| `minDrainDuration` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | false | MinDrainDuration defines the minimum drain duration allowing time for endpoint deprogramming to complete.
If unspecified, defaults to 5 seconds. | + + +#### ShutdownManager + + + +ShutdownManager defines the configuration for the shutdown manager. + +_Appears in:_ +- [EnvoyGatewayKubernetesProvider](#envoygatewaykubernetesprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `image` | _string_ | true | Image specifies the ShutdownManager container image to be used, instead of the default image. | + + +#### SlowStart + + + +SlowStart defines the configuration related to the slow start load balancer policy. + +_Appears in:_ +- [LoadBalancer](#loadbalancer) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `window` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | true | Window defines the duration of the warm up period for newly added host.
During slow start window, traffic sent to the newly added hosts will gradually increase.
Currently only supports linear growth of traffic. For additional details,
see https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/cluster.proto#config-cluster-v3-cluster-slowstartconfig | + + + + +#### SourceMatchType + +_Underlying type:_ _string_ + + + +_Appears in:_ +- [SourceMatch](#sourcematch) + +| Value | Description | +| ----- | ----------- | +| `Exact` | SourceMatchExact All IP Addresses within the specified Source IP CIDR are treated as a single client selector
and share the same rate limit bucket.
| +| `Distinct` | SourceMatchDistinct Each IP Address within the specified Source IP CIDR is treated as a distinct client selector
and uses a separate rate limit bucket/counter.
Note: This is only supported for Global Rate Limits.
| + + +#### StringMatch + + + +StringMatch defines how to match any strings. +This is a general purpose match condition that can be used by other EG APIs +that need to match against a string. + +_Appears in:_ +- [ProxyMetrics](#proxymetrics) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[StringMatchType](#stringmatchtype)_ | false | Type specifies how to match against a string. | +| `value` | _string_ | true | Value specifies the string value that the match must have. | + + +#### StringMatchType + +_Underlying type:_ _string_ + +StringMatchType specifies the semantics of how a string value should be compared. +Valid MatchType values are "Exact", "Prefix", "Suffix", "RegularExpression". + +_Appears in:_ +- [StringMatch](#stringmatch) + +| Value | Description | +| ----- | ----------- | +| `Exact` | StringMatchExact :the input string must match exactly the match value.
| +| `Prefix` | StringMatchPrefix :the input string must start with the match value.
| +| `Suffix` | StringMatchSuffix :the input string must end with the match value.
| +| `RegularExpression` | StringMatchRegularExpression :The input string must match the regular expression
specified in the match value.
The regex string must adhere to the syntax documented in
https://github.com/google/re2/wiki/Syntax.
| + + +#### TCPActiveHealthChecker + + + +TCPActiveHealthChecker defines the settings of tcp health check. + +_Appears in:_ +- [ActiveHealthCheck](#activehealthcheck) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `send` | _[ActiveHealthCheckPayload](#activehealthcheckpayload)_ | false | Send defines the request payload. | +| `receive` | _[ActiveHealthCheckPayload](#activehealthcheckpayload)_ | false | Receive defines the expected response payload. | + + +#### TCPClientTimeout + + + +TCPClientTimeout only provides timeout configuration on the listener whose protocol is TCP or TLS. + +_Appears in:_ +- [ClientTimeout](#clienttimeout) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `idleTimeout` | _[Duration](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Duration)_ | false | IdleTimeout for a TCP connection. Idle time is defined as a period in which there are no
bytes sent or received on either the upstream or downstream connection.
Default: 1 hour. | + + +#### TCPKeepalive + + + +TCPKeepalive define the TCP Keepalive configuration. + +_Appears in:_ +- [BackendTrafficPolicySpec](#backendtrafficpolicyspec) +- [ClientTrafficPolicySpec](#clienttrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `probes` | _integer_ | false | The total number of unacknowledged probes to send before deciding
the connection is dead.
Defaults to 9. | +| `idleTime` | _[Duration](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Duration)_ | false | The duration a connection needs to be idle before keep-alive
probes start being sent.
The duration format is
Defaults to `7200s`. | +| `interval` | _[Duration](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Duration)_ | false | The duration between keep-alive probes.
Defaults to `75s`. | + + +#### TCPTimeout + + + + + +_Appears in:_ +- [Timeout](#timeout) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `connectTimeout` | _[Duration](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Duration)_ | false | The timeout for network connection establishment, including TCP and TLS handshakes.
Default: 10 seconds. | + + +#### TLSSettings + + + + + +_Appears in:_ +- [BackendTLSConfig](#backendtlsconfig) +- [ClientTLSSettings](#clienttlssettings) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `minVersion` | _[TLSVersion](#tlsversion)_ | false | Min specifies the minimal TLS protocol version to allow.
The default is TLS 1.2 if this is not specified. | +| `maxVersion` | _[TLSVersion](#tlsversion)_ | false | Max specifies the maximal TLS protocol version to allow
The default is TLS 1.3 if this is not specified. | +| `ciphers` | _string array_ | false | Ciphers specifies the set of cipher suites supported when
negotiating TLS 1.0 - 1.2. This setting has no effect for TLS 1.3.
In non-FIPS Envoy Proxy builds the default cipher 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
In builds using BoringSSL FIPS the default cipher list is:
- ECDHE-ECDSA-AES128-GCM-SHA256
- ECDHE-RSA-AES128-GCM-SHA256
- ECDHE-ECDSA-AES256-GCM-SHA384
- ECDHE-RSA-AES256-GCM-SHA384 | +| `ecdhCurves` | _string array_ | false | ECDHCurves specifies the set of supported ECDH curves.
In non-FIPS Envoy Proxy builds the default curves are:
- X25519
- P-256
In builds using BoringSSL FIPS the default curve is:
- P-256 | +| `signatureAlgorithms` | _string array_ | false | SignatureAlgorithms specifies which signature algorithms the listener should
support. | +| `alpnProtocols` | _[ALPNProtocol](#alpnprotocol) array_ | false | ALPNProtocols supplies the list of ALPN protocols that should be
exposed by the listener. By default h2 and http/1.1 are enabled.
Supported values are:
- http/1.0
- http/1.1
- h2 | + + +#### TLSVersion + +_Underlying type:_ _string_ + +TLSVersion specifies the TLS version + +_Appears in:_ +- [BackendTLSConfig](#backendtlsconfig) +- [ClientTLSSettings](#clienttlssettings) +- [TLSSettings](#tlssettings) + +| Value | Description | +| ----- | ----------- | +| `Auto` | TLSAuto allows Envoy to choose the optimal TLS Version
| +| `1.0` | TLS1.0 specifies TLS version 1.0
| +| `1.1` | TLS1.1 specifies TLS version 1.1
| +| `1.2` | TLSv1.2 specifies TLS version 1.2
| +| `1.3` | TLSv1.3 specifies TLS version 1.3
| + + +#### TargetSelector + + + + + +_Appears in:_ +- [BackendTrafficPolicySpec](#backendtrafficpolicyspec) +- [ClientTrafficPolicySpec](#clienttrafficpolicyspec) +- [EnvoyExtensionPolicySpec](#envoyextensionpolicyspec) +- [PolicyTargetReferences](#policytargetreferences) +- [SecurityPolicySpec](#securitypolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `group` | _[Group](#group)_ | true | Group is the group that this selector targets. Defaults to gateway.networking.k8s.io | +| `kind` | _[Kind](#kind)_ | true | Kind is the resource kind that this selector targets. | +| `matchLabels` | _object (keys:string, values:string)_ | true | MatchLabels are the set of label selectors for identifying the targeted resource | + + +#### Timeout + + + +Timeout defines configuration for timeouts related to connections. + +_Appears in:_ +- [BackendTrafficPolicySpec](#backendtrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `tcp` | _[TCPTimeout](#tcptimeout)_ | false | Timeout settings for TCP. | +| `http` | _[HTTPTimeout](#httptimeout)_ | false | Timeout settings for HTTP. | + + +#### TracingProvider + + + +TracingProvider defines the tracing provider configuration. + +_Appears in:_ +- [ProxyTracing](#proxytracing) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[TracingProviderType](#tracingprovidertype)_ | true | Type defines the tracing provider type. | +| `host` | _string_ | false | Host define the provider service hostname.
Deprecated: Use BackendRefs instead. | +| `port` | _integer_ | false | Port defines the port the provider service is exposed on.
Deprecated: Use BackendRefs instead. | +| `backendRefs` | _[BackendRef](#backendref) array_ | false | BackendRefs references a Kubernetes object that represents the
backend server to which the trace will be sent.
Only Service kind is supported for now. | +| `zipkin` | _[ZipkinTracingProvider](#zipkintracingprovider)_ | false | Zipkin defines the Zipkin tracing provider configuration | + + +#### TracingProviderType + +_Underlying type:_ _string_ + + + +_Appears in:_ +- [TracingProvider](#tracingprovider) + +| Value | Description | +| ----- | ----------- | +| `OpenTelemetry` | | +| `OpenTelemetry` | | +| `Zipkin` | | + + +#### TriggerEnum + +_Underlying type:_ _string_ + +TriggerEnum specifies the conditions that trigger retries. + +_Appears in:_ +- [RetryOn](#retryon) + +| Value | Description | +| ----- | ----------- | +| `5xx` | The upstream server responds with any 5xx response code, or does not respond at all (disconnect/reset/read timeout).
Includes connect-failure and refused-stream.
| +| `gateway-error` | The response is a gateway error (502,503 or 504).
| +| `reset` | The upstream server does not respond at all (disconnect/reset/read timeout.)
| +| `connect-failure` | Connection failure to the upstream server (connect timeout, etc.). (Included in *5xx*)
| +| `retriable-4xx` | The upstream server responds with a retriable 4xx response code.
Currently, the only response code in this category is 409.
| +| `refused-stream` | The upstream server resets the stream with a REFUSED_STREAM error code.
| +| `retriable-status-codes` | The upstream server responds with any response code matching one defined in the RetriableStatusCodes.
| +| `cancelled` | The gRPC status code in the response headers is “cancelled”.
| +| `deadline-exceeded` | The gRPC status code in the response headers is “deadline-exceeded”.
| +| `internal` | The gRPC status code in the response headers is “internal”.
| +| `resource-exhausted` | The gRPC status code in the response headers is “resource-exhausted”.
| +| `unavailable` | The gRPC status code in the response headers is “unavailable”.
| + + +#### UnixSocket + + + +UnixSocket describes TCP/UDP unix domain socket address, corresponding to Envoy's Pipe +https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/address.proto#config-core-v3-pipe + +_Appears in:_ +- [BackendEndpoint](#backendendpoint) +- [ExtensionService](#extensionservice) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `path` | _string_ | true | Path defines the unix domain socket path of the backend endpoint. | + + +#### Wasm + + + +Wasm defines a Wasm extension. + + +Note: at the moment, Envoy Gateway does not support configuring Wasm runtime. +v8 is used as the VM runtime for the Wasm extensions. + +_Appears in:_ +- [EnvoyExtensionPolicySpec](#envoyextensionpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `name` | _string_ | false | Name is a unique name for this Wasm extension. It is used to identify the
Wasm extension if multiple extensions are handled by the same vm_id and root_id.
It's also used for logging/debugging.
If not specified, EG will generate a unique name for the Wasm extension. | +| `rootID` | _string_ | true | RootID is a unique ID for a set of extensions in a VM which will share a
RootContext and Contexts if applicable (e.g., an Wasm HttpFilter and an Wasm AccessLog).
If left blank, all extensions with a blank root_id with the same vm_id will share Context(s).

Note: RootID must match the root_id parameter used to register the Context in the Wasm code. | +| `code` | _[WasmCodeSource](#wasmcodesource)_ | true | Code is the Wasm code for the extension. | +| `config` | _[JSON](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#json-v1-apiextensions-k8s-io)_ | false | Config is the configuration for the Wasm extension.
This configuration will be passed as a JSON string to the Wasm extension. | +| `failOpen` | _boolean_ | false | FailOpen is a switch used to control the behavior when a fatal error occurs
during the initialization or the execution of the Wasm extension.
If FailOpen is set to true, the system bypasses the Wasm extension and
allows the traffic to pass through. Otherwise, if it is set to false or
not set (defaulting to false), the system blocks the traffic and returns
an HTTP 5xx error. | + + +#### WasmCodeSource + + + +WasmCodeSource defines the source of the Wasm code. + +_Appears in:_ +- [Wasm](#wasm) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[WasmCodeSourceType](#wasmcodesourcetype)_ | true | Type is the type of the source of the Wasm code.
Valid WasmCodeSourceType values are "HTTP" or "Image". | +| `http` | _[HTTPWasmCodeSource](#httpwasmcodesource)_ | false | HTTP is the HTTP URL containing the Wasm code.

Note that the HTTP server must be accessible from the Envoy proxy. | +| `image` | _[ImageWasmCodeSource](#imagewasmcodesource)_ | false | Image is the OCI image containing the Wasm code.

Note that the image must be accessible from the Envoy Gateway. | +| `pullPolicy` | _[ImagePullPolicy](#imagepullpolicy)_ | false | PullPolicy is the policy to use when pulling the Wasm module by either the HTTP or Image source.
This field is only applicable when the SHA256 field is not set.

If not specified, the default policy is IfNotPresent except for OCI images whose tag is latest.

Note: EG does not update the Wasm module every time an Envoy proxy requests
the Wasm module even if the pull policy is set to Always.
It only updates the Wasm module when the EnvoyExtension resource version changes. | + + +#### WasmCodeSourceType + +_Underlying type:_ _string_ + +WasmCodeSourceType specifies the types of sources for the Wasm code. + +_Appears in:_ +- [WasmCodeSource](#wasmcodesource) + +| Value | Description | +| ----- | ----------- | +| `HTTP` | HTTPWasmCodeSourceType allows the user to specify the Wasm code in an HTTP URL.
| +| `Image` | ImageWasmCodeSourceType allows the user to specify the Wasm code in an OCI image.
| + + +#### WithUnderscoresAction + +_Underlying type:_ _string_ + +WithUnderscoresAction configures the action to take when an HTTP header with underscores +is encountered. + +_Appears in:_ +- [HeaderSettings](#headersettings) + +| Value | Description | +| ----- | ----------- | +| `Allow` | WithUnderscoresActionAllow allows headers with underscores to be passed through.
| +| `RejectRequest` | WithUnderscoresActionRejectRequest rejects the client request. HTTP/1 requests are rejected with
the 400 status. HTTP/2 requests end with the stream reset.
| +| `DropHeader` | WithUnderscoresActionDropHeader drops the client header with name containing underscores. The header
is dropped before the filter chain is invoked and as such filters will not see
dropped headers.
| + + +#### XDSTranslatorHook + +_Underlying type:_ _string_ + +XDSTranslatorHook defines the types of hooks that an Envoy Gateway extension may support +for the xds-translator + +_Appears in:_ +- [XDSTranslatorHooks](#xdstranslatorhooks) + +| Value | Description | +| ----- | ----------- | +| `VirtualHost` | | +| `Route` | | +| `HTTPListener` | | +| `Translation` | | + + +#### XDSTranslatorHooks + + + +XDSTranslatorHooks contains all the pre and post hooks for the xds-translator runner. + +_Appears in:_ +- [ExtensionHooks](#extensionhooks) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `pre` | _[XDSTranslatorHook](#xdstranslatorhook) array_ | true | | +| `post` | _[XDSTranslatorHook](#xdstranslatorhook) array_ | true | | + + +#### XFCCCertData + +_Underlying type:_ _string_ + +XFCCCertData specifies the fields in the client certificate to be forwarded in the XFCC header. + +_Appears in:_ +- [XForwardedClientCert](#xforwardedclientcert) + +| Value | Description | +| ----- | ----------- | +| `Subject` | XFCCCertDataSubject is the Subject field of the current client certificate.
| +| `Cert` | XFCCCertDataCert is the entire client certificate in URL encoded PEM format.
| +| `Chain` | XFCCCertDataChain is the entire client certificate chain (including the leaf certificate) in URL encoded PEM format.
| +| `DNS` | XFCCCertDataDNS is the DNS type Subject Alternative Name field of the current client certificate.
| +| `URI` | XFCCCertDataURI is the URI type Subject Alternative Name field of the current client certificate.
| + + +#### XFCCForwardMode + +_Underlying type:_ _string_ + +XFCCForwardMode defines how XFCC header is handled by Envoy Proxy. + +_Appears in:_ +- [XForwardedClientCert](#xforwardedclientcert) + +| Value | Description | +| ----- | ----------- | +| `Sanitize` | XFCCForwardModeSanitize removes the XFCC header from the request. This is the default mode.
| +| `ForwardOnly` | XFCCForwardModeForwardOnly forwards the XFCC header in the request if the client connection is mTLS.
| +| `AppendForward` | XFCCForwardModeAppendForward appends the client certificate information to the request’s XFCC header and forward it if the client connection is mTLS.
| +| `SanitizeSet` | XFCCForwardModeSanitizeSet resets the XFCC header with the client certificate information and forward it if the client connection is mTLS.
The existing certificate information in the XFCC header is removed.
| +| `AlwaysForwardOnly` | XFCCForwardModeAlwaysForwardOnly always forwards the XFCC header in the request, regardless of whether the client connection is mTLS.
| + + +#### XForwardedClientCert + + + +XForwardedClientCert configures how Envoy Proxy handle the x-forwarded-client-cert (XFCC) HTTP header. + +_Appears in:_ +- [HeaderSettings](#headersettings) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `mode` | _[XFCCForwardMode](#xfccforwardmode)_ | false | Mode defines how XFCC header is handled by Envoy Proxy.
If not set, the default mode is `Sanitize`. | +| `certDetailsToAdd` | _[XFCCCertData](#xfcccertdata) array_ | false | CertDetailsToAdd specifies the fields in the client certificate to be forwarded in the XFCC header.

Hash(the SHA 256 digest of the current client certificate) and By(the Subject Alternative Name)
are always included if the client certificate is forwarded.

This field is only applicable when the mode is set to `AppendForward` or
`SanitizeSet` and the client connection is mTLS. | + + +#### XForwardedForSettings + + + +XForwardedForSettings provides configuration for using X-Forwarded-For headers for determining the client IP address. + +_Appears in:_ +- [ClientIPDetectionSettings](#clientipdetectionsettings) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `numTrustedHops` | _integer_ | false | NumTrustedHops controls the number of additional ingress proxy hops from the right side of XFF HTTP
headers to trust when determining the origin client's IP address.
Refer to https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers#x-forwarded-for
for more details. | + + +#### ZipkinTracingProvider + + + +ZipkinTracingProvider defines the Zipkin tracing provider configuration. + +_Appears in:_ +- [TracingProvider](#tracingprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `enable128BitTraceId` | _boolean_ | false | Enable128BitTraceID determines whether a 128bit trace id will be used
when creating a new trace instance. If set to false, a 64bit trace
id will be used. | +| `disableSharedSpanContext` | _boolean_ | false | DisableSharedSpanContext determines whether the default Envoy behaviour of
client and server spans sharing the same span context should be disabled. | + + diff --git a/site/content/en/docs/boilerplates/index.md b/site/content/en/docs/boilerplates/index.md new file mode 100644 index 00000000000..dda80adbcbf --- /dev/null +++ b/site/content/en/docs/boilerplates/index.md @@ -0,0 +1,5 @@ +--- +headless: true +--- + +This file tells Hugo that the files in this directory tree shouldn't be rendered as normal pages on the site. diff --git a/site/content/en/docs/boilerplates/o11y_prerequisites.md b/site/content/en/docs/boilerplates/o11y_prerequisites.md new file mode 100644 index 00000000000..58586502f72 --- /dev/null +++ b/site/content/en/docs/boilerplates/o11y_prerequisites.md @@ -0,0 +1,14 @@ +--- +--- + +Follow the steps from the [Quickstart](../tasks/quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +Envoy Gateway provides an add-ons Helm Chart, which includes all the needing components for observability. +By default, the [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/) is disabled. + +Install the add-ons Helm Chart: + +```shell +helm install eg-addons oci://docker.io/envoyproxy/gateway-addons-helm --version v0.0.0-latest --set opentelemetry-collector.enabled=true -n monitoring --create-namespace +``` diff --git a/site/content/en/docs/concepts/_index.md b/site/content/en/docs/concepts/_index.md new file mode 100644 index 00000000000..4d568bd4491 --- /dev/null +++ b/site/content/en/docs/concepts/_index.md @@ -0,0 +1,5 @@ +--- +title: "Concepts" +weight: 1 +description: Learn about key concepts when working with Envoy Gateway +--- diff --git a/site/content/en/docs/concepts/concepts_overview.md b/site/content/en/docs/concepts/concepts_overview.md new file mode 100644 index 00000000000..31838b520f2 --- /dev/null +++ b/site/content/en/docs/concepts/concepts_overview.md @@ -0,0 +1,54 @@ ++++ +title = "Envoy Gateway Resources" ++++ + +There are several resources that play a part in enabling you to meet your Kubernetes ingress traffic handling needs. This page provides a brief overview of the resources you’ll be working with. + +## Overview + +![](/img/envoy-gateway-resources-overview.png) + +There are several resources that play a part in enabling you to meet your Kubernetes ingress traffic handling needs. This page provides a brief overview of the resources you’ll be working with. + +# Overview + +## Kubernetes Gateway API Resources +- **GatewayClass:** Defines a class of Gateways with common configuration. +- **Gateway:** Specifies how traffic can enter the cluster. +- **Routes:** **HTTPRoute, GRPCRoute, TLSRoute, TCPRoute, UDPRoute:** Define routing rules for different types of traffic. +## Envoy Gateway (EG) API Resources +- **EnvoyProxy:** Represents the deployment and configuration of the Envoy proxy within a Kubernetes cluster, managing its lifecycle and settings. +- **EnvoyPatchPolicy, ClientTrafficPolicy, SecurityPolicy, BackendTrafficPolicy, EnvoyExtensionPolicy, BackendTLSPolicy:** Additional policies and configurations specific to Envoy Gateway. +- **Backend:** A resource that makes routing to cluster-external backends easier and makes access to external processes via Unix Domain Sockets possible. + +| Resource | API | Required | Purpose | References | Description | +| ----------------------------------------------------------------------- | ----------- | -------- | ------------------ | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [GatewayClass][1] | Gateway API | Yes | Gateway Config | Core | Defines a class of Gateways with common configuration. | +| [Gateway][2] | Gateway API | Yes | Gateway Config | GatewayClass | Specifies how traffic can enter the cluster. | +| [HTTPRoute][3] [GRPCRoute][4] [TLSRoute][5] [TCPRoute][6] [UDPRoute][7] | Gateway API | Yes | Routing | Gateway | Define routing rules for different types of traffic. **Note:**_For simplicity these resources are referenced collectively as Route in the References column_ | +| [Backend][8] | EG API | No | Routing | N/A | Used for routing to cluster-external backends using FQDN or IP. Can also be used when you want to extend Envoy with external processes accessed via Unix Domain Sockets. | +| [ClientTrafficPolicy][9] | EG API | No | Traffic Handling | Gateway | Specifies policies for handling client traffic, including rate limiting, retries, and other client-specific configurations. | +| [BackendTrafficPolicy][10] | EG API | No | Traffic Handling | Gateway Route | Specifies policies for traffic directed towards backend services, including load balancing, health checks, and failover strategies. **Note:**_Most specific configuration wins_ | +| [SecurityPolicy][11] | EG API | No | Security | Gateway Route | Defines security-related policies such as authentication, authorization, and encryption settings for traffic handled by Envoy Gateway. **Note:**_Most specific configuration wins_ | +| [BackendTLSPolicy][12] | Gateway API | No | Security | Service | Defines TLS settings for backend connections, including certificate management, TLS version settings, and other security configurations. This policy is applied to Kubernetes Services. | +| [EnvoyProxy][13] | EG API | No | Customize & Extend | GatewayClass Gateway | The EnvoyProxy resource represents the deployment and configuration of the Envoy proxy itself within a Kubernetes cluster, managing its lifecycle and settings. **Note:**_Most specific configuration wins_ | +| [EnvoyPatchPolicy][14] | EG API | No | Customize & Extend | GatewayClass Gateway | This policy defines custom patches to be applied to Envoy Gateway resources, allowing users to tailor the configuration to their specific needs. **Note:**_Most specific configuration wins_ | +| [EnvoyExtensionPolicy][15] | EG API | No | Customize & Extend | Gateway Route, Backend | Allows for the configuration of Envoy proxy extensions, enabling custom behavior and functionality. **Note:**_Most specific configuration wins_ | + + + +[1]: https://gateway-api.sigs.k8s.io/api-types/gatewayclass/ +[2]: https://gateway-api.sigs.k8s.io/api-types/gateway/ +[3]: https://gateway-api.sigs.k8s.io/api-types/httproute/ +[4]: https://gateway-api.sigs.k8s.io/api-types/grpcroute/ +[5]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.TLSRoute +[6]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.TCPRoute +[7]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.UDPRoute +[8]: ../tasks/traffic/backend +[9]: ../api/extension_types#clienttrafficpolicy +[10]: ../api/extension_types#backendtrafficpolicy +[11]: ../api/extension_types#securitypolicy +[12]: https://gateway-api.sigs.k8s.io/api-types/backendtlspolicy/ +[13]: ../api/extension_types#envoyproxy +[14]: ../api/extension_types#envoypatchpolicy +[15]: ../api/extension_types#envoyextensionpolicy \ No newline at end of file diff --git a/site/content/en/v0.5.0/install/_index.md b/site/content/en/docs/install/_index.md similarity index 100% rename from site/content/en/v0.5.0/install/_index.md rename to site/content/en/docs/install/_index.md diff --git a/site/content/en/docs/install/custom-cert.md b/site/content/en/docs/install/custom-cert.md new file mode 100644 index 00000000000..dd059c03520 --- /dev/null +++ b/site/content/en/docs/install/custom-cert.md @@ -0,0 +1,146 @@ +--- +title: Control Plane Authentication using custom certs +weight: -70 +--- + +Envoy Gateway establishes a secure TLS connection for control plane communication between Envoy Gateway pods and the Envoy Proxy fleet. The TLS Certificates used here are self signed and generated using a job that runs before envoy gateway is created, and these certs and mounted on to the envoy gateway and envoy proxy pods. + +This task will walk you through configuring custom certs for control plane auth. + +## Before you begin + +We use Cert-Manager to manage the certificates. You can install it by following the [official guide](https://cert-manager.io/docs/installation/kubernetes/). + +## Configure custom certs for control plane + +1. First you need to set up the CA issuer, in this task, we use the `selfsigned-issuer` as an example. + + *You should not use the self-signed issuer in production, you should use a real CA issuer.* + + ```shell + cat < + +## Maintainers + +| Name | Email | Url | +| ---- | ------ | --- | +| envoy-gateway-steering-committee | | | +| envoy-gateway-maintainers | | | + +## Source Code + +* + +## Requirements + +| Repository | Name | Version | +|------------|------|---------| +| https://fluent.github.io/helm-charts | fluent-bit | 0.30.4 | +| https://grafana.github.io/helm-charts | grafana | 8.0.0 | +| https://grafana.github.io/helm-charts | loki | 4.8.0 | +| https://grafana.github.io/helm-charts | tempo | 1.3.1 | +| https://open-telemetry.github.io/opentelemetry-helm-charts | opentelemetry-collector | 0.73.1 | +| https://prometheus-community.github.io/helm-charts | prometheus | 25.21.0 | + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| fluent-bit.config.filters | string | `"[FILTER]\n Name kubernetes\n Match kube.*\n Merge_Log On\n Keep_Log Off\n K8S-Logging.Parser On\n K8S-Logging.Exclude On\n\n[FILTER]\n Name grep\n Match kube.*\n Regex $kubernetes['container_name'] ^envoy$\n\n[FILTER]\n Name parser\n Match kube.*\n Key_Name log\n Parser envoy\n Reserve_Data True\n"` | | +| fluent-bit.config.inputs | string | `"[INPUT]\n Name tail\n Path /var/log/containers/*.log\n multiline.parser docker, cri\n Tag kube.*\n Mem_Buf_Limit 5MB\n Skip_Long_Lines On\n"` | | +| fluent-bit.config.outputs | string | `"[OUTPUT]\n Name loki\n Match kube.*\n Host loki.monitoring.svc.cluster.local\n Port 3100\n Labels job=fluentbit, app=$kubernetes['labels']['app'], k8s_namespace_name=$kubernetes['namespace_name'], k8s_pod_name=$kubernetes['pod_name'], k8s_container_name=$kubernetes['container_name']\n"` | | +| fluent-bit.config.service | string | `"[SERVICE]\n Daemon Off\n Flush {{ .Values.flush }}\n Log_Level {{ .Values.logLevel }}\n Parsers_File parsers.conf\n Parsers_File custom_parsers.conf\n HTTP_Server On\n HTTP_Listen 0.0.0.0\n HTTP_Port {{ .Values.metricsPort }}\n Health_Check On\n"` | | +| fluent-bit.enabled | bool | `true` | | +| fluent-bit.fullnameOverride | string | `"fluent-bit"` | | +| fluent-bit.image.repository | string | `"fluent/fluent-bit"` | | +| fluent-bit.podAnnotations."fluentbit.io/exclude" | string | `"true"` | | +| fluent-bit.podAnnotations."prometheus.io/path" | string | `"/api/v1/metrics/prometheus"` | | +| fluent-bit.podAnnotations."prometheus.io/port" | string | `"2020"` | | +| fluent-bit.podAnnotations."prometheus.io/scrape" | string | `"true"` | | +| fluent-bit.testFramework.enabled | bool | `false` | | +| grafana.adminPassword | string | `"admin"` | | +| grafana.dashboardProviders."dashboardproviders.yaml".apiVersion | int | `1` | | +| grafana.dashboardProviders."dashboardproviders.yaml".providers[0].disableDeletion | bool | `false` | | +| grafana.dashboardProviders."dashboardproviders.yaml".providers[0].editable | bool | `true` | | +| grafana.dashboardProviders."dashboardproviders.yaml".providers[0].folder | string | `"envoy-gateway"` | | +| grafana.dashboardProviders."dashboardproviders.yaml".providers[0].name | string | `"envoy-gateway"` | | +| grafana.dashboardProviders."dashboardproviders.yaml".providers[0].options.path | string | `"/var/lib/grafana/dashboards/envoy-gateway"` | | +| grafana.dashboardProviders."dashboardproviders.yaml".providers[0].orgId | int | `1` | | +| grafana.dashboardProviders."dashboardproviders.yaml".providers[0].type | string | `"file"` | | +| grafana.dashboardsConfigMaps.envoy-gateway | string | `"grafana-dashboards"` | | +| grafana.datasources."datasources.yaml".apiVersion | int | `1` | | +| grafana.datasources."datasources.yaml".datasources[0].name | string | `"Prometheus"` | | +| grafana.datasources."datasources.yaml".datasources[0].type | string | `"prometheus"` | | +| grafana.datasources."datasources.yaml".datasources[0].url | string | `"http://prometheus"` | | +| grafana.enabled | bool | `true` | | +| grafana.fullnameOverride | string | `"grafana"` | | +| grafana.service.type | string | `"LoadBalancer"` | | +| loki.backend.replicas | int | `0` | | +| loki.deploymentMode | string | `"SingleBinary"` | | +| loki.enabled | bool | `true` | | +| loki.fullnameOverride | string | `"loki"` | | +| loki.gateway.enabled | bool | `false` | | +| loki.loki.auth_enabled | bool | `false` | | +| loki.loki.commonConfig.replication_factor | int | `1` | | +| loki.loki.compactorAddress | string | `"loki"` | | +| loki.loki.memberlist | string | `"loki-memberlist"` | | +| loki.loki.rulerConfig.storage.type | string | `"local"` | | +| loki.loki.storage.type | string | `"filesystem"` | | +| loki.monitoring.lokiCanary.enabled | bool | `false` | | +| loki.monitoring.selfMonitoring.enabled | bool | `false` | | +| loki.monitoring.selfMonitoring.grafanaAgent.installOperator | bool | `false` | | +| loki.read.replicas | int | `0` | | +| loki.singleBinary.replicas | int | `1` | | +| loki.test.enabled | bool | `false` | | +| loki.write.replicas | int | `0` | | +| opentelemetry-collector.config.exporters.logging.verbosity | string | `"detailed"` | | +| opentelemetry-collector.config.exporters.loki.endpoint | string | `"http://loki.monitoring.svc:3100/loki/api/v1/push"` | | +| opentelemetry-collector.config.exporters.otlp.endpoint | string | `"tempo.monitoring.svc:4317"` | | +| opentelemetry-collector.config.exporters.otlp.tls.insecure | bool | `true` | | +| opentelemetry-collector.config.exporters.prometheus.endpoint | string | `"0.0.0.0:19001"` | | +| opentelemetry-collector.config.extensions.health_check | object | `{}` | | +| opentelemetry-collector.config.processors.attributes.actions[0].action | string | `"insert"` | | +| opentelemetry-collector.config.processors.attributes.actions[0].key | string | `"loki.attribute.labels"` | | +| opentelemetry-collector.config.processors.attributes.actions[0].value | string | `"k8s.pod.name, k8s.namespace.name"` | | +| opentelemetry-collector.config.receivers.otlp.protocols.grpc.endpoint | string | `"${env:MY_POD_IP}:4317"` | | +| opentelemetry-collector.config.receivers.otlp.protocols.http.endpoint | string | `"${env:MY_POD_IP}:4318"` | | +| opentelemetry-collector.config.receivers.zipkin.endpoint | string | `"${env:MY_POD_IP}:9411"` | | +| opentelemetry-collector.config.service.extensions[0] | string | `"health_check"` | | +| opentelemetry-collector.config.service.pipelines.logs.exporters[0] | string | `"loki"` | | +| opentelemetry-collector.config.service.pipelines.logs.processors[0] | string | `"attributes"` | | +| opentelemetry-collector.config.service.pipelines.logs.receivers[0] | string | `"otlp"` | | +| opentelemetry-collector.config.service.pipelines.metrics.exporters[0] | string | `"prometheus"` | | +| opentelemetry-collector.config.service.pipelines.metrics.receivers[0] | string | `"otlp"` | | +| opentelemetry-collector.config.service.pipelines.traces.exporters[0] | string | `"otlp"` | | +| opentelemetry-collector.config.service.pipelines.traces.receivers[0] | string | `"otlp"` | | +| opentelemetry-collector.config.service.pipelines.traces.receivers[1] | string | `"zipkin"` | | +| opentelemetry-collector.enabled | bool | `false` | | +| opentelemetry-collector.fullnameOverride | string | `"otel-collector"` | | +| opentelemetry-collector.mode | string | `"deployment"` | | +| prometheus.alertmanager.enabled | bool | `false` | | +| prometheus.enabled | bool | `true` | | +| prometheus.kube-state-metrics.enabled | bool | `false` | | +| prometheus.prometheus-node-exporter.enabled | bool | `false` | | +| prometheus.prometheus-pushgateway.enabled | bool | `false` | | +| prometheus.server.fullnameOverride | string | `"prometheus"` | | +| prometheus.server.global.scrape_interval | string | `"15s"` | | +| prometheus.server.image.repository | string | `"prom/prometheus"` | | +| prometheus.server.persistentVolume.enabled | bool | `false` | | +| prometheus.server.readinessProbeInitialDelay | int | `0` | | +| prometheus.server.securityContext | object | `{}` | | +| prometheus.server.service.type | string | `"LoadBalancer"` | | +| tempo.enabled | bool | `true` | | +| tempo.fullnameOverride | string | `"tempo"` | | +| tempo.service.type | string | `"LoadBalancer"` | | + diff --git a/site/content/en/docs/install/gateway-helm-api.md b/site/content/en/docs/install/gateway-helm-api.md new file mode 100644 index 00000000000..9f2046a537f --- /dev/null +++ b/site/content/en/docs/install/gateway-helm-api.md @@ -0,0 +1,66 @@ ++++ +title = "Gateway Helm Chart" ++++ + +![Version: v0.0.0-latest](https://img.shields.io/badge/Version-v0.0.0--latest-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: latest](https://img.shields.io/badge/AppVersion-latest-informational?style=flat-square) + +The Helm chart for Envoy Gateway + +**Homepage:** + +## Maintainers + +| Name | Email | Url | +| ---- | ------ | --- | +| envoy-gateway-steering-committee | | | +| envoy-gateway-maintainers | | | + +## Source Code + +* + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| certgen | object | `{"job":{"annotations":{},"resources":{},"ttlSecondsAfterFinished":30},"rbac":{"annotations":{},"labels":{}}}` | Certgen is used to generate the certificates required by EnvoyGateway. If you want to construct a custom certificate, you can generate a custom certificate through Cert-Manager before installing EnvoyGateway. Certgen will not overwrite the custom certificate. Please do not manually modify `values.yaml` to disable certgen, it may cause EnvoyGateway OIDC,OAuth2,etc. to not work as expected. | +| config.envoyGateway.gateway.controllerName | string | `"gateway.envoyproxy.io/gatewayclass-controller"` | | +| config.envoyGateway.logging.level.default | string | `"info"` | | +| config.envoyGateway.provider.type | string | `"Kubernetes"` | | +| createNamespace | bool | `false` | | +| deployment.envoyGateway.image.repository | string | `""` | | +| deployment.envoyGateway.image.tag | string | `""` | | +| deployment.envoyGateway.imagePullPolicy | string | `""` | | +| deployment.envoyGateway.imagePullSecrets | list | `[]` | | +| deployment.envoyGateway.resources.limits.cpu | string | `"500m"` | | +| deployment.envoyGateway.resources.limits.memory | string | `"1024Mi"` | | +| deployment.envoyGateway.resources.requests.cpu | string | `"100m"` | | +| deployment.envoyGateway.resources.requests.memory | string | `"256Mi"` | | +| deployment.pod.affinity | object | `{}` | | +| deployment.pod.annotations."prometheus.io/port" | string | `"19001"` | | +| deployment.pod.annotations."prometheus.io/scrape" | string | `"true"` | | +| deployment.pod.labels | object | `{}` | | +| deployment.pod.tolerations | list | `[]` | | +| deployment.pod.topologySpreadConstraints | list | `[]` | | +| deployment.ports[0].name | string | `"grpc"` | | +| deployment.ports[0].port | int | `18000` | | +| deployment.ports[0].targetPort | int | `18000` | | +| deployment.ports[1].name | string | `"ratelimit"` | | +| deployment.ports[1].port | int | `18001` | | +| deployment.ports[1].targetPort | int | `18001` | | +| deployment.ports[2].name | string | `"wasm"` | | +| deployment.ports[2].port | int | `18002` | | +| deployment.ports[2].targetPort | int | `18002` | | +| deployment.ports[3].name | string | `"metrics"` | | +| deployment.ports[3].port | int | `19001` | | +| deployment.ports[3].targetPort | int | `19001` | | +| deployment.replicas | int | `1` | | +| global.images.envoyGateway.image | string | `nil` | | +| global.images.envoyGateway.pullPolicy | string | `nil` | | +| global.images.envoyGateway.pullSecrets | list | `[]` | | +| global.images.ratelimit.image | string | `"docker.io/envoyproxy/ratelimit:master"` | | +| global.images.ratelimit.pullPolicy | string | `"IfNotPresent"` | | +| global.images.ratelimit.pullSecrets | list | `[]` | | +| kubernetesClusterDomain | string | `"cluster.local"` | | +| podDisruptionBudget.minAvailable | int | `0` | | + diff --git a/site/content/en/v1.0.2/install/install-egctl.md b/site/content/en/docs/install/install-egctl.md similarity index 100% rename from site/content/en/v1.0.2/install/install-egctl.md rename to site/content/en/docs/install/install-egctl.md diff --git a/site/content/en/docs/install/install-helm.md b/site/content/en/docs/install/install-helm.md new file mode 100644 index 00000000000..b3468f642f9 --- /dev/null +++ b/site/content/en/docs/install/install-helm.md @@ -0,0 +1,144 @@ ++++ +title = "Install with Helm" +weight = -100 ++++ + +[Helm](https://helm.sh) is a package manager for Kubernetes that automates the release and management of software on Kubernetes. + +Envoy Gateway can be installed via a Helm chart with a few simple steps, depending on if you are deploying for the first time, upgrading Envoy Gateway from an existing installation, or migrating from Envoy Gateway. + +## Before you begin + +{{% alert title="Compatibility Matrix" color="warning" %}} +Refer to the [Version Compatibility Matrix](/news/releases/matrix) to learn more. +{{% /alert %}} + +The Envoy Gateway Helm chart is hosted by DockerHub. + +It is published at `oci://docker.io/envoyproxy/gateway-helm`. + +{{% alert title="Note" color="primary" %}} +We use `v0.0.0-latest` as the latest development version. + +You can visit [Envoy Gateway Helm Chart](https://hub.docker.com/r/envoyproxy/gateway-helm/tags) for more releases. +{{% /alert %}} + +## Install with Helm + +Envoy Gateway is typically deployed to Kubernetes from the command line. If you don't have Kubernetes, you should use `kind` to create one. + +{{% alert title="Developer Guide" color="primary" %}} +Refer to the [Developer Guide](../../contributions/develop) to learn more. +{{% /alert %}} + +Install the Gateway API CRDs and Envoy Gateway: + +```shell +helm install eg oci://docker.io/envoyproxy/gateway-helm --version v0.0.0-latest -n envoy-gateway-system --create-namespace +``` + +Wait for Envoy Gateway to become available: + +```shell +kubectl wait --timeout=5m -n envoy-gateway-system deployment/envoy-gateway --for=condition=Available +``` + +Install the GatewayClass, Gateway, HTTPRoute and example app: + +```shell +kubectl apply -f https://github.com/envoyproxy/gateway/releases/download/latest/quickstart.yaml -n default +``` + +**Note**: [`quickstart.yaml`] defines that Envoy Gateway will listen for +traffic on port 80 on its globally-routable IP address, to make it easy to use +browsers to test Envoy Gateway. When Envoy Gateway sees that its Listener is +using a privileged port (<1024), it will map this internally to an +unprivileged port, so that Envoy Gateway doesn't need additional privileges. +It's important to be aware of this mapping, since you may need to take it into +consideration when debugging. + +[`quickstart.yaml`]: https://github.com/envoyproxy/gateway/releases/download/latest/quickstart.yaml + +## Helm chart customizations + +Some of the quick ways of using the helm install command for envoy gateway installation are below. + +{{% alert title="Helm Chart Values" color="primary" %}} +If you want to know all the available fields inside the values.yaml file, please see the [Helm Chart Values](./gateway-helm-api). +{{% /alert %}} + +### Increase the replicas + +```shell +helm install eg oci://docker.io/envoyproxy/gateway-helm --version v0.0.0-latest -n envoy-gateway-system --create-namespace --set deployment.replicas=2 +``` + +### Change the kubernetesClusterDomain name + +If you have installed your cluster with different domain name you can use below command. + +```shell +helm install eg oci://docker.io/envoyproxy/gateway-helm --version v0.0.0-latest -n envoy-gateway-system --create-namespace --set kubernetesClusterDomain= +``` + +**Note**: Above are some of the ways we can directly use for customization of our installation. But if you are looking for more complex changes [values.yaml](https://helm.sh/docs/chart_template_guide/values_files/) comes to rescue. + +### Using values.yaml file for complex installation + +```yaml +deployment: + envoyGateway: + resources: + limits: + cpu: 700m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + ports: + - name: grpc + port: 18005 + targetPort: 18000 + - name: ratelimit + port: 18006 + targetPort: 18001 + +config: + envoyGateway: + logging: + level: + default: debug +``` + +Here we have made three changes to our values.yaml file. Increase the resources limit for cpu to `700m`, changed the port for grpc to `18005` and for ratelimit to `18006` and also updated the logging level to `debug`. + +You can use the below command to install the envoy gateway using values.yaml file. + +```shell +helm install eg oci://docker.io/envoyproxy/gateway-helm --version v0.0.0-latest -n envoy-gateway-system --create-namespace -f values.yaml +``` + +## Open Ports + +These are the ports used by Envoy Gateway and the managed Envoy Proxy. + +### Envoy Gateway + +| Envoy Gateway | Address | Port | Configurable | +|:----------------------:|:---------:|:------:| :------: | +| Xds EnvoyProxy Server | 0.0.0.0 | 18000 | No | +| Xds RateLimit Server | 0.0.0.0 | 18001 | No | +| Admin Server | 127.0.0.1 | 19000 | Yes | +| Metrics Server | 0.0.0.0 | 19001 | No | +| Health Check | 127.0.0.1 | 8081 | No | + +### EnvoyProxy + +| Envoy Proxy | Address | Port | +|:---------------------------------:|:-----------:| :-----: | +| Admin Server | 127.0.0.1 | 19000 | +| Heath Check | 0.0.0.0 | 19001 | + +{{% alert title="Next Steps" color="warning" %}} +Envoy Gateway should now be successfully installed and running. To experience more abilities of Envoy Gateway, refer to [Tasks](../tasks). +{{% /alert %}} diff --git a/site/content/en/docs/install/install-yaml.md b/site/content/en/docs/install/install-yaml.md new file mode 100644 index 00000000000..c2a4da45fcd --- /dev/null +++ b/site/content/en/docs/install/install-yaml.md @@ -0,0 +1,39 @@ ++++ +title = "Install with Kubernetes YAML" +weight = -99 ++++ + +This task walks you through installing Envoy Gateway in your Kubernetes cluster. + +The manual install process does not allow for as much control over configuration +as the [Helm install method](./install-helm), so if you need more control over your Envoy Gateway +installation, it is recommended that you use helm. + +## Before you begin + +Envoy Gateway is designed to run in Kubernetes for production. The most essential requirements are: + +* Kubernetes 1.25 or later +* The `kubectl` command-line tool + +{{% alert title="Compatibility Matrix" color="warning" %}} +Refer to the [Version Compatibility Matrix](/news/releases/matrix) to learn more. +{{% /alert %}} + +## Install with YAML + +Envoy Gateway is typically deployed to Kubernetes from the command line. If you don't have Kubernetes, you should use `kind` to create one. + +{{% alert title="Developer Guide" color="primary" %}} +Refer to the [Developer Guide](../../contributions/develop) to learn more. +{{% /alert %}} + +1. In your terminal, run the following command: + + ```shell + kubectl apply --server-side -f https://github.com/envoyproxy/gateway/releases/download/latest/install.yaml + ``` + +2. Next Steps + + Envoy Gateway should now be successfully installed and running, but in order to experience more abilities of Envoy Gateway, you can refer to [Tasks](/latest/tasks). diff --git a/site/content/en/v1.0.2/tasks/_index.md b/site/content/en/docs/tasks/_index.md similarity index 100% rename from site/content/en/v1.0.2/tasks/_index.md rename to site/content/en/docs/tasks/_index.md diff --git a/site/content/en/docs/tasks/extensibility/_index.md b/site/content/en/docs/tasks/extensibility/_index.md new file mode 100644 index 00000000000..664c734aeca --- /dev/null +++ b/site/content/en/docs/tasks/extensibility/_index.md @@ -0,0 +1,5 @@ +--- +title: "Extensibility" +weight: 4 +description: This section includes Extensibility tasks. +--- diff --git a/site/content/en/docs/tasks/extensibility/build-wasm-image.md b/site/content/en/docs/tasks/extensibility/build-wasm-image.md new file mode 100644 index 00000000000..dfe983dd193 --- /dev/null +++ b/site/content/en/docs/tasks/extensibility/build-wasm-image.md @@ -0,0 +1,71 @@ +--- +title: "Build a Wasm image" +--- + +Envoy Gateway supports two types of Wasm extensions within the [EnvoyExtensionPolicy][] API: HTTP Wasm Extensions and Image Wasm Extensions. +Packaging a Wasm extension as an OCI image is beneficial because it simplifies versioning and distribution for users. +Additionally, users can leverage existing image toolchain to build and manage Wasm images. + +This document describes how to build OCI images which are consumable by Envoy Gateway. + +## Wasm Image Formats + +There are two types of images that are supported by Envoy Gateway. One is in the Docker format, and another is the standard +OCI specification compliant format. Please note that both of them are supported by any OCI registries. You can choose +either format depending on your preference, and both types of images are consumable by Envoy Gateway [EnvoyExtensionPolicy][] API. + +## Build Wasm Docker image + +We assume that you have a valid Wasm binary named `plugin.wasm`. Then you can build a Wasm Docker image with the Docker CLI. + +1. First, we prepare the following Dockerfile: + +``` +$ cat Dockerfile +FROM scratch + +COPY plugin.wasm ./ +``` + +**Note: you must have exactly one `COPY` instruction in the Dockerfile in order to end up having only one layer in produced images.** + +2. Then, build your image via `docker build` command + +``` +$ docker build . -t my-registry/mywasm:0.1.0 +``` + +3. Finally, push the image to your registry via `docker push` command + +``` +$ docker push my-registry/mywasm:0.1.0 +``` + +## Build Wasm OCI image + +We assume that you have a valid Wasm binary named `plugin.wasm`, and you have [buildah](https://buildah.io/) installed on your machine. +Then you can build a Wasm OCI image with the `buildah` CLI. + +1. First, we create a working container from `scratch` base image with `buildah from` command. + +``` +$ buildah --name mywasm from scratch +mywasm +``` + +2. Then copy the Wasm binary into that base image by `buildah copy` command to create the layer. + +``` +$ buildah copy mywasm plugin.wasm ./ +af82a227630327c24026d7c6d3057c3d5478b14426b74c547df011ca5f23d271 +``` + +**Note: you must execute `buildah copy` exactly once in order to end up having only one layer in produced images** + +4. Now, you can build an OCI image and push it to your registry via `buildah commit` command + +``` +$ buildah commit mywasm docker://my-remote-registry/mywasm:0.1.0 +``` + +[EnvoyExtensionPolicy]: ../../../api/extension_types#envoyextensionpolicy diff --git a/site/content/en/docs/tasks/extensibility/envoy-patch-policy.md b/site/content/en/docs/tasks/extensibility/envoy-patch-policy.md new file mode 100644 index 00000000000..ff819754d1f --- /dev/null +++ b/site/content/en/docs/tasks/extensibility/envoy-patch-policy.md @@ -0,0 +1,359 @@ +--- +title: "Envoy Patch Policy" +--- + +This task explains the usage of the [EnvoyPatchPolicy][] API. +__Note:__ This API is meant for users extremely familiar with Envoy [xDS][] semantics. +Also before considering this API for production use cases, please be aware that this API +is unstable and the outcome may change across versions. Use at your own risk. + +## Introduction + +The [EnvoyPatchPolicy][] API allows user to modify the output [xDS][] +configuration generated by Envoy Gateway intended for EnvoyProxy, +using [JSON Patch][] semantics. + +## Motivation + +This API was introduced to allow advanced users to be able to leverage Envoy Proxy functionality +not exposed by Envoy Gateway APIs today. + +## Quickstart + +### Prerequisites + +* Follow the steps from the [Quickstart](../../quickstart) task to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +### Enable EnvoyPatchPolicy + +* By default [EnvoyPatchPolicy][] is disabled. Lets enable it in the [EnvoyGateway][] startup configuration + +* The default installation of Envoy Gateway installs a default [EnvoyGateway][] configuration and attaches it +using a `ConfigMap`. In the next step, we will update this resource to enable EnvoyPatchPolicy. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +* After updating the `ConfigMap`, you will need to restart the `envoy-gateway` deployment so the configuration kicks in + +```shell +kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system +``` + +## Testing + +### Customize Response + +* Use EnvoyProxy's [Local Reply Modification][] feature to return a custom response back to the client when +the status code is `404` + +* Apply the configuration + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <// + name: default/eg/http + operation: + op: add + path: "/default_filter_chain/filters/0/typed_config/local_reply_config" + value: + mappers: + - filter: + status_code_filter: + comparison: + op: EQ + value: + default_value: 404 + runtime_key: key_b + status_code: 406 + body: + inline_string: "could not find what you are looking for" +EOF +``` + +{{% /tab %}} +{{% tab header="Apply from file" %}} +Save and apply the following resource to your cluster: + +```yaml +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyPatchPolicy +metadata: + name: custom-response-patch-policy + namespace: default +spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: eg + namespace: default + type: JSONPatch + jsonPatches: + - type: "type.googleapis.com/envoy.config.listener.v3.Listener" + # The listener name is of the form // + name: default/eg/http + operation: + op: add + path: "/default_filter_chain/filters/0/typed_config/local_reply_config" + value: + mappers: + - filter: + status_code_filter: + comparison: + op: EQ + value: + default_value: 404 + runtime_key: key_b + status_code: 406 + body: + inline_string: "could not find what you are looking for" +``` + +{{% /tab %}} +{{< /tabpane >}} + +When mergeGateways is enabled, there will be one Envoy deployment for all Gateways in the cluster. +Then the EnvoyPatchPolicy should target a specific GatewayClass. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <// + name: default/eg/http + operation: + op: add + path: "/default_filter_chain/filters/0/typed_config/local_reply_config" + value: + mappers: + - filter: + status_code_filter: + comparison: + op: EQ + value: + default_value: 404 + runtime_key: key_b + status_code: 406 + body: + inline_string: "could not find what you are looking for" +EOF +``` + +{{% /tab %}} +{{% tab header="Apply from file" %}} +Save and apply the following resource to your cluster: + +```yaml +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyPatchPolicy +metadata: + name: custom-response-patch-policy + namespace: default +spec: + targetRef: + group: gateway.networking.k8s.io + kind: GatewayClass + name: eg + namespace: default + type: JSONPatch + jsonPatches: + - type: "type.googleapis.com/envoy.config.listener.v3.Listener" + # The listener name is of the form // + name: default/eg/http + operation: + op: add + path: "/default_filter_chain/filters/0/typed_config/local_reply_config" + value: + mappers: + - filter: + status_code_filter: + comparison: + op: EQ + value: + default_value: 404 + runtime_key: key_b + status_code: 406 + body: + inline_string: "could not find what you are looking for" +``` + +{{% /tab %}} +{{< /tabpane >}} + +* Edit the HTTPRoute resource from the Quickstart to only match on paths with value `/get` + +```shell +kubectl patch httproute backend --type=json --patch ' + - op: add + path: /spec/rules/0/matches/0/path/value + value: /get + ' +``` + +* Test it out by specifying a path apart from `/get` + +``` +$ curl --header "Host: www.example.com" http://localhost:8888/find +Handling connection for 8888 +could not find what you are looking for +``` + +## Debugging + +### Runtime + +* The `Status` subresource should have information about the status of the resource. Make sure +`Accepted=True` and `Programmed=True` conditions are set to ensure that the policy has been +applied to Envoy Proxy. + +```yaml +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyPatchPolicy +metadata: + annotations: + kubectl.kubernetes.io/last-applied-configuration: | + {"apiVersion":"gateway.envoyproxy.io/v1alpha1","kind":"EnvoyPatchPolicy","metadata":{"annotations":{},"name":"custom-response-patch-policy","namespace":"default"},"spec":{"jsonPatches":[{"name":"default/eg/http","operation":{"op":"add","path":"/default_filter_chain/filters/0/typed_config/local_reply_config","value":{"mappers":[{"body":{"inline_string":"could not find what you are looking for"},"filter":{"status_code_filter":{"comparison":{"op":"EQ","value":{"default_value":404}}}}}]}},"type":"type.googleapis.com/envoy.config.listener.v3.Listener"}],"priority":0,"targetRef":{"group":"gateway.networking.k8s.io","kind":"Gateway","name":"eg","namespace":"default"},"type":"JSONPatch"}} + creationTimestamp: "2023-07-31T21:47:53Z" + generation: 1 + name: custom-response-patch-policy + namespace: default + resourceVersion: "10265" + uid: a35bda6e-a0cc-46d7-a63a-cee765174bc3 +spec: + jsonPatches: + - name: default/eg/http + operation: + op: add + path: /default_filter_chain/filters/0/typed_config/local_reply_config + value: + mappers: + - body: + inline_string: could not find what you are looking for + filter: + status_code_filter: + comparison: + op: EQ + value: + default_value: 404 + type: type.googleapis.com/envoy.config.listener.v3.Listener + priority: 0 + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: eg + namespace: default + type: JSONPatch +status: + conditions: + - lastTransitionTime: "2023-07-31T21:48:19Z" + message: EnvoyPatchPolicy has been accepted. + observedGeneration: 1 + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: "2023-07-31T21:48:19Z" + message: successfully applied patches. + reason: Programmed + status: "True" + type: Programmed +``` + +### Offline + +* You can use [egctl x translate][] to validate the translated xds output. + +## Caveats + +This API will always be an unstable API and the same outcome cannot be garunteed +across versions for these reasons +* The Envoy Proxy API might deprecate and remove API fields +* Envoy Gateway might alter the xDS translation creating a different xDS output +such as changing the `name` field of resources. + +[EnvoyPatchPolicy]: ../../../api/extension_types#envoypatchpolicy +[EnvoyGateway]: ../../../api/extension_types#envoygateway +[JSON Patch]: https://datatracker.ietf.org/doc/html/rfc6902 +[xDS]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/operations/dynamic_configuration +[Local Reply Modification]: https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/local_reply +[egctl x translate]: ../operations/egctl#egctl-experimental-translate diff --git a/site/content/en/docs/tasks/extensibility/ext-proc.md b/site/content/en/docs/tasks/extensibility/ext-proc.md new file mode 100644 index 00000000000..9028447ab09 --- /dev/null +++ b/site/content/en/docs/tasks/extensibility/ext-proc.md @@ -0,0 +1,290 @@ +--- +title: "External Processing" +--- + +This task provides instructions for configuring external processing. + +External processing calls an external gRPC service to process HTTP requests and responses. +The external processing service can inspect and mutate requests and responses. + +Envoy Gateway introduces a new CRD called [EnvoyExtensionPolicy][] that allows the user to configure external processing. +This instantiated resource can be linked to a [Gateway][Gateway] and [HTTPRoute][HTTPRoute] resource. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +Verify the Gateway status: + +```shell +kubectl get gateway/eg -o yaml +``` + +## GRPC External Processing Service + +### Installation + +Install a demo GRPC service that will be used as the external processing service: + +```shell +kubectl apply -f https://raw.githubusercontent.com/envoyproxy/gateway/latest/examples/kubernetes/ext-proc-grpc-service.yaml +``` + +Create a new HTTPRoute resource to route traffic on the path `/myapp` to the backend service. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the HTTPRoute status: + +```shell +kubectl get httproute/myapp -o yaml +``` + +### Configuration + +Create a new EnvoyExtensionPolicy resource to configure the external processing service. This EnvoyExtensionPolicy targets the HTTPRoute +"myApp" created in the previous step. It calls the GRPC external processing service "grpc-ext-proc" on port 9002 for +processing. + +By default, requests and responses are not sent to the external processor. The `processingMode` struct is used to define what should be sent to the external processor. +In this example, we configure the following processing modes: +* The empty `request` field configures envoy to send request headers to the external processor. +* The `response` field includes configuration for body processing. As a result, response headers are sent to the external processor. Additionally, the response body is streamed to the external processor. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the Envoy Extension Policy configuration: + +```shell +kubectl get envoyextensionpolicy/ext-proc-example -o yaml +``` + + +Because the gRPC external processing service is enabled with TLS, a [BackendTLSPolicy][] needs to be created to configure +the communication between the Envoy proxy and the gRPC auth service. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the BackendTLSPolicy configuration: + +```shell +kubectl get backendtlspolicy/grpc-ext-proc-btls -o yaml +``` + +### Testing + +Ensure the `GATEWAY_HOST` environment variable from the [Quickstart](../../quickstart) is set. If not, follow the +Quickstart instructions to set the variable. + +```shell +echo $GATEWAY_HOST +``` + +Send a request to the backend service without `Authentication` header: + +```shell +curl -v -H "Host: www.example.com" "http://${GATEWAY_HOST}/myapp" +``` + +You should see that the external processor added headers: +- `x-request-ext-processed` - this header was added before the request was forwarded to the backend +- `x-response-ext-processed`- this header was added before the response was returned to the client + + +``` +curl -v -H "Host: www.example.com" http://localhost:10080/myapp +[...] +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< date: Fri, 14 Jun 2024 19:30:40 GMT +< content-length: 502 +< x-response-ext-processed: true +< +{ + "path": "/myapp", + "host": "www.example.com", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { +[...] + "X-Request-Ext-Processed": [ + "true" + ], +[...] + } +``` + +## Clean-Up + +Follow the steps from the [Quickstart](../../quickstart) to uninstall Envoy Gateway and the example manifest. + +Delete the demo auth services, HTTPRoute, EnvoyExtensionPolicy and BackendTLSPolicy: + +```shell +kubectl delete -f https://raw.githubusercontent.com/envoyproxy/gateway/latest/examples/kubernetes/ext-proc-grpc-service.yaml +kubectl delete httproute/myapp +kubectl delete envoyextensionpolicy/ext-proc-example +kubectl delete backendtlspolicy/grpc-ext-proc-btls +``` + +## Next Steps + +Checkout the [Developer Guide](../../../contributions/develop) to get involved in the project. + +[EnvoyExtensionPolicy]: ../../../api/extension_types#envoyextensionpolicy +[BackendTLSPolicy]: https://gateway-api.sigs.k8s.io/api-types/backendtlspolicy/ +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute diff --git a/site/content/en/docs/tasks/extensibility/extension-server.md b/site/content/en/docs/tasks/extensibility/extension-server.md new file mode 100644 index 00000000000..7d67c23f6da --- /dev/null +++ b/site/content/en/docs/tasks/extensibility/extension-server.md @@ -0,0 +1,209 @@ +--- +title: "Envoy Gateway Extension Server" +--- + +This task explains how to extend Envoy Gateway using an Extension Server. Envoy Gateway +can be configured to call an external server over gRPC with the xDS configuration _before_ +it is sent to Envoy Proxy. The external server can modify the provided configuration +programmatically using any semantics supported by the [xDS][] API. + +Using an extension server allows vendors to add xDS configuration that Envoy Gateway itself +doesn't support with a very high level of control over the generated xDS configuration. + +**Note:** Modifying the xDS configuration generated by Envoy Gateway may break functionality +configured by native Envoy Gateway means. Like other cases where the xDS configuration +is modified outside of Envoy Gateway's control, this is risky and should be tested thoroughly, +especially when using the same extension server across different Envoy Gateway versions. + +## Introduction + +One of the Envoy Gateway project goals is to "provide a common foundation for vendors to +build value-added products without having to re-engineer fundamental interactions". The +Envoy Gateway Extension Server provides a mechanism where Envoy Gateway tracks all provider +resources and then calls a set of hooks that allow the generated xDS configuration to be +modified before it is sent to Envoy Proxy. See the [design documentation][] for full details. + +This task sets up an example extension server that adds the Envoy Proxy Basic Authentication +HTTP filter to all the listeners generated by Envoy Gateway. The example extension server +includes its own CRD which allows defining username/password pairs that will be accepted by +the Envoy Proxy. + +**Note:** Envoy Gateway supports adding Basic Authentication to routes using a [SecurityPolicy][]. +See [this task](../security/basic-auth) for the preferred way to configure Basic +Authentication. + + +## Quickstart + +### Prerequisites + +* Follow the steps from the [Quickstart](../quickstart) task to install Envoy Gateway and the example manifest. + Before proceeding, you should be able to query the example backend using HTTP. + +### Build and run the example Extension Server + +Build and deploy the example extension server in the `examples/extension-server` folder into the cluster +running Envoy Gateway. + +* Build the extension server image + + **Note:** The provided `Makefile` builds an image with the name `extension-server:latest`. You may need to create +a different tag for it in order to allow Kubernetes to pull it correctly. + + ```shell + make image + ``` + +* Publish the extension server image in your docker repository + + {{< tabpane text=true >}} + {{% tab header="local kind server" %}} + + ```shell + kind load docker-image --name envoy-gateway extension-server:latest + ``` + + {{% /tab %}} + {{% tab header="other Kubernetes server" %}} + + ```shell + docker tag extension-server:latest $YOUR_DOCKER_REPO + docker push $YOUR_DOCKER_REPO + ``` + + {{% /tab %}} + {{< /tabpane >}} + +* Deploy the extension server in your cluster + + If you are using your own docker image repository, make sure to update the `values.yaml` with the correct +image name and tag. + + ```shell + helm install -n envoy-gateway-system extension-server ./examples/extension-server/charts/extension-server + ``` + +### Configure Envoy Gateway + +* Grant Envoy Gateway's `ServiceAccount` permission to access the extension server's CRD + + ```shell + kubectl create clusterrole listener-context-example-viewer \ + --verb=get,list,watch \ + --resource=ListenerContextExample + + kubectl create clusterrolebinding envoy-gateway-listener-context \ + --clusterrole=listener-context-example-viewer \ + --serviceaccount=envoy-gateway-system:envoy-gateway + ``` + +* Configure Envoy Gateway to use the Extension Server + + Add the following fragment to Envoy Gateway's [configuration][] file: + + ```yaml + extensionManager: + # Envoy Gateway will watch these resource kinds and use them as extension policies + # which can be attached to Gateway resources. + policyResources: + - group: example.extensions.io + version: v1alpha1 + kind: ListenerContextExample + hooks: + # The type of hooks that should be invoked + xdsTranslator: + post: + - HTTPListener + service: + # The service that is hosting the extension server + fqdn: + hostname: extension-server.envoy-gateway-system.svc.cluster.local + port: 5005 + ``` + + After updating Envoy Gateway's configuration file, restart Envoy Gateway. + +## Testing + +Get the Gateway's address: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +The extension server adds the Basic Authentication HTTP filter to all listeners configured by +Envoy Gateway. Initially there are no valid user/password combinations available. Accessing the +example backend should fail with a 401 status: + +```console +$ curl -v --header "Host: www.example.com" "http://${GATEWAY_HOST}/example" +... +> GET /example HTTP/1.1 +> Host: www.example.com +> User-Agent: curl/7.81.0 +> Accept: */* +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 401 Unauthorized +< www-authenticate: Basic realm="http://www.example.com/example" +< content-length: 58 +< content-type: text/plain +< date: Mon, 08 Jul 2024 10:53:11 GMT +< +... +User authentication failed. Missing username and password. +... +``` + +Add a new Username/Password combination using the example extension server's CRD: + +```shell +kubectl apply -f - << EOF +apiVersion: example.extensions.io/v1alpha1 +kind: ListenerContextExample +metadata: + name: listeneruser +spec: + targetRefs: + - kind: Gateway + name: eg + group: gateway.networking.k8s.io + username: user + password: p@ssw0rd +EOF +``` + +Authenticating with this user/password combination will now work. + +```console +$ curl -v http://${GATEWAY_HOST}/example -H "Host: www.example.com" --user 'user:p@ssw0rd' +... +> GET /example HTTP/1.1 +> Host: www.example.com +> Authorization: Basic dXNlcm5hbWU6cEBzc3cwcmQ= +> User-Agent: curl/7.81.0 +> Accept: */* +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< date: Mon, 08 Jul 2024 10:56:17 GMT +< content-length: 559 +< +... + "headers": { + "Authorization": [ + "Basic dXNlcm5hbWU6cEBzc3cwcmQ=" + ], + "X-Example-Ext": [ + "user" + ], +... +``` + + +[xDS]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/operations/dynamic_configuration +[design documentation]: /contributions/design/extending-envoy-gateway +[SecurityPolicy]: /latest/api/extension_types/#securitypolicy +[configuration]: /latest/api/extension_types/#extensionmanager diff --git a/site/content/en/docs/tasks/extensibility/wasm.md b/site/content/en/docs/tasks/extensibility/wasm.md new file mode 100644 index 00000000000..d973de77950 --- /dev/null +++ b/site/content/en/docs/tasks/extensibility/wasm.md @@ -0,0 +1,194 @@ +--- +title: "Wasm Extensions" +--- + +This task provides instructions for extending Envoy Gateway with WebAssembly (Wasm) extensions. + +Wasm extensions allow you to extend the functionality of Envoy Gateway by running custom code against HTTP requests and responses, +without modifying the Envoy Gateway binary. These extensions can be written in any language that compiles to Wasm, such as C++, Rust, AssemblyScript, or TinyGo. + +Envoy Gateway introduces a new CRD called [EnvoyExtensionPolicy][] that allows the user to configure Wasm extensions. +This instantiated resource can be linked to a [Gateway][Gateway] and [HTTPRoute][HTTPRoute] resource. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +Verify the Gateway status: + +```shell +kubectl get gateway/eg -o yaml +``` + +## Configuration + +Envoy Gateway supports two types of Wasm extensions: +* HTTP Wasm Extension: The Wasm extension is fetched from a remote URL. +* Image Wasm Extension: The Wasm extension is packaged as an OCI image and fetched from an image registry. + +The following example demonstrates how to configure an [EnvoyExtensionPolicy][] to attach a Wasm extension to an [EnvoyExtensionPolicy][] . +This Wasm extension adds a custom header `x-wasm-custom: FOO` to the response. + +### HTTP Wasm Extension + +This [EnvoyExtensionPolicy][] configuration fetches the Wasm extension from an HTTP URL. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the EnvoyExtensionPolicy status: + +```shell +kubectl get envoyextensionpolicy/http-wasm-source-test -o yaml +``` + +### Image Wasm Extension + +This [EnvoyExtensionPolicy][] configuration fetches the Wasm extension from an OCI image. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the EnvoyExtensionPolicy status: + +```shell +kubectl get envoyextensionpolicy/http-wasm-source-test -o yaml +``` + +### Testing + +Ensure the `GATEWAY_HOST` environment variable from the [Quickstart](../../quickstart) is set. If not, follow the +Quickstart instructions to set the variable. + +```shell +echo $GATEWAY_HOST +``` + +Send a request to the backend service: + +```shell +curl -i -H "Host: www.example.com" "http://${GATEWAY_HOST}" +``` + +You should see that the wasm extension has added this header to the response: + +``` +x-wasm-custom: FOO +``` + +## Clean-Up + +Follow the steps from the [Quickstart](../../quickstart) to uninstall Envoy Gateway and the example manifest. + +Delete the EnvoyExtensionPolicy: + +```shell +kubectl delete envoyextensionpolicy/wasm-test +``` + +## Next Steps + +Checkout the [Developer Guide](../../../contributions/develop) to get involved in the project. + +[EnvoyExtensionPolicy]: ../../../api/extension_types#envoyextensionpolicy +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute diff --git a/site/content/en/docs/tasks/observability/_index.md b/site/content/en/docs/tasks/observability/_index.md new file mode 100644 index 00000000000..9ca4896ee8b --- /dev/null +++ b/site/content/en/docs/tasks/observability/_index.md @@ -0,0 +1,5 @@ +--- +title: "Observability" +weight: 4 +description: This section includes Observability tasks. +--- diff --git a/site/content/en/v1.0.2/tasks/observability/gateway-api-metrics.md b/site/content/en/docs/tasks/observability/gateway-api-metrics.md similarity index 100% rename from site/content/en/v1.0.2/tasks/observability/gateway-api-metrics.md rename to site/content/en/docs/tasks/observability/gateway-api-metrics.md diff --git a/site/content/en/docs/tasks/observability/gateway-exported-metrics.md b/site/content/en/docs/tasks/observability/gateway-exported-metrics.md new file mode 100644 index 00000000000..cf04f1d444b --- /dev/null +++ b/site/content/en/docs/tasks/observability/gateway-exported-metrics.md @@ -0,0 +1,97 @@ +--- +title: "Gateway Exported Metrics" +--- + +The Envoy Gateway provides a collection of self-monitoring metrics in [Prometheus format][prom-format]. + +These metrics allow monitoring of the behavior of Envoy Gateway itself (as distinct from that of the EnvoyProxy it managed). + +{{% alert title="EnvoyProxy Metrics" color="warning" %}} +For EnvoyProxy Metrics, please refer to the [EnvoyProxy Metrics](./proxy-metric) to learn more. +{{% /alert %}} + +## Watching Components + +The Resource Provider, xDS Translator and Infra Manager etc. are key components that made up of Envoy Gateway, +they all follow the design of [Watching Components](../../../contributions/design/watching). + +Envoy Gateway collects the following metrics in Watching Components: + +| Name | Description | +|----------------------------------------|--------------------------------------------------------------| +| `watchable_depth` | Current depth of watchable map. | +| `watchable_subscribe_duration_seconds` | How long in seconds a subscribed watchable queue is handled. | +| `watchable_subscribe_total` | Total number of subscribed watchable queue. | + +Each metric includes the `runner` label to identify the corresponding components, +the relationship between label values and components is as follows: + +| Value | Components | +|--------------------|---------------------------------| +| `gateway-api` | Gateway API Translator | +| `infrastructure` | Infrastructure Manager | +| `xds-server` | xDS Server | +| `xds-translator` | xDS Translator | +| `global-ratelimit` | Global RateLimit xDS Translator | + +Metrics may include one or more additional labels, such as `message`, `status` and `reason` etc. + +## Status Updater + +Envoy Gateway monitors the status updates of various resources (like `GatewayClass`, `Gateway` and `HTTPRoute` etc.) through Status Updater. + +Envoy Gateway collects the following metrics in Status Updater: + +| Name | Description | +|----------------------------------|------------------------------------------------| +| `status_update_total` | Total number of status update by object kind. | +| `status_update_duration_seconds` | How long a status update takes to finish. | + +Each metric includes `kind` label to identify the corresponding resources. + +## xDS Server + +Envoy Gateway monitors the cache and xDS connection status in xDS Server. + +Envoy Gateway collects the following metrics in xDS Server: + +| Name | Description | +|-------------------------------|--------------------------------------------------------| +| `xds_snapshot_create_total` | Total number of xds snapshot cache creates. | +| `xds_snapshot_update_total` | Total number of xds snapshot cache updates by node id. | +| `xds_stream_duration_seconds` | How long a xds stream takes to finish. | + +- For xDS snapshot cache update and xDS stream connection status, each metric includes `nodeID` label to identify the connection peer. +- For xDS stream connection status, each metric also includes `streamID` label to identify the connection stream, and `isDeltaStream` label to identify the delta connection stream. + +## Infrastructure Manager + +Envoy Gateway monitors the `apply` (`create` or `update`) and `delete` operations in Infrastructure Manager. + +Envoy Gateway collects the following metrics in Infrastructure Manager: + +| Name | Description | +|------------------------------------|---------------------------------------------------------| +| `resource_apply_total` | Total number of applied resources. | +| `resource_apply_duration_seconds` | How long in seconds a resource be applied successfully. | +| `resource_delete_total` | Total number of deleted resources. | +| `resource_delete_duration_seconds` | How long in seconds a resource be deleted successfully. | + +Each metric includes the `kind` label to identify the corresponding resources being applied or deleted by Infrastructure Manager. + +Metrics may also include `name` and `namespace` label to identify the name and namespace of corresponding Infrastructure Manager. + +## Wasm + +Envoy Gateway monitors the status of Wasm remote fetch cache. + +| Name | Description | +|---------------------------|--------------------------------------------------| +| `wasm_cache_entries` | Number of Wasm remote fetch cache entries. | +| `wasm_cache_lookup_total` | Total number of Wasm remote fetch cache lookups. | +| `wasm_remote_fetch_total` | Total number of Wasm remote fetches and results. | + +For metric `wasm_cache_lookup_total`, we are using `hit` label (boolean) to indicate whether the Wasm cache has been hit. + + +[prom-format]: https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format diff --git a/site/content/en/docs/tasks/observability/gateway-observability.md b/site/content/en/docs/tasks/observability/gateway-observability.md new file mode 100644 index 00000000000..6e0040b4f5d --- /dev/null +++ b/site/content/en/docs/tasks/observability/gateway-observability.md @@ -0,0 +1,176 @@ +--- +title: "Gateway Observability" +--- + +Envoy Gateway provides observability for the ControlPlane and the underlying EnvoyProxy instances. +This task show you how to config gateway control-plane observability, includes metrics. + +## Prerequisites + +{{< boilerplate o11y_prerequisites >}} + +## Metrics + +The default installation of Envoy Gateway installs a default [EnvoyGateway][] configuration and attaches it +using a `ConfigMap`. In this section, we will update this resource to enable various ways to retrieve metrics +from Envoy Gateway. + +{{% alert title="Exported Metrics" color="warning" %}} +Refer to the [Gateway Exported Metrics List](./gateway-exported-metrics) to learn more about Envoy Gateway's Metrics. +{{% /alert %}} + +### Retrieve Prometheus Metrics from Envoy Gateway + +By default, prometheus metric is enabled. You can directly retrieve metrics from Envoy Gateway: + +```shell +export ENVOY_POD_NAME=$(kubectl get pod -n envoy-gateway-system --selector=control-plane=envoy-gateway,app.kubernetes.io/instance=eg -o jsonpath='{.items[0].metadata.name}') +kubectl port-forward pod/$ENVOY_POD_NAME -n envoy-gateway-system 19001:19001 + +# check metrics +curl localhost:19001/metrics +``` + +The following is an example to disable prometheus metric for Envoy Gateway. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +After updating the `ConfigMap`, you will need to restart the `envoy-gateway` deployment so the configuration kicks in: + +```shell +kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system +``` + +### Enable Open Telemetry sink in Envoy Gateway + +The following is an example to send metric via Open Telemetry sink to OTEL gRPC Collector. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +After updating the `ConfigMap`, you will need to restart the `envoy-gateway` deployment so the configuration kicks in: + +```shell +kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system +``` + +Verify OTel-Collector metrics: + +```shell +export OTEL_POD_NAME=$(kubectl get pod -n monitoring --selector=app.kubernetes.io/name=opentelemetry-collector -o jsonpath='{.items[0].metadata.name}') +kubectl port-forward pod/$OTEL_POD_NAME -n monitoring 19001:19001 + +# check metrics +curl localhost:19001/metrics +``` + +[EnvoyGateway]: ../../api/extension_types#envoygateway diff --git a/site/content/en/docs/tasks/observability/grafana-integration.md b/site/content/en/docs/tasks/observability/grafana-integration.md new file mode 100644 index 00000000000..259f6958bf0 --- /dev/null +++ b/site/content/en/docs/tasks/observability/grafana-integration.md @@ -0,0 +1,87 @@ +--- +title: "Visualising metrics using Grafana" +--- + +Envoy Gateway provides support for exposing Envoy Gateway and Envoy Proxy metrics to a Prometheus instance. +This task shows you how to visualise the metrics exposed to Prometheus using Grafana. + +## Prerequisites + +{{< boilerplate o11y_prerequisites >}} + +Follow the steps from the [Gateway Observability](./gateway-observability) and [Proxy Metrics](./proxy-metric) to enable Prometheus metrics +for both Envoy Gateway (Control Plane) and Envoy Proxy (Data Plane). + +Expose endpoints: + +```shell +GRAFANA_IP=$(kubectl get svc grafana -n monitoring -o jsonpath='{.status.loadBalancer.ingress[0].ip}') +``` + +## Connecting Grafana with Prometheus datasource + +To visualise metrics from Prometheus, we have to connect Grafana with Prometheus. If you installed Grafana follow the command +from prerequisites sections, the Prometheus datasource should be already configured. + +You can also add the datasource manually by following the instructions from [Grafana Docs](https://grafana.com/docs/grafana/latest/datasources/prometheus/configure-prometheus-data-source/). + +## Accessing Grafana + +You can access the Grafana instance by visiting `http://{GRAFANA_IP}`, derived in prerequisites. + +To log in to Grafana, use the credentials `admin:admin`. + +Envoy Gateway has examples of dashboard for you to get started, you can check them out under `Dashboards/envoy-gateway`. + +If you'd like import Grafana dashboards on your own, please refer to Grafana docs for [importing dashboards](https://grafana.com/docs/grafana/latest/dashboards/manage-dashboards/#import-a-dashboard). + +### Envoy Proxy Global + +This dashboard example shows the overall downstream and upstream stats for each Envoy Proxy instance. + +![Envoy Proxy Global](/img/envoy-proxy-global-dashboard.png) + +### Envoy Clusters + +This dashboard example shows the overall stats for each cluster from Envoy Proxy fleet. + +![Envoy Clusters](/img/envoy-clusters-dashboard.png) + +### Envoy Gateway Global + +This dashboard example shows the overall stats exported by Envoy Gateway fleet. + +![Envoy Gateway Global: Watching Components](/img/envoy-gateway-global-watching-components.png) + +![Envoy Gateway Global: Status Updater](/img/envoy-gateway-global-status-updater.png) + +![Envoy Gateway Global: xDS Server](/img/envoy-gateway-global-xds-server.png) + +![Envoy Gateway Global: Infrastructure Manager](/img/envoy-gateway-global-infra-manager.png) + +### Resources Monitor + +This dashboard example shows the overall resources stats for both Envoy Gateway and Envoy Proxy fleet. + +![Envoy Gateway Resources](/img/resources-monitor-dashboard.png) + +## Update Dashboards + +All dashboards of Envoy Gateway are maintained under `charts/gateway-addons-helm/dashboards`, +feel free to make [contributions](../../../contributions/CONTRIBUTING). + +### Grafonnet + +Newer dashboards are generated with [Jsonnet](https://jsonnet.org/) with the [Grafonnet](https://grafana.github.io/grafonnet/index.html). +This is the preferred method for any new dashboards. + +You can run `make helm-generate.gateway-addons-helm` to generate new version of dashboards. +All the generated dashboards have a `.gen.json` suffix. + +### Legacy Dashboards + +Many of our older dashboards are manually created in the UI and exported as JSON and checked in. + +These example dashboards cannot be updated in-place by default, if you are trying to +make some changes to the older dashboards, you can save them directly as a JSON file +and then re-import. diff --git a/site/content/en/docs/tasks/observability/proxy-accesslog.md b/site/content/en/docs/tasks/observability/proxy-accesslog.md new file mode 100644 index 00000000000..fb0200f1739 --- /dev/null +++ b/site/content/en/docs/tasks/observability/proxy-accesslog.md @@ -0,0 +1,251 @@ +--- +title: "Proxy Access Logs" +--- + +Envoy Gateway provides observability for the ControlPlane and the underlying EnvoyProxy instances. +This task show you how to config proxy access logs. + +## Prerequisites + +{{< boilerplate o11y_prerequisites >}} + +By default, the Service type of `loki` is ClusterIP, you can change it to LoadBalancer type for further usage: + +```shell +kubectl patch service loki -n monitoring -p '{"spec": {"type": "LoadBalancer"}}' +``` + +Expose endpoints: + +```shell +LOKI_IP=$(kubectl get svc loki -n monitoring -o jsonpath='{.status.loadBalancer.ingress[0].ip}') +``` + +## Default Access Log + +If custom format string is not specified, Envoy Gateway uses the following default format: + +```json +{ + "start_time": "%START_TIME%", + "method": "%REQ(:METHOD)%", + "x-envoy-origin-path": "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%", + "protocol": "%PROTOCOL%", + "response_code": "%RESPONSE_CODE%", + "response_flags": "%RESPONSE_FLAGS%", + "response_code_details": "%RESPONSE_CODE_DETAILS%", + "connection_termination_details": "%CONNECTION_TERMINATION_DETAILS%", + "upstream_transport_failure_reason": "%UPSTREAM_TRANSPORT_FAILURE_REASON%", + "bytes_received": "%BYTES_RECEIVED%", + "bytes_sent": "%BYTES_SENT%", + "duration": "%DURATION%", + "x-envoy-upstream-service-time": "%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%", + "x-forwarded-for": "%REQ(X-FORWARDED-FOR)%", + "user-agent": "%REQ(USER-AGENT)%", + "x-request-id": "%REQ(X-REQUEST-ID)%", + ":authority": "%REQ(:AUTHORITY)%", + "upstream_host": "%UPSTREAM_HOST%", + "upstream_cluster": "%UPSTREAM_CLUSTER%", + "upstream_local_address": "%UPSTREAM_LOCAL_ADDRESS%", + "downstream_local_address": "%DOWNSTREAM_LOCAL_ADDRESS%", + "downstream_remote_address": "%DOWNSTREAM_REMOTE_ADDRESS%", + "requested_server_name": "%REQUESTED_SERVER_NAME%", + "route_name": "%ROUTE_NAME%" +} +``` + +> Note: Envoy Gateway disable envoy headers by default, you can enable it by setting `EnableEnvoyHeaders` to `true` in the [ClientTrafficPolicy](../../api/extension_types#backendtrafficpolicy) CRD. + + +Verify logs from loki: + +```shell +curl -s "http://$LOKI_IP:3100/loki/api/v1/query_range" --data-urlencode "query={job=\"fluentbit\"}" | jq '.data.result[0].values' +``` + +## Disable Access Log + +If you want to disable it, set the `telemetry.accesslog.disable` to `true` in the `EnvoyProxy` CRD. + +```shell +kubectl apply -f - <}} + +## Metrics + +By default, Envoy Gateway expose metrics with prometheus endpoint. + +Verify metrics: + +```shell +export ENVOY_POD_NAME=$(kubectl get pod -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}') +kubectl port-forward pod/$ENVOY_POD_NAME -n envoy-gateway-system 19001:19001 + +# check metrics +curl localhost:19001/stats/prometheus | grep "default/backend/rule/0" +``` + +You can disable metrics by setting the `telemetry.metrics.prometheus.disable` to `true` in the `EnvoyProxy` CRD. + +```shell +kubectl apply -f https://raw.githubusercontent.com/envoyproxy/gateway/latest/examples/kubernetes/metric/disable-prometheus.yaml +``` + +Envoy Gateway can send metrics to OpenTelemetry Sink. +Send metrics to OTel-Collector: + +```shell +kubectl apply -f https://raw.githubusercontent.com/envoyproxy/gateway/latest/examples/kubernetes/metric/otel-sink.yaml +``` + +Verify OTel-Collector metrics: + +```shell +export OTEL_POD_NAME=$(kubectl get pod -n monitoring --selector=app.kubernetes.io/name=opentelemetry-collector -o jsonpath='{.items[0].metadata.name}') +kubectl port-forward pod/$OTEL_POD_NAME -n monitoring 19001:19001 + +# check metrics +curl localhost:19001/metrics | grep "default/backend/rule/0" +``` diff --git a/site/content/en/docs/tasks/observability/proxy-trace.md b/site/content/en/docs/tasks/observability/proxy-trace.md new file mode 100644 index 00000000000..ddaf68e415a --- /dev/null +++ b/site/content/en/docs/tasks/observability/proxy-trace.md @@ -0,0 +1,233 @@ +--- +title: "Proxy Tracing" +--- + +Envoy Gateway provides observability for the ControlPlane and the underlying EnvoyProxy instances. +This task show you how to config proxy tracing. + +## Prerequisites + +{{< boilerplate o11y_prerequisites >}} + +Expose Tempo endpoints: + +```shell +TEMPO_IP=$(kubectl get svc tempo -n monitoring -o jsonpath='{.status.loadBalancer.ingress[0].ip}') +``` + +## Traces + +By default, Envoy Gateway doesn't send traces to any sink. +You can enable traces by setting the `telemetry.tracing` in the [EnvoyProxy][envoy-proxy-crd] CRD. +Currently, Envoy Gateway support OpenTelemetry and [Zipkin](../../api/extension_types#zipkintracingprovider) tracer. + +### Tracing Provider + +The following configurations show how to apply proxy with different providers: + +{{< tabpane text=true >}} +{{% tab header="OpenTelemetry" %}} + +```shell +kubectl apply -f - <}} + +Query trace by trace id: + +```shell +curl -s "http://$TEMPO_IP:3100/api/traces/" | jq +``` + + +### Sampling Rate + +Envoy Gateway use 100% sample rate, which means all requests will be traced. +This may cause performance issues when traffic is very high, you can adjust +the sample rate by setting the `telemetry.tracing.samplingRate` in the [EnvoyProxy][envoy-proxy-crd] CRD. + +The following configurations show how to apply proxy with 1% sample rates: + +```shell +kubectl apply -f - <}} + +Follow the steps from the [Global Rate Limit](../traffic/global-rate-limit) to install RateLimit. + +## Traces + +By default, the Envoy Gateway does not configure RateLimit to send traces to the OpenTelemetry Sink. +You can configure the collector in the `rateLimit.telemetry.tracing` of the `EnvoyGateway`CRD. + +RateLimit uses the OpenTelemetry Exporter to export traces to the collector. +You can configure a collector that supports the OTLP protocol, which includes but is not limited to: OpenTelemetry Collector, Jaeger, Zipkin, and so on. + +***Note:*** + +* By default, the Envoy Gateway configures a `100%` sampling rate for RateLimit, which may lead to performance issues. + +Assuming the OpenTelemetry Collector is running in the `observability` namespace, and it has a service named `otel-svc`, +we only want to sample `50%` of the trace data. We would configure it as follows: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +After updating the ConfigMap, you will need to restart the envoy-gateway deployment so the configuration kicks in: + +```shell +kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system +``` diff --git a/site/content/en/docs/tasks/operations/_index.md b/site/content/en/docs/tasks/operations/_index.md new file mode 100644 index 00000000000..d87097c7d1e --- /dev/null +++ b/site/content/en/docs/tasks/operations/_index.md @@ -0,0 +1,5 @@ +--- +title: "Operations" +weight: 4 +description: This section includes Operations tasks. +--- diff --git a/site/content/en/docs/tasks/operations/customize-envoyproxy.md b/site/content/en/docs/tasks/operations/customize-envoyproxy.md new file mode 100644 index 00000000000..562237bfc43 --- /dev/null +++ b/site/content/en/docs/tasks/operations/customize-envoyproxy.md @@ -0,0 +1,955 @@ +--- +title: "Customize EnvoyProxy" +--- + +Envoy Gateway provides an [EnvoyProxy][] CRD that can be linked to the ParametersRef +in GatewayClass, allowing cluster admins to customize the managed EnvoyProxy Deployment and +Service. To learn more about GatewayClass and ParametersRef, please refer to [Gateway API documentation][]. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +Before you start, you need to add `ParametersRef` in GatewayClass, and refer to EnvoyProxy Config: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +## Customize EnvoyProxy Deployment Replicas + +You can customize the EnvoyProxy Deployment Replicas via EnvoyProxy Config like: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +After you apply the config, you should see the replicas of envoyproxy changes to 2. +And also you can dynamically change the value. + +``` shell +kubectl get deployment -l gateway.envoyproxy.io/owning-gateway-name=eg -n envoy-gateway-system +``` + +## Customize EnvoyProxy Image + +You can customize the EnvoyProxy Image via EnvoyProxy Config like: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +After applying the config, you can get the deployment image, and see it has changed. + +## Customize EnvoyProxy Pod Annotations + +You can customize the EnvoyProxy Pod Annotations via EnvoyProxy Config like: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +After applying the config, you can get the envoyproxy pods, and see new annotations has been added. + +## Customize EnvoyProxy Deployment Resources + +You can customize the EnvoyProxy Deployment Resources via EnvoyProxy Config like: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +## Customize EnvoyProxy Deployment Env + +You can customize the EnvoyProxy Deployment Env via EnvoyProxy Config like: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +> Envoy Gateway has provided two initial `env` `ENVOY_GATEWAY_NAMESPACE` and `ENVOY_POD_NAME` for envoyproxy container. + +After applying the config, you can get the envoyproxy deployment, and see resources has been changed. + +## Customize EnvoyProxy Deployment Volumes or VolumeMounts + +You can customize the EnvoyProxy Deployment Volumes or VolumeMounts via EnvoyProxy Config like: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +After applying the config, you can get the envoyproxy deployment, and see resources has been changed. + +## Customize EnvoyProxy Service Annotations + +You can customize the EnvoyProxy Service Annotations via EnvoyProxy Config like: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +After applying the config, you can get the envoyproxy service, and see annotations has been added. + +## Customize EnvoyProxy Bootstrap Config + +You can customize the EnvoyProxy bootstrap config via EnvoyProxy Config. +There are two ways to customize it: + +* Replace: the whole bootstrap config will be replaced by the config you provided. +* Merge: the config you provided will be merged into the default bootstrap config. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +You can use [egctl translate][] +to get the default xDS Bootstrap configuration used by Envoy Gateway. + +After applying the config, the bootstrap config will be overridden by the new config you provided. +Any errors in the configuration will be surfaced as status within the `GatewayClass` resource. +You can also validate this configuration using [egctl translate][]. + +## Customize EnvoyProxy Horizontal Pod Autoscaler + +You can enable [Horizontal Pod Autoscaler](https://github.com/envoyproxy/gateway/issues/703) for EnvoyProxy Deployment. However, before enabling the HPA for EnvoyProxy, please ensure that the [metrics-server](https://github.com/kubernetes-sigs/metrics-server) component is installed in the cluster. + +Once confirmed, you can apply it via EnvoyProxy Config as shown below: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +After applying the config, the EnvoyProxy HPA (Horizontal Pod Autoscaler) is generated. However, upon activating the EnvoyProxy's HPA, the Envoy Gateway will no longer reference the `replicas` field specified in the `envoyDeployment`, as outlined [here](#customize-envoyproxy-deployment-replicas). + +## Customize EnvoyProxy Command line options + +You can customize the EnvoyProxy Command line options via `spec.extraArgs` in EnvoyProxy Config. +For example, the following configuration will add `--disable-extensions` arg in order to disable `envoy.access_loggers/envoy.access_loggers.wasm` extension: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +## Customize EnvoyProxy with Patches + +You can customize the EnvoyProxy using patches. + +### Patching Deployment for EnvoyProxy + +For example, the following configuration will add resource limits to the `envoy` and the `shutdown-manager` containers in the `envoyproxy` deployment: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +After applying the configuration, you will see the change in both containers in the `envoyproxy` deployment. + +### Patching Service for EnvoyProxy + +For example, the following configuration will add an annotation for the `envoyproxy` service: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +After applying the configuration, you will see the `custom-annotation: foobar` has been added to the `envoyproxy` service. + +## Customize Filter Order + +Under the hood, Envoy Gateway uses a series of [Envoy HTTP filters](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/http_filters) +to process HTTP requests and responses, and to apply various policies. + +By default, Envoy Gateway applies the following filters in the order shown: +* envoy.filters.http.fault +* envoy.filters.http.cors +* envoy.filters.http.ext_authz +* envoy.filters.http.basic_authn +* envoy.filters.http.oauth2 +* envoy.filters.http.jwt_authn +* envoy.filters.http.ext_proc +* envoy.filters.http.wasm +* envoy.filters.http.rbac +* envoy.filters.http.local_ratelimit +* envoy.filters.http.ratelimit +* envoy.filters.http.router + +The default order in which these filters are applied is opinionated and may not suit all use cases. +To address this, Envoy Gateway allows you to adjust the execution order of these filters with the `filterOrder` field in the [EnvoyProxy][] resource. + +`filterOrder` is a list of customized filter order configurations. Each configuration can specify a filter +name and a filter to place it before or after. These configurations are applied in the order they are listed. +If a filter occurs in multiple configurations, the final order is the result of applying all these configurations in order. +To avoid conflicts, it is recommended to only specify one configuration per filter. + +For example, the following configuration moves the `envoy.filters.http.wasm` filter before the `envoy.filters.http.jwt_authn` +filter and the `envoy.filters.http.cors` filter after the `envoy.filters.http.basic_authn` filter: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +[Gateway API documentation]: https://gateway-api.sigs.k8s.io/ +[EnvoyProxy]: ../../../api/extension_types#envoyproxy +[egctl translate]: ../egctl/#validating-gateway-api-configuration diff --git a/site/content/en/docs/tasks/operations/deployment-mode.md b/site/content/en/docs/tasks/operations/deployment-mode.md new file mode 100644 index 00000000000..16b339fe571 --- /dev/null +++ b/site/content/en/docs/tasks/operations/deployment-mode.md @@ -0,0 +1,1072 @@ +--- +title: "Deployment Mode" +--- +## Deployment modes + +### One GatewayClass per Envoy Gateway Controller +* An Envoy Gateway is associated with a single [GatewayClass][] resource under one controller. +This is the simplest deployment mode and is suitable for scenarios where each Gateway needs to have its own dedicated set of resources and configurations. + +### Multiple GatewayClasses per Envoy Gateway Controller +* An Envoy Gateway is associated with multiple [GatewayClass][] resources under one controller. +* Support for accepting multiple GatewayClasses was added [here][issue1231]. + +### Separate Envoy Gateway Controllers +If you've instantiated multiple GatewayClasses, you can also run separate Envoy Gateway controllers in different namespaces, linking a GatewayClass to each of them for multi-tenancy. +Please follow the example [Multi-tenancy](#multi-tenancy). + +### Merged Gateways onto a single EnvoyProxy fleet +By default, each Gateway has its own dedicated set of Envoy Proxy and its configurations. +However, for some deployments, it may be more convenient to merge listeners across multiple Gateways and deploy a single Envoy Proxy fleet. + +This can help to efficiently utilize the infra resources in the cluster and manage them in a centralized manner, or have a single IP address for all of the listeners. +Setting the `mergeGateways` field in the EnvoyProxy resource linked to GatewayClass will result in merging all Gateway listeners under one GatewayClass resource. + +* The tuple of port, protocol, and hostname must be unique across all Listeners. + +Please follow the example [Merged gateways deployment](#merged-gateways-deployment). + +### Supported Modes + +#### Kubernetes + +* The default deployment model is - Envoy Gateway **watches** for resources such a `Service` & `HTTPRoute` in **all** namespaces +and **creates** managed data plane resources such as EnvoyProxy `Deployment` in the **namespace where Envoy Gateway is running**. +* Envoy Gateway also supports [Namespaced deployment mode][], you can watch resources in the specific namespaces by assigning +`EnvoyGateway.provider.kubernetes.watch.namespaces` or `EnvoyGateway.provider.kubernetes.watch.namespaceSelector` and **creates** managed data plane resources in the **namespace where Envoy Gateway is running**. +* Support for alternate deployment modes is being tracked [here][issue1117]. + +### Multi-tenancy + +#### Kubernetes + +* A `tenant` is a group within an organization (e.g. a team or department) who shares organizational resources. We recommend +each `tenant` deploy their own Envoy Gateway controller in their respective `namespace`. Below is an example of deploying Envoy Gateway +by the `marketing` and `product` teams in separate namespaces. + +* Lets deploy Envoy Gateway in the `marketing` namespace and also watch resources only in this namespace. We are also setting the controller name to a unique string here `gateway.envoyproxy.io/marketing-gatewayclass-controller`. + +```shell +helm install \ +--set config.envoyGateway.gateway.controllerName=gateway.envoyproxy.io/marketing-gatewayclass-controller \ +--set config.envoyGateway.provider.kubernetes.watch.type=Namespaces \ +--set config.envoyGateway.provider.kubernetes.watch.namespaces={marketing} \ +eg-marketing oci://docker.io/envoyproxy/gateway-helm \ +--version v0.0.0-latest -n marketing --create-namespace +``` + +Lets create a `GatewayClass` linked to the marketing team's Envoy Gateway controller, and as well other resources linked to it, so the `backend` application operated by this team can be exposed to external clients. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Lets port forward to the generated envoy proxy service in the `marketing` namespace and send a request to it. + +```shell +export ENVOY_SERVICE=$(kubectl get svc -n marketing --selector=gateway.envoyproxy.io/owning-gateway-namespace=marketing,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}') +kubectl -n marketing port-forward service/${ENVOY_SERVICE} 8888:8080 & +``` + +```shell +curl --verbose --header "Host: www.marketing.example.com" http://localhost:8888/get +``` + +```console +* Trying 127.0.0.1:8888... +* Connected to localhost (127.0.0.1) port 8888 (#0) +> GET /get HTTP/1.1 +> Host: www.marketing.example.com +> User-Agent: curl/7.86.0 +> Accept: */* +> +Handling connection for 8888 +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< date: Thu, 20 Apr 2023 19:19:42 GMT +< content-length: 521 +< x-envoy-upstream-service-time: 0 +< server: envoy +< +{ + "path": "/get", + "host": "www.marketing.example.com", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/7.86.0" + ], + "X-Envoy-Expected-Rq-Timeout-Ms": [ + "15000" + ], + "X-Envoy-Internal": [ + "true" + ], + "X-Forwarded-For": [ + "10.1.0.157" + ], + "X-Forwarded-Proto": [ + "http" + ], + "X-Request-Id": [ + "c637977c-458a-48ae-92b3-f8c429849322" + ] + }, + "namespace": "marketing", + "ingress": "", + "service": "", + "pod": "backend-74888f465f-bcs8f" +* Connection #0 to host localhost left intact +``` + +* Lets deploy Envoy Gateway in the `product` namespace and also watch resources only in this namespace. + +```shell +helm install \ +--set config.envoyGateway.gateway.controllerName=gateway.envoyproxy.io/product-gatewayclass-controller \ +--set config.envoyGateway.provider.kubernetes.watch.type=Namespaces \ +--set config.envoyGateway.provider.kubernetes.watch.namespaces={product} \ +eg-product oci://docker.io/envoyproxy/gateway-helm \ +--version v0.0.0-latest -n product --create-namespace +``` + +Lets create a `GatewayClass` linked to the product team's Envoy Gateway controller, and as well other resources linked to it, so the `backend` application operated by this team can be exposed to external clients. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Lets port forward to the generated envoy proxy service in the `product` namespace and send a request to it. + +```shell +export ENVOY_SERVICE=$(kubectl get svc -n product --selector=gateway.envoyproxy.io/owning-gateway-namespace=product,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}') +kubectl -n product port-forward service/${ENVOY_SERVICE} 8889:8080 & +``` + +```shell +curl --verbose --header "Host: www.product.example.com" http://localhost:8889/get +``` + +```shell +* Trying 127.0.0.1:8889... +* Connected to localhost (127.0.0.1) port 8889 (#0) +> GET /get HTTP/1.1 +> Host: www.product.example.com +> User-Agent: curl/7.86.0 +> Accept: */* +> +Handling connection for 8889 +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< date: Thu, 20 Apr 2023 19:20:17 GMT +< content-length: 517 +< x-envoy-upstream-service-time: 0 +< server: envoy +< +{ + "path": "/get", + "host": "www.product.example.com", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/7.86.0" + ], + "X-Envoy-Expected-Rq-Timeout-Ms": [ + "15000" + ], + "X-Envoy-Internal": [ + "true" + ], + "X-Forwarded-For": [ + "10.1.0.156" + ], + "X-Forwarded-Proto": [ + "http" + ], + "X-Request-Id": [ + "39196453-2250-4331-b756-54003b2853c2" + ] + }, + "namespace": "product", + "ingress": "", + "service": "", + "pod": "backend-74888f465f-64fjs" +* Connection #0 to host localhost left intact +``` + +With the below command you can ensure that you are no able to access the marketing team's backend exposed using the `www.marketing.example.com` hostname +and the product team's data plane. + +```shell +curl --verbose --header "Host: www.marketing.example.com" http://localhost:8889/get +``` + +```console +* Trying 127.0.0.1:8889... +* Connected to localhost (127.0.0.1) port 8889 (#0) +> GET /get HTTP/1.1 +> Host: www.marketing.example.com +> User-Agent: curl/7.86.0 +> Accept: */* +> +Handling connection for 8889 +* Mark bundle as not supporting multiuse +< HTTP/1.1 404 Not Found +< date: Thu, 20 Apr 2023 19:22:13 GMT +< server: envoy +< content-length: 0 +< +* Connection #0 to host localhost left intact +``` + +### Merged gateways deployment + +In this example, we will deploy GatewayClass + +```shell +apiVersion: gateway.networking.k8s.io/v1 +kind: GatewayClass +metadata: + name: merged-eg +spec: + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parametersRef: + group: gateway.envoyproxy.io + kind: EnvoyProxy + name: custom-proxy-config + namespace: envoy-gateway-system +``` + +with a referenced [EnvoyProxy][] resource configured to enable merged Gateways deployment mode. + +```shell +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyProxy +metadata: + name: custom-proxy-config + namespace: envoy-gateway-system +spec: + mergeGateways: true +``` + +#### Deploy merged-gateways example + +Deploy resources on your cluster from the example. + +```shell +kubectl apply -f https://raw.githubusercontent.com/envoyproxy/gateway/latest/examples/kubernetes/merged-gateways.yaml +``` + +Verify that Gateways are deployed and programmed + +```shell +kubectl get gateways -n default + +NAMESPACE NAME CLASS ADDRESS PROGRAMMED AGE +default merged-eg-1 merged-eg 172.18.255.202 True 2m4s +default merged-eg-2 merged-eg 172.18.255.202 True 2m4s +default merged-eg-3 merged-eg 172.18.255.202 True 2m4s +``` + +Verify that HTTPRoutes are deployed + +```shell +kubectl get httproute -n default +NAMESPACE NAME HOSTNAMES AGE +default hostname1-route ["www.merged1.com"] 2m4s +default hostname2-route ["www.merged2.com"] 2m4s +default hostname3-route ["www.merged3.com"] 2m4s +``` + +If you take a look at the deployed Envoy Proxy service you would notice that all of the Gateway listeners ports are added to that service. + +```shell +kubectl get service -n envoy-gateway-system +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +envoy-gateway ClusterIP 10.96.141.4 18000/TCP,18001/TCP 6m43s +envoy-gateway-metrics-service ClusterIP 10.96.113.191 19001/TCP 6m43s +envoy-merged-eg-668ac7ae LoadBalancer 10.96.48.255 172.18.255.202 8081:30467/TCP,8082:31793/TCP,8080:31153/TCP 3m17s +``` + +There should be also one deployment (envoy-merged-eg-668ac7ae-775f9865d-55zhs) for every Gateway and its name should reference the name of the GatewayClass. + +```shell +kubectl get pods -n envoy-gateway-system +NAME READY STATUS RESTARTS AGE +envoy-gateway-5d998778f6-wr6m9 1/1 Running 0 6m43s +envoy-merged-eg-668ac7ae-775f9865d-55zhs 2/2 Running 0 3m17s +``` + +#### Testing the Configuration + +Get the name of the merged gateways Envoy service: + +```shell +export ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gatewayclass=merged-eg -o jsonpath='{.items[0].metadata.name}') +``` + +Fetch external IP of the service: + +```shell +export GATEWAY_HOST=$(kubectl get svc/${ENVOY_SERVICE} -n envoy-gateway-system -o jsonpath='{.status.loadBalancer.ingress[0].ip}') +``` + +In certain environments, the load balancer may be exposed using a hostname, instead of an IP address. If so, replace +`ip` in the above command with `hostname`. + +Curl the route hostname-route2 through Envoy proxy: + +```shell +curl --header "Host: www.merged2.com" http://$GATEWAY_HOST:8081/example2 +``` + +```shell +{ + "path": "/example2", + "host": "www.merged2.com", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/8.4.0" + ], + "X-Envoy-Internal": [ + "true" + ], + "X-Forwarded-For": [ + "172.18.0.2" + ], + "X-Forwarded-Proto": [ + "http" + ], + "X-Request-Id": [ + "deed2767-a483-4291-9429-0e256ab3a65f" + ] + }, + "namespace": "default", + "ingress": "", + "service": "", + "pod": "merged-backend-64ddb65fd7-ttv5z" +} +``` + +Curl the route hostname-route1 through Envoy proxy: + +```shell +curl --header "Host: www.merged1.com" http://$GATEWAY_HOST:8080/example +``` + +```shell +{ + "path": "/example", + "host": "www.merged1.com", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/8.4.0" + ], + "X-Envoy-Internal": [ + "true" + ], + "X-Forwarded-For": [ + "172.18.0.2" + ], + "X-Forwarded-Proto": [ + "http" + ], + "X-Request-Id": [ + "20a53440-6327-4c3c-bc8b-8e79e7311043" + ] + }, + "namespace": "default", + "ingress": "", + "service": "", + "pod": "merged-backend-64ddb65fd7-ttv5z" +} +``` + +#### Verify deployment of multiple GatewayClass + +Install the GatewayClass, Gateway, HTTPRoute and example app from [Quickstart][] example: + +```shell +kubectl apply -f https://github.com/envoyproxy/gateway/releases/download/latest/quickstart.yaml -n default +``` + +Lets create also and additional `Gateway` linked to the GatewayClass and `backend` application from Quickstart example. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify that Gateways are deployed and programmed + +```shell +kubectl get gateways -n default +``` + +```shell +NAME CLASS ADDRESS PROGRAMMED AGE +eg eg 172.18.255.203 True 114s +eg-2 eg 172.18.255.204 True 89s +merged-eg-1 merged-eg 172.18.255.202 True 8m33s +merged-eg-2 merged-eg 172.18.255.202 True 8m33s +merged-eg-3 merged-eg 172.18.255.202 True 8m33s +``` + +Verify that HTTPRoutes are deployed + +```shell +kubectl get httproute -n default +``` + +```shell +NAMESPACE NAME HOSTNAMES AGE +default backend ["www.example.com"] 2m29s +default eg-2 ["www.quickstart.example.com"] 87s +default hostname1-route ["www.merged1.com"] 10m4s +default hostname2-route ["www.merged2.com"] 10m4s +default hostname3-route ["www.merged3.com"] 10m4s +``` + +Verify that services are now deployed separately. + +```shell +kubectl get service -n envoy-gateway-system +``` + +```shell +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +envoy-default-eg-2-7e515b2f LoadBalancer 10.96.121.46 172.18.255.204 8080:32705/TCP 3m27s +envoy-default-eg-e41e7b31 LoadBalancer 10.96.11.244 172.18.255.203 80:31930/TCP 2m26s +envoy-gateway ClusterIP 10.96.141.4 18000/TCP,18001/TCP 14m25s +envoy-gateway-metrics-service ClusterIP 10.96.113.191 19001/TCP 14m25s +envoy-merged-eg-668ac7ae LoadBalancer 10.96.243.32 172.18.255.202 8082:31622/TCP,8080:32262/TCP,8081:32305/TCP 10m59s +``` + +There should be two deployments for each of newly deployed Gateway and its name should reference the name of the namespace and the Gateway. + +```shell +kubectl get pods -n envoy-gateway-system +``` + +```shell +NAME READY STATUS RESTARTS AGE +envoy-default-eg-2-7e515b2f-8c98fdf88-p6jhg 2/2 Running 0 3m27s +envoy-default-eg-e41e7b31-6f998d85d7-jpvmj 2/2 Running 0 2m26s +envoy-gateway-5d998778f6-wr6m9 1/1 Running 0 14m25s +envoy-merged-eg-668ac7ae-5958f7b7f6-9h9v2 2/2 Running 0 10m59s +``` + +#### Testing the Configuration + +Get the name of the merged gateways Envoy service: + +```shell +export DEFAULT_ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}') +``` + +Fetch external IP of the service: + +```shell +export DEFAULT_GATEWAY_HOST=$(kubectl get svc/${DEFAULT_ENVOY_SERVICE} -n envoy-gateway-system -o jsonpath='{.status.loadBalancer.ingress[0].ip}') +``` + +Curl the route Quickstart backend route through Envoy proxy: + +```shell +curl --header "Host: www.example.com" http://$DEFAULT_GATEWAY_HOST +``` + +```shell +{ + "path": "/", + "host": "www.example.com", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/8.4.0" + ], + "X-Envoy-Internal": [ + "true" + ], + "X-Forwarded-For": [ + "172.18.0.2" + ], + "X-Forwarded-Proto": [ + "http" + ], + "X-Request-Id": [ + "70a40595-67a1-4776-955b-2dee361baed7" + ] + }, + "namespace": "default", + "ingress": "", + "service": "", + "pod": "backend-96f75bbf-6w67z" +} +``` + +Curl the route hostname-route3 through Envoy proxy: + +```shell +curl --header "Host: www.merged3.com" http://$GATEWAY_HOST:8082/example3 +``` + +```shell +{ + "path": "/example3", + "host": "www.merged3.com", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/8.4.0" + ], + "X-Envoy-Internal": [ + "true" + ], + "X-Forwarded-For": [ + "172.18.0.2" + ], + "X-Forwarded-Proto": [ + "http" + ], + "X-Request-Id": [ + "47aeaef3-abb5-481a-ab92-c2ae3d0862d6" + ] + }, + "namespace": "default", + "ingress": "", + "service": "", + "pod": "merged-backend-64ddb65fd7-k84gv" +} +``` + +[Quickstart]: ../quickstart.md +[EnvoyProxy]: ../../api/extension_types#envoyproxy +[GatewayClass]: https://gateway-api.sigs.k8s.io/api-types/gatewayclass/ +[Namespaced deployment mode]: ../../api/extension_types#kuberneteswatchmode +[issue1231]: https://github.com/envoyproxy/gateway/issues/1231 +[issue1117]: https://github.com/envoyproxy/gateway/issues/1117 diff --git a/site/content/en/docs/tasks/operations/egctl.md b/site/content/en/docs/tasks/operations/egctl.md new file mode 100644 index 00000000000..ac1f13d7a61 --- /dev/null +++ b/site/content/en/docs/tasks/operations/egctl.md @@ -0,0 +1,908 @@ +--- +title: "Use egctl" +--- + +`egctl` is a command line tool to provide additional functionality for Envoy Gateway users. + + + +## egctl experimental translate + +This subcommand allows users to translate from an input configuration type to an output configuration type. + +The `translate` subcommand can translate Kubernetes resources to: +* Gateway API resources + This is useful in order to see how validation would occur if these resources were applied to Kubernetes. + + Use the `--to gateway-api` parameter to translate to Gateway API resources. + +* Envoy Gateway intermediate representation (IR) + This represents Envoy Gateway's translation of the Gateway API resources. + + Use the `--to ir` parameter to translate to Envoy Gateway intermediate representation. + +* Envoy Proxy xDS + This is the xDS configuration provided to Envoy Proxy. + + Use the `--to xds` parameter to translate to Envoy Proxy xDS. + + +In the below example, we will translate the Kubernetes resources (including the Gateway API resources) into xDS +resources. + +```shell +cat < Note: If CRDs are already installed, then we need to specify `--skip-crds` to avoid repeated installation of CRDs resources. + +```bash +egctl x install --name shop-backend --namespace shop +``` + + +## egctl experimental uninstall + +This subcommand can be used to uninstall envoy-gateway. + +```bash +egctl x uninstall +``` + +By default, this will only uninstall the envoy-gateway workload resource, if we want to also uninstall CRDs, we need to specify `--with-crds` + +```bash +egctl x uninstall --with-crds +``` \ No newline at end of file diff --git a/site/content/en/docs/tasks/quickstart.md b/site/content/en/docs/tasks/quickstart.md new file mode 100644 index 00000000000..4075eb5d478 --- /dev/null +++ b/site/content/en/docs/tasks/quickstart.md @@ -0,0 +1,156 @@ +--- +title: "Quickstart" +weight: 1 +description: Get started with Envoy Gateway in a few simple steps. +--- + +This "quick start" will help you get started with Envoy Gateway in a few simple steps. + +## Prerequisites + +A Kubernetes cluster. + +__Note:__ Refer to the [Compatibility Matrix](/news/releases/matrix) for supported Kubernetes versions. + +__Note:__ In case your Kubernetes cluster, does not have a LoadBalancer implementation, we recommend installing one +so the `Gateway` resource has an Address associated with it. We recommend using [MetalLB](https://metallb.universe.tf/installation/). + +## Installation + +Install the Gateway API CRDs and Envoy Gateway: + +```shell +helm install eg oci://docker.io/envoyproxy/gateway-helm --version v0.0.0-latest -n envoy-gateway-system --create-namespace +``` + +Wait for Envoy Gateway to become available: + +```shell +kubectl wait --timeout=5m -n envoy-gateway-system deployment/envoy-gateway --for=condition=Available +``` + +Install the GatewayClass, Gateway, HTTPRoute and example app: + +```shell +kubectl apply -f https://github.com/envoyproxy/gateway/releases/download/latest/quickstart.yaml -n default +``` + +**Note**: [`quickstart.yaml`] defines that Envoy Gateway will listen for +traffic on port 80 on its globally-routable IP address, to make it easy to use +browsers to test Envoy Gateway. When Envoy Gateway sees that its Listener is +using a privileged port (<1024), it will map this internally to an +unprivileged port, so that Envoy Gateway doesn't need additional privileges. +It's important to be aware of this mapping, since you may need to take it into +consideration when debugging. + +[`quickstart.yaml`]: https://github.com/envoyproxy/gateway/releases/download/latest/quickstart.yaml + +## Testing the Configuration + +{{< tabpane text=true >}} +{{% tab header="With External LoadBalancer Support" %}} + +You can also test the same functionality by sending traffic to the External IP. To get the external IP of the +Envoy service, run: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +In certain environments, the load balancer may be exposed using a hostname, instead of an IP address. If so, replace +`ip` in the above command with `hostname`. + +Curl the example app through Envoy proxy: + +```shell +curl --verbose --header "Host: www.example.com" http://$GATEWAY_HOST/get +``` + +{{% /tab %}} +{{% tab header="Without LoadBalancer Support" %}} + +Get the name of the Envoy service created the by the example Gateway: + +```shell +export ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}') +``` + +Port forward to the Envoy service: + +```shell +kubectl -n envoy-gateway-system port-forward service/${ENVOY_SERVICE} 8888:80 & +``` + +Curl the example app through Envoy proxy: + +```shell +curl --verbose --header "Host: www.example.com" http://localhost:8888/get +``` + +{{% /tab %}} +{{< /tabpane >}} + +## v1.1 Upgrade Notes + +Due to breaking changes in the Gateway API v1.1, some manual migration steps are required to upgrade Envoy Gateway to v1.1. + +Delete `BackendTLSPolicy` CRD (and resources): + +```shell +kubectl delete crd backendtlspolicies.gateway.networking.k8s.io +``` + +Update Gateway-API and Envoy Gateway CRDs: + +```shell +helm pull oci://docker.io/envoyproxy/gateway-helm --version v1.1.0 --untar +kubectl apply -f ./gateway-helm/crds/gatewayapi-crds.yaml +kubectl apply -f ./gateway-helm/crds/generated +``` + +Update your `BackendTLSPolicy` and `GRPCRoute` resources according to Gateway-API [v1.1 Upgrade Notes](https://gateway-api.sigs.k8s.io/guides/#v11-upgrade-notes) + +Update your Envoy Gateway xPolicy resources: remove the namespace section from targetRef. + +Install Envoy Gateway v1.1.0: + +```shell +helm upgrade eg oci://docker.io/envoyproxy/gateway-helm --version v1.1.0 -n envoy-gateway-system +``` + +## What to explore next? + +In this quickstart, you have: +- Installed Envoy Gateway +- Deployed a backend service, and a gateway +- Configured the gateway using Kubernetes Gateway API resources [Gateway](https://gateway-api.sigs.k8s.io/api-types/gateway/) and [HttpRoute](https://gateway-api.sigs.k8s.io/api-types/httproute/) to direct incoming requests over HTTP to the backend service. + +Here is a suggested list of follow-on tasks to guide you in your exploration of Envoy Gateway: + +- [HTTP Routing](traffic/http-routing) +- [Traffic Splitting](traffic/http-traffic-splitting) +- [Secure Gateways](security/secure-gateways/) +- [Global Rate Limit](traffic/global-rate-limit/) +- [gRPC Routing](traffic/grpc-routing/) + +Review the [Tasks](./) section for the scenario matching your use case. The Envoy Gateway tasks are organized by category: traffic management, security, extensibility, observability, and operations. + +## Clean-Up + +Use the steps in this section to uninstall everything from the quickstart. + +Delete the GatewayClass, Gateway, HTTPRoute and Example App: + +```shell +kubectl delete -f https://github.com/envoyproxy/gateway/releases/download/latest/quickstart.yaml --ignore-not-found=true +``` + +Delete the Gateway API CRDs and Envoy Gateway: + +```shell +helm uninstall eg -n envoy-gateway-system +``` + +## Next Steps + +Checkout the [Developer Guide](../../contributions/develop) to get involved in the project. diff --git a/site/content/en/docs/tasks/security/_index.md b/site/content/en/docs/tasks/security/_index.md new file mode 100644 index 00000000000..0e6a64144a7 --- /dev/null +++ b/site/content/en/docs/tasks/security/_index.md @@ -0,0 +1,5 @@ +--- +title: "Security" +weight: 2 +description: This section includes Security tasks. +--- diff --git a/site/content/en/docs/tasks/security/backend-mtls.md b/site/content/en/docs/tasks/security/backend-mtls.md new file mode 100644 index 00000000000..1d91c7a95f8 --- /dev/null +++ b/site/content/en/docs/tasks/security/backend-mtls.md @@ -0,0 +1,200 @@ +--- +title: "Backend Mutual TLS: Gateway to Backend" +--- + +This task demonstrates how mTLS can be achieved between the Gateway and a backend. +This task uses a self-signed CA, so it should be used for testing and demonstration purposes only. + +Envoy Gateway supports the Gateway-API defined [BackendTLSPolicy][] to establish TLS. For mTLS, the Gateway must authenticate by presenting a client certificate to the backend. + +## Prerequisites + +- OpenSSL to generate TLS assets. + +## Installation + +Follow the steps from the [Backend TLS][] to install Envoy Gateway and configure TLS to the backend server. + +## TLS Certificates + +Generate the certificates and keys used by the Gateway for authentication against the backend. + +Create a root certificate and private key to sign certificates: + +```shell +openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -subj '/O=example Inc./CN=example.com' -keyout clientca.key -out clientca.crt +``` + +Create a certificate and a private key for `www.example.com`: + +```shell +openssl req -new -newkey rsa:2048 -nodes -keyout client.key -out client.csr -subj "/CN=example-client/O=example organization" +openssl x509 -req -days 365 -CA clientca.crt -CAkey clientca.key -set_serial 0 -in client.csr -out client.crt +``` + +Store the cert/key in a Secret: + +```shell +kubectl -n envoy-gateway-system create secret tls example-client-cert --key=client.key --cert=client.crt +``` + +Store the CA Cert in another Secret: + +```shell +kubectl create configmap example-client-ca --from-file=clientca.crt +``` + +## Enforce Client Certificate Authentication on the backend + +Patch the existing quickstart backend to enforce Client Certificate Authentication. The patch will mount the server certificate and key required for TLS, and the CA certificate into the backend as volumes. + +```shell +kubectl patch deployment backend --type=json --patch ' + - op: add + path: /spec/template/spec/containers/0/volumeMounts + value: + - name: client-certs-volume + mountPath: /etc/client-certs + - name: secret-volume + mountPath: /etc/secret-volume + - op: add + path: /spec/template/spec/volumes + value: + - name: client-certs-volume + configMap: + name: example-client-ca + items: + - key: clientca.crt + path: crt + - name: secret-volume + secret: + secretName: example-cert + items: + - key: tls.crt + path: crt + - key: tls.key + path: key + - op: add + path: /spec/template/spec/containers/0/env/- + value: + name: TLS_CLIENT_CACERTS + value: /etc/client-certs/crt + ' +``` + +## Configure Envoy Proxy to use a client certificate + +In addition to enablement of backend TLS with the Gateway-API BackendTLSPolicy, Envoy Gateway supports customizing TLS parameters such as TLS Client Certificate. +To achieve this, the [EnvoyProxy][] resource can be used to specify a TLS Client Certificate. + +First, you need to add ParametersRef in GatewayClass, and refer to EnvoyProxy Config: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +## Testing mTLS + +Query the TLS-enabled backend through Envoy proxy: + +```shell +curl -v -HHost:www.example.com --resolve "www.example.com:80:127.0.0.1" \ +http://www.example.com:80/get +``` + +Inspect the output and see that the response contains the details of the TLS handshake between Envoy and the backend. +The response now contains a "peerCertificates" attribute that reflects the client certificate used by the Gateway to establish mTLS with the backend. + +```shell +< HTTP/1.1 200 OK +[...] + "tls": { + "version": "TLSv1.2", + "serverName": "www.example.com", + "negotiatedProtocol": "http/1.1", + "cipherSuite": "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" + "peerCertificates": ["-----BEGIN CERTIFICATE-----\n[...]-----END CERTIFICATE-----\n"] + } +``` + +[Backend TLS]: ./backend-tls +[BackendTLSPolicy]: https://gateway-api.sigs.k8s.io/api-types/backendtlspolicy/ +[EnvoyProxy]: ../../api/extension_types#envoyproxy \ No newline at end of file diff --git a/site/content/en/docs/tasks/security/backend-tls.md b/site/content/en/docs/tasks/security/backend-tls.md new file mode 100644 index 00000000000..53e9ccbd44a --- /dev/null +++ b/site/content/en/docs/tasks/security/backend-tls.md @@ -0,0 +1,390 @@ +--- +title: "Backend TLS: Gateway to Backend" +--- + +This task demonstrates how TLS can be achieved between the Gateway and a backend. +This task uses a self-signed CA, so it should be used for testing and demonstration purposes only. + +Envoy Gateway supports the Gateway-API defined [BackendTLSPolicy][]. + +## Prerequisites + +- OpenSSL to generate TLS assets. + +## Installation + +Follow the steps from the [Quickstart][] to install Envoy Gateway and the example manifest. + +## TLS Certificates + +Generate the certificates and keys used by the backend to terminate TLS connections from the Gateways. + +Create a root certificate and private key to sign certificates: + +```shell +openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -subj '/O=example Inc./CN=example.com' -keyout ca.key -out ca.crt +``` + +Create a certificate and a private key for `www.example.com`: + +```shell +openssl req -out www.example.com.csr -newkey rsa:2048 -nodes -keyout www.example.com.key -subj "/CN=www.example.com/O=example organization" -addext "subjectAltName = DNS:www.example.com" +openssl x509 -req -days 365 -CA ca.crt -CAkey ca.key -set_serial 0 -in www.example.com.csr -out www.example.com.crt +``` + +Note that the certificate must contain a DNS SAN for the relevant domain. + +Store the cert/key in a Secret: + +```shell +kubectl create secret tls example-cert --key=www.example.com.key --cert=www.example.com.crt +``` + +Store the CA Cert in another Secret: + +```shell +kubectl create configmap example-ca --from-file=ca.crt +``` + +## Setup TLS on the backend + +Patch the existing quickstart backend to enable TLS. The patch will mount the TLS certificate secret into the backend as volume. + +```shell +kubectl patch deployment backend --type=json --patch ' + - op: add + path: /spec/template/spec/containers/0/volumeMounts + value: + - name: secret-volume + mountPath: /etc/secret-volume + - op: add + path: /spec/template/spec/volumes + value: + - name: secret-volume + secret: + secretName: example-cert + items: + - key: tls.crt + path: crt + - key: tls.key + path: key + - op: add + path: /spec/template/spec/containers/0/env/- + value: + name: TLS_SERVER_CERT + value: /etc/secret-volume/crt + - op: add + path: /spec/template/spec/containers/0/env/- + value: + name: TLS_SERVER_PRIVKEY + value: /etc/secret-volume/key + ' +``` + +Create a service that exposes port 443 on the backend service. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Create a [BackendTLSPolicy][] instructing Envoy Gateway to establish a TLS connection with the backend and validate the backend certificate is issued by a trusted CA and contains an appropriate DNS SAN. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Patch the HTTPRoute's backend reference, so that it refers to the new TLS-enabled service: + +```shell +kubectl patch HTTPRoute backend --type=json --patch ' + - op: replace + path: /spec/rules/0/backendRefs/0/port + value: 443 + - op: replace + path: /spec/rules/0/backendRefs/0/name + value: tls-backend + ' +``` + +Verify the HTTPRoute status: + +```shell +kubectl get HTTPRoute backend -o yaml +``` + +## Testing backend TLS + +{{< tabpane text=true >}} +{{% tab header="With External LoadBalancer Support" %}} + +Get the External IP of the Gateway: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +Query the example app through the Gateway: + +```shell +curl -v -HHost:www.example.com --resolve "www.example.com:80:${GATEWAY_HOST}" \ +http://www.example.com:80/get +``` + +Inspect the output and see that the response contains the details of the TLS handshake between Envoy and the backend: + +```shell +< HTTP/1.1 200 OK +[...] + "tls": { + "version": "TLSv1.2", + "serverName": "www.example.com", + "negotiatedProtocol": "http/1.1", + "cipherSuite": "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" + } +``` + +{{% /tab %}} +{{% tab header="Without LoadBalancer Support" %}} + +Get the name of the Envoy service created the by the example Gateway: + +```shell +export ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}') +``` + +Port forward to the Envoy service: + +```shell +kubectl -n envoy-gateway-system port-forward service/${ENVOY_SERVICE} 80:80 & +``` + +Query the TLS-enabled backend through Envoy proxy: + +```shell +curl -v -HHost:www.example.com --resolve "www.example.com:80:127.0.0.1" \ +http://www.example.com:80/get +``` + +Inspect the output and see that the response contains the details of the TLS handshake between Envoy and the backend: + +```shell +< HTTP/1.1 200 OK +[...] + "tls": { + "version": "TLSv1.2", + "serverName": "www.example.com", + "negotiatedProtocol": "http/1.1", + "cipherSuite": "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" + } +``` + +{{% /tab %}} +{{< /tabpane >}} + +## Customize backend TLS Parameters + +In addition to enablement of backend TLS with the Gateway-API BackendTLSPolicy, Envoy Gateway supports customizing TLS parameters. +To achieve this, the [EnvoyProxy][] resource can be used to specify TLS parameters. We will customize the TLS version in this example. + +First, you need to add ParametersRef in GatewayClass, and refer to EnvoyProxy Config: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +You can customize the EnvoyProxy Backend TLS Parameters via EnvoyProxy Config like: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +## Testing TLS Parameters + +Query the TLS-enabled backend through Envoy proxy: + +```shell +curl -v -HHost:www.example.com --resolve "www.example.com:80:127.0.0.1" \ +http://www.example.com:80/get +``` + +Inspect the output and see that the response contains the details of the TLS handshake between Envoy and the backend. +The TLS version is now TLS1.3, as configured in the EnvoyProxy resource. The TLS cipher is also changed, since TLS1.3 supports different ciphers from TLS1.2. + +```shell +< HTTP/1.1 200 OK +[...] + "tls": { + "version": "TLSv1.3", + "serverName": "www.example.com", + "negotiatedProtocol": "http/1.1", + "cipherSuite": "TLS_AES_128_GCM_SHA256" + } +``` + +[Quickstart]: ../quickstart +[BackendTLSPolicy]: https://gateway-api.sigs.k8s.io/api-types/backendtlspolicy/ +[EnvoyProxy]: ../../api/extension_types#envoyproxy \ No newline at end of file diff --git a/site/content/en/docs/tasks/security/basic-auth.md b/site/content/en/docs/tasks/security/basic-auth.md new file mode 100644 index 00000000000..956963b6da5 --- /dev/null +++ b/site/content/en/docs/tasks/security/basic-auth.md @@ -0,0 +1,221 @@ +--- +title: "Basic Authentication" +--- + +This task provides instructions for configuring [HTTP Basic authentication][http Basic authentication]. +HTTP Basic authentication checks if an incoming request has a valid username and password before routing the request to +a backend service. + +Envoy Gateway introduces a new CRD called [SecurityPolicy][SecurityPolicy] that allows the user to configure HTTP Basic +authentication. +This instantiated resource can be linked to a [Gateway][Gateway], [HTTPRoute][HTTPRoute] or [GRPCRoute][GRPCRoute] resource. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## Configuration + +Envoy Gateway uses [.htpasswd][.htpasswd] format to store the username-password pairs for authentication. +The file must be stored in a kubernetes secret and referenced in the [SecurityPolicy][SecurityPolicy] configuration. +The secret is an Opaque secret, and the username-password pairs must be stored in the key ".htpasswd". + +### Create a root certificate + +Create a root certificate and private key to sign certificates: + +```shell +openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -subj '/O=example Inc./CN=example.com' -keyout example.com.key -out example.com.crt +``` + +### Create a certificate secret + +Create a certificate and a private key for `www.example.com`: + +```shell +openssl req -out www.example.com.csr -newkey rsa:2048 -nodes -keyout www.example.com.key -subj "/CN=www.example.com/O=example organization" +openssl x509 -req -days 365 -CA example.com.crt -CAkey example.com.key -set_serial 0 -in www.example.com.csr -out www.example.com.crt +``` + +### Create certificate + +```shell +kubectl create secret tls example-cert --key=www.example.com.key --cert=www.example.com.crt +``` + +### Enable HTTPS +Update the Gateway from the Quickstart to include an HTTPS listener that listens on port `443` and references the +`example-cert` Secret: + +```shell +kubectl patch gateway eg --type=json --patch ' + - op: add + path: /spec/listeners/- + value: + name: https + protocol: HTTPS + port: 443 + tls: + mode: Terminate + certificateRefs: + - kind: Secret + group: "" + name: example-cert + ' +``` + +### Create a .htpasswd file +First, create a [.htpasswd][.htpasswd] file with the username and password you want to use for authentication. + +Note: Please always use HTTPS with Basic Authentication. This prevents credentials from being transmitted in plain text. + +The input password won't be saved, instead, a hash will be generated and saved in the output file. When a request +tries to access protected resources, the password in the "Authorization" HTTP header will be hashed and compared with the +saved hash. + +Note: only SHA hash algorithm is supported for now. + +```shell +htpasswd -cbs .htpasswd foo bar +``` + +You can also add more users to the file: + +```shell +htpasswd -bs .htpasswd foo1 bar1 +``` + +### Create a basic-auth secret + + +Next, create a kubernetes secret with the generated .htpasswd file in the previous step. + +```shell +kubectl create secret generic basic-auth --from-file=.htpasswd +``` + +### Create a SecurityPolicy + +The below example defines a SecurityPolicy that authenticates requests against the user list in the kubernetes +secret generated in the previous step. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the SecurityPolicy configuration: + +```shell +kubectl get securitypolicy/basic-auth-example -o yaml +``` + +## Testing + +Ensure the `GATEWAY_HOST` environment variable from the [Quickstart](../../quickstart) is set. If not, follow the +Quickstart instructions to set the variable. + +```shell +echo $GATEWAY_HOST +``` + +Send a request to the backend service without `Authentication` header: + +```shell +curl -kv -H "Host: www.example.com" "https://${GATEWAY_HOST}/" +``` + +You should see `401 Unauthorized` in the response, indicating that the request is not allowed without authentication. + +```shell +* Connected to 127.0.0.1 (127.0.0.1) port 443 +... +* Server certificate: +* subject: CN=www.example.com; O=example organization +* issuer: O=example Inc.; CN=example.com +> GET / HTTP/2 +> Host: www.example.com +> User-Agent: curl/8.6.0 +> Accept: */* +... +< HTTP/2 401 +< content-length: 58 +< content-type: text/plain +< date: Wed, 06 Mar 2024 15:59:36 GMT +< + +* Connection #0 to host 127.0.0.1 left intact +User authentication failed. Missing username and password. +``` + +Send a request to the backend service with `Authentication` header: + +```shell +curl -kv -H "Host: www.example.com" -u 'foo:bar' "https://${GATEWAY_HOST}/" +``` + +The request should be allowed and you should see the response from the backend service. + +```shell + +## Clean-Up + +Follow the steps from the [Quickstart](../../quickstart) to uninstall Envoy Gateway and the example manifest. + +Delete the SecurityPolicy and the secret + +```shell +kubectl delete securitypolicy/basic-auth-example +kubectl delete secret/basic-auth +kubectl delete secret/example-cert +``` + +## Next Steps + +Checkout the [Developer Guide](../../../contributions/develop) to get involved in the project. + +[SecurityPolicy]: ../../../contributions/design/security-policy +[http Basic authentication]: https://tools.ietf.org/html/rfc2617 +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute +[GRPCRoute]: https://gateway-api.sigs.k8s.io/api-types/grpcroute +[.htpasswd]: https://httpd.apache.org/docs/current/programs/htpasswd.html diff --git a/site/content/en/docs/tasks/security/cors.md b/site/content/en/docs/tasks/security/cors.md new file mode 100644 index 00000000000..cfbe979cd22 --- /dev/null +++ b/site/content/en/docs/tasks/security/cors.md @@ -0,0 +1,177 @@ +--- +title: "CORS" +--- + +This task provides instructions for configuring [Cross-Origin Resource Sharing (CORS)][cors] on Envoy Gateway. +CORS defines a way for client web applications that are loaded in one domain to interact with resources in a different +domain. + +Envoy Gateway introduces a new CRD called [SecurityPolicy][SecurityPolicy] that allows the user to configure CORS. +This instantiated resource can be linked to a [Gateway][Gateway], [HTTPRoute][HTTPRoute] or [GRPCRoute][GRPCRoute] resource. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## Configuration + +When configuring CORS either an origin with a precise hostname can be configured or an hostname containing a wildcard prefix, +allowing all subdomains of the specified hostname. +In addition to that the entire origin (with or without specifying a scheme) can be a wildcard to allow all origins. + +The below example defines a SecurityPolicy that allows CORS for all HTTP requests originating from `www.foo.com`. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the SecurityPolicy configuration: + +```shell +kubectl get securitypolicy/cors-example -o yaml +``` + +## Testing + +Ensure the `GATEWAY_HOST` environment variable from the [Quickstart](../../quickstart) is set. If not, follow the +Quickstart instructions to set the variable. + +```shell +echo $GATEWAY_HOST +``` + +Verify that the CORS headers are present in the response of the OPTIONS request from `http://www.foo.com`: + +```shell +curl -H "Origin: http://www.foo.com" \ + -H "Host: www.example.com" \ + -H "Access-Control-Request-Method: GET" \ + -X OPTIONS -v -s \ + http://$GATEWAY_HOST \ + 1> /dev/null +``` + +You should see the below response, indicating that the request from `http://www.foo.com` is allowed: + +```shell +< access-control-allow-origin: http://www.foo.com +< access-control-allow-methods: GET, POST +< access-control-allow-headers: x-header-1, x-header-2 +< access-control-max-age: 86400 +< access-control-expose-headers: x-header-3, x-header-4 +``` + +If you try to send a request from `http://www.bar.com`, you should see the below response: + +```shell +curl -H "Origin: http://www.bar.com" \ + -H "Host: www.example.com" \ + -H "Access-Control-Request-Method: GET" \ + -X OPTIONS -v -s \ + http://$GATEWAY_HOST \ + 1> /dev/null +``` + +You won't see any CORS headers in the response, indicating that the request from `http://www.bar.com` was not allowed. + +If you try to send a request from `http://www.foo.com:8080`, you should also see similar response because the port number +`8080` is not included in the allowed origins. + +```shell +```shell +curl -H "Origin: http://www.foo.com:8080" \ + -H "Host: www.example.com" \ + -H "Access-Control-Request-Method: GET" \ + -X OPTIONS -v -s \ + http://$GATEWAY_HOST \ + 1> /dev/null +``` + +Note: +* CORS specification requires that the browsers to send a preflight request to the server to ask if it's allowed +to access the limited resource in another domains. The browsers are supposed to follow the response from the server to +determine whether to send the actual request or not. The CORS filter only response to the preflight requests according to +its configuration. It won't deny any requests. The browsers are responsible for enforcing the CORS policy. +* The targeted HTTPRoute or the HTTPRoutes that the targeted Gateway routes to must allow the OPTIONS method for the CORS +filter to work. Otherwise, the OPTIONS request won't match the routes and the CORS filter won't be invoked. + + +## Clean-Up + +Follow the steps from the [Quickstart](../../quickstart) to uninstall Envoy Gateway and the example manifest. + +Delete the SecurityPolicy: + +```shell +kubectl delete securitypolicy/cors-example +``` + +## Next Steps + +Checkout the [Developer Guide](../../../contributions/develop) to get involved in the project. + +[SecurityPolicy]: ../../../contributions/design/security-policy +[cors]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute +[GRPCRoute]: https://gateway-api.sigs.k8s.io/api-types/grpcroute diff --git a/site/content/en/docs/tasks/security/ext-auth.md b/site/content/en/docs/tasks/security/ext-auth.md new file mode 100644 index 00000000000..5fc73321106 --- /dev/null +++ b/site/content/en/docs/tasks/security/ext-auth.md @@ -0,0 +1,460 @@ +--- +title: "External Authorization" +--- + +This task provides instructions for configuring external authentication. + +External authorization calls an external HTTP or gRPC service to check whether an incoming HTTP request is authorized +or not. If the request is deemed unauthorized, then the request will be denied with a 403 (Forbidden) response. If the +request is authorized, then the request will be allowed to proceed to the backend service. + +Envoy Gateway introduces a new CRD called [SecurityPolicy][SecurityPolicy] that allows the user to configure external authorization. +This instantiated resource can be linked to a [Gateway][Gateway] and [HTTPRoute][HTTPRoute] resource. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +Verify the Gateway status: + +```shell +kubectl get gateway/eg -o yaml +``` + +## HTTP External Authorization Service + +### Installation + +Install a demo HTTP service that will be used as the external authorization service: + +```shell +kubectl apply -f https://raw.githubusercontent.com/envoyproxy/gateway/latest/examples/kubernetes/ext-auth-http-service.yaml +``` + +Create a new HTTPRoute resource to route traffic on the path `/myapp` to the backend service. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the HTTPRoute status: + +```shell +kubectl get httproute/myapp -o yaml +``` + +### Configuration + +Create a new SecurityPolicy resource to configure the external authorization. This SecurityPolicy targets the HTTPRoute +"myApp" created in the previous step. It calls the HTTP external authorization service "http-ext-auth" on port 9002 for +authorization. The `headersToBackend` field specifies the headers that will be sent to the backend service if the request +is successfully authorized. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the SecurityPolicy configuration: + +```shell +kubectl get securitypolicy/ext-auth-example -o yaml +``` + +### Testing + +Ensure the `GATEWAY_HOST` environment variable from the [Quickstart](../../quickstart) is set. If not, follow the +Quickstart instructions to set the variable. + +```shell +echo $GATEWAY_HOST +``` + +Send a request to the backend service without `Authentication` header: + +```shell +curl -v -H "Host: www.example.com" "http://${GATEWAY_HOST}/myapp" +``` + +You should see `403 Forbidden` in the response, indicating that the request is not allowed without authentication. + +```shell +* Connected to 172.18.255.200 (172.18.255.200) port 80 (#0) +> GET /myapp HTTP/1.1 +> Host: www.example.com +> User-Agent: curl/7.68.0 +> Accept: */* +... +< HTTP/1.1 403 Forbidden +< date: Mon, 11 Mar 2024 03:41:15 GMT +< x-envoy-upstream-service-time: 0 +< content-length: 0 +< +* Connection #0 to host 172.18.255.200 left intact +``` + +Send a request to the backend service with `Authentication` header: + +```shell +curl -v -H "Host: www.example.com" -H "Authorization: Bearer token1" "http://${GATEWAY_HOST}/myapp" +``` + +The request should be allowed and you should see the response from the backend service. +Because the `x-current-user` header from the auth response has been sent to the backend service, +you should see the `x-current-user` header in the response. + +``` +"X-Current-User": [ + "user1" + ], +``` + +## GRPC External Authorization Service + +### Installation + +Install a demo gRPC service that will be used as the external authorization service. The demo gRPC service is enabled +with TLS and a BackendTLSConfig is created to configure the communication between the Envoy proxy and the gRPC service. + +Note: TLS is optional for HTTP or gRPC external authorization services. However, enabling TLS is recommended for enhanced +security in production environments. + +```shell +kubectl apply -f https://raw.githubusercontent.com/envoyproxy/gateway/latest/examples/kubernetes/ext-auth-grpc-service.yaml +``` + +The HTTPRoute created in the previous section is still valid and can be used with the gRPC auth service, but if you have +not created the HTTPRoute, you can create it now. + +Create a new HTTPRoute resource to route traffic on the path `/myapp` to the backend service. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the HTTPRoute status: + +```shell +kubectl get httproute/myapp -o yaml +``` + +### Configuration + +Update the SecurityPolicy that was created in the previous section to use the gRPC external authorization service. +It calls the gRPC external authorization service "grpc-ext-auth" on port 9002 for authorization. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the SecurityPolicy configuration: + +```shell +kubectl get securitypolicy/ext-auth-example -o yaml +``` + +Because the gRPC external authorization service is enabled with TLS, a BackendTLSConfig needs to be created to configure +the communication between the Envoy proxy and the gRPC auth service. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the BackendTLSPolicy configuration: + +```shell +kubectl get backendtlspolicy/grpc-ext-auth-btls -o yaml +``` + +### Testing + +Ensure the `GATEWAY_HOST` environment variable from the [Quickstart](../../quickstart) is set. If not, follow the +Quickstart instructions to set the variable. + +```shell +echo $GATEWAY_HOST +``` + +Send a request to the backend service without `Authentication` header: + +```shell +curl -v -H "Host: www.example.com" "http://${GATEWAY_HOST}/myapp" +``` + +You should see `403 Forbidden` in the response, indicating that the request is not allowed without authentication. + +```shell +* Connected to 172.18.255.200 (172.18.255.200) port 80 (#0) +> GET /myapp HTTP/1.1 +> Host: www.example.com +> User-Agent: curl/7.68.0 +> Accept: */* +... +< HTTP/1.1 403 Forbidden +< date: Mon, 11 Mar 2024 03:41:15 GMT +< x-envoy-upstream-service-time: 0 +< content-length: 0 +< +* Connection #0 to host 172.18.255.200 left intact +``` + +Send a request to the backend service with `Authentication` header: + +```shell +curl -v -H "Host: www.example.com" -H "Authorization: Bearer token1" "http://${GATEWAY_HOST}/myapp" +``` + +## Clean-Up + +Follow the steps from the [Quickstart](../../quickstart) to uninstall Envoy Gateway and the example manifest. + +Delete the demo auth services, HTTPRoute, SecurityPolicy and BackendTLSPolicy: + +```shell +kubectl delete -f https://raw.githubusercontent.com/envoyproxy/gateway/latest/examples/kubernetes/ext-auth-http-service.yaml +kubectl delete -f https://raw.githubusercontent.com/envoyproxy/gateway/latest/examples/kubernetes/ext-auth-grpc-service.yaml +kubectl delete httproute/myapp +kubectl delete securitypolicy/ext-auth-example +kubectl delete backendtlspolicy/grpc-ext-auth-btls +``` + +## Next Steps + +Checkout the [Developer Guide](../../../contributions/develop) to get involved in the project. + +[SecurityPolicy]: ../../../contributions/design/security-policy +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute diff --git a/site/content/en/docs/tasks/security/jwt-authentication.md b/site/content/en/docs/tasks/security/jwt-authentication.md new file mode 100644 index 00000000000..8b160403882 --- /dev/null +++ b/site/content/en/docs/tasks/security/jwt-authentication.md @@ -0,0 +1,170 @@ +--- +title: "JWT Authentication" +--- + +This task provides instructions for configuring [JSON Web Token (JWT)][jwt] authentication. JWT authentication checks +if an incoming request has a valid JWT before routing the request to a backend service. Currently, Envoy Gateway only +supports validating a JWT from an HTTP header, e.g. `Authorization: Bearer `. + +Envoy Gateway introduces a new CRD called [SecurityPolicy][SecurityPolicy] that allows the user to configure JWT authentication. +This instantiated resource can be linked to a [Gateway][Gateway], [HTTPRoute][HTTPRoute] or [GRPCRoute][GRPCRoute] resource. + +## Prerequisites + +Follow the steps from the [Quickstart](../quickstart) to install Envoy Gateway and the example manifest. +For GRPC - follow the steps from the [GRPC Routing](../traffic/grpc-routing) example. +Before proceeding, you should be able to query the example backend using HTTP or GRPC. + +## Configuration + +Allow requests with a valid JWT by creating an [SecurityPolicy][SecurityPolicy] and attaching it to the example +HTTPRoute or GRPCRoute. + +### HTTPRoute + +```shell +kubectl apply -f https://raw.githubusercontent.com/envoyproxy/gateway/latest/examples/kubernetes/jwt/jwt.yaml +``` + +Two HTTPRoute has been created, one for `/foo` and another for `/bar`. A SecurityPolicy has been created and targeted +HTTPRoute foo to authenticate requests for `/foo`. The HTTPRoute bar is not targeted by the SecurityPolicy and will allow +unauthenticated requests to `/bar`. + +Verify the HTTPRoute configuration and status: + +```shell +kubectl get httproute/foo -o yaml +kubectl get httproute/bar -o yaml +``` + +The SecurityPolicy is configured for JWT authentication and uses a single [JSON Web Key Set (JWKS)][jwks] +provider for authenticating the JWT. + +Verify the SecurityPolicy configuration: + +```shell +kubectl get securitypolicy/jwt-example -o yaml +``` + +### GRPCRoute + +```shell +kubectl apply -f https://raw.githubusercontent.com/envoyproxy/gateway/latest/examples/kubernetes/jwt/grpc-jwt.yaml +``` + +A SecurityPolicy has been created and targeted GRPCRoute yages to authenticate all requests for `yages` service.. + +Verify the GRPCRoute configuration and status: + +```shell +kubectl get grpcroute/yages -o yaml +``` + +The SecurityPolicy is configured for JWT authentication and uses a single [JSON Web Key Set (JWKS)][jwks] +provider for authenticating the JWT. + +Verify the SecurityPolicy configuration: + +```shell +kubectl get securitypolicy/jwt-example -o yaml +``` + +## Testing + +Ensure the `GATEWAY_HOST` environment variable from the [Quickstart](../../quickstart) is set. If not, follow the +Quickstart instructions to set the variable. + +```shell +echo $GATEWAY_HOST +``` + +### HTTPRoute + +Verify that requests to `/foo` are denied without a JWT: + +```shell +curl -sS -o /dev/null -H "Host: www.example.com" -w "%{http_code}\n" http://$GATEWAY_HOST/foo +``` + +A `401` HTTP response code should be returned. + +Get the JWT used for testing request authentication: + +```shell +TOKEN=$(curl https://raw.githubusercontent.com/envoyproxy/gateway/main/examples/kubernetes/jwt/test.jwt -s) && echo "$TOKEN" | cut -d '.' -f2 - | base64 --decode - +``` + +__Note:__ The above command decodes and returns the token's payload. You can replace `f2` with `f1` to view the token's +header. + +Verify that a request to `/foo` with a valid JWT is allowed: + +```shell +curl -sS -o /dev/null -H "Host: www.example.com" -H "Authorization: Bearer $TOKEN" -w "%{http_code}\n" http://$GATEWAY_HOST/foo +``` + +A `200` HTTP response code should be returned. + +Verify that requests to `/bar` are allowed __without__ a JWT: + +```shell +curl -sS -o /dev/null -H "Host: www.example.com" -w "%{http_code}\n" http://$GATEWAY_HOST/bar +``` + +### GRPCRoute + +Verify that requests to `yages`service are denied without a JWT: + +```shell +grpcurl -plaintext -authority=grpc-example.com ${GATEWAY_HOST}:80 yages.Echo/Ping +``` + +You should see the below response + +```shell +Error invoking method "yages.Echo/Ping": rpc error: code = Unauthenticated desc = failed to query for service descriptor "yages.Echo": Jwt is missing +``` + +Get the JWT used for testing request authentication: + +```shell +TOKEN=$(curl https://raw.githubusercontent.com/envoyproxy/gateway/main/examples/kubernetes/jwt/test.jwt -s) && echo "$TOKEN" | cut -d '.' -f2 - | base64 --decode - +``` + +__Note:__ The above command decodes and returns the token's payload. You can replace `f2` with `f1` to view the token's +header. + +Verify that a request to `yages` service with a valid JWT is allowed: + +```shell +grpcurl -plaintext -H "authorization: Bearer $TOKEN" -authority=grpc-example.com ${GATEWAY_HOST}:80 yages.Echo/Ping +``` + +You should see the below response + +```shell +{ + "text": "pong" +} +``` + +## Clean-Up + +Follow the steps from the [Quickstart](../../quickstart) to uninstall Envoy Gateway and the example manifest. + +Delete the SecurityPolicy: + +```shell +kubectl delete securitypolicy/jwt-example +``` + +## Next Steps + +Checkout the [Developer Guide](../../../contributions/develop) to get involved in the project. + +[SecurityPolicy]: ../../../contributions/design/security-policy +[jwt]: https://tools.ietf.org/html/rfc7519 +[jwks]: https://tools.ietf.org/html/rfc7517 +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute +[GRPCRoute]: https://gateway-api.sigs.k8s.io/api-types/grpcroute diff --git a/site/content/en/docs/tasks/security/mutual-tls.md b/site/content/en/docs/tasks/security/mutual-tls.md new file mode 100644 index 00000000000..64f471ba19d --- /dev/null +++ b/site/content/en/docs/tasks/security/mutual-tls.md @@ -0,0 +1,186 @@ +--- +title: "Mutual TLS: External Clients to the Gateway" +--- + +This task demonstrates how mutual TLS can be achieved between external clients and the Gateway. +This task uses a self-signed CA, so it should be used for testing and demonstration purposes only. + +## Prerequisites + +- OpenSSL to generate TLS assets. + +## Installation + +Follow the steps from the [Quickstart](../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## TLS Certificates + +Generate the certificates and keys used by the Gateway to terminate client TLS connections. + +Create a root certificate and private key to sign certificates: + +```shell +openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -subj '/O=example Inc./CN=example.com' -keyout example.com.key -out example.com.crt +``` + +Create a certificate and a private key for `www.example.com`: + +```shell +openssl req -out www.example.com.csr -newkey rsa:2048 -nodes -keyout www.example.com.key -subj "/CN=www.example.com/O=example organization" +openssl x509 -req -days 365 -CA example.com.crt -CAkey example.com.key -set_serial 0 -in www.example.com.csr -out www.example.com.crt +``` + +Store the cert/key in a Secret: + +```shell +kubectl create secret tls example-cert --key=www.example.com.key --cert=www.example.com.crt --certificate-authority=example.com.crt +``` + +Store the CA Cert in another Secret: + +```shell +kubectl create secret generic example-ca-cert --from-file=ca.crt=example.com.crt +``` + +Create a certificate and a private key for the client `client.example.com`: + +```shell +openssl req -out client.example.com.csr -newkey rsa:2048 -nodes -keyout client.example.com.key -subj "/CN=client.example.com/O=example organization" +openssl x509 -req -days 365 -CA example.com.crt -CAkey example.com.key -set_serial 0 -in client.example.com.csr -out client.example.com.crt +``` + +Update the Gateway from the Quickstart to include an HTTPS listener that listens on port `443` and references the +`example-cert` Secret: + +```shell +kubectl patch gateway eg --type=json --patch ' + - op: add + path: /spec/listeners/- + value: + name: https + protocol: HTTPS + port: 443 + tls: + mode: Terminate + certificateRefs: + - kind: Secret + group: "" + name: example-cert + ' +``` + +Verify the Gateway status: + +```shell +kubectl get gateway/eg -o yaml +``` + +Create a [ClientTrafficPolicy][] to enforce client validation using the CA Certificate as a trusted anchor. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +## Testing + +{{< tabpane text=true >}} +{{% tab header="With External LoadBalancer Support" %}} + +Get the External IP of the Gateway: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +Query the example app through the Gateway: + +```shell +curl -v -HHost:www.example.com --resolve "www.example.com:443:${GATEWAY_HOST}" \ +--cert client.example.com.crt --key client.example.com.key \ +--cacert example.com.crt https://www.example.com/get +``` + +Don't specify the client key and certificate in the above command, and ensure that the connection fails: + +```shell +curl -v -HHost:www.example.com --resolve "www.example.com:443:${GATEWAY_HOST}" \ +--cacert example.com.crt https://www.example.com/get +``` + +{{% /tab %}} +{{% tab header="Without LoadBalancer Support" %}} + +Get the name of the Envoy service created the by the example Gateway: + +```shell +export ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}') +``` + +Port forward to the Envoy service: + +```shell +kubectl -n envoy-gateway-system port-forward service/${ENVOY_SERVICE} 8443:443 & +``` + +Query the example app through Envoy proxy: + +```shell +curl -v -HHost:www.example.com --resolve "www.example.com:8443:127.0.0.1" \ +--cert client.example.com.crt --key client.example.com.key \ +--cacert example.com.crt https://www.example.com:8443/get +``` + +{{% /tab %}} +{{< /tabpane >}} + +[ClientTrafficPolicy]: ../../../api/extension_types#clienttrafficpolicy diff --git a/site/content/en/docs/tasks/security/oidc.md b/site/content/en/docs/tasks/security/oidc.md new file mode 100644 index 00000000000..ac7d6d60ba9 --- /dev/null +++ b/site/content/en/docs/tasks/security/oidc.md @@ -0,0 +1,322 @@ +--- +title: "OIDC Authentication" +--- + +This task provides instructions for configuring [OpenID Connect (OIDC)][oidc] authentication. +OpenID Connect (OIDC) is an authentication standard built on top of OAuth 2.0. +It enables EG to rely on authentication that is performed by an OpenID Connect Provider (OP) +to verify the identity of a user. + +Envoy Gateway introduces a new CRD called [SecurityPolicy][SecurityPolicy] that allows the user to configure OIDC +authentication. +This instantiated resource can be linked to a [Gateway][Gateway] and [HTTPRoute][HTTPRoute] resource. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +EG OIDC authentication requires the redirect URL to be HTTPS. Follow the [Secure Gateways](../secure-gateways) guide +to generate the TLS certificates and update the Gateway configuration to add an HTTPS listener. + +Verify the Gateway status: + +```shell +kubectl get gateway/eg -o yaml +``` + +Let's create an HTTPRoute that represents an application protected by OIDC. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the HTTPRoute status: + +```shell +kubectl get httproute/myapp -o yaml +``` + +## OIDC Authentication for a HTTPRoute + +OIDC can be configured at the Gateway level to authenticate all the HTTPRoutes that are associated with the Gateway with +the same OIDC configuration, or at the HTTPRoute level to authenticate each HTTPRoute with different OIDC configurations. + +This section demonstrates how to configure OIDC authentication for a specific HTTPRoute. + +### Register an OIDC application + +This task uses Google as the OIDC provider to demonstrate the configuration of OIDC. However, EG works with any OIDC +providers, including Auth0, Azure AD, Keycloak, Okta, OneLogin, Salesforce, UAA, etc. + +Follow the steps in the [Google OIDC documentation][google-oidc] to register an OIDC application. Please make sure the +redirect URL is set to the one you configured in the SecurityPolicy that you will create in the step below. In this example, +the redirect URL is `http://www.example.com:8443/myapp/oauth2/callback`. + +After registering the application, you should have the following information: +* Client ID: The client ID of the OIDC application. +* Client Secret: The client secret of the OIDC application. + +### Create a kubernetes secret + +Next, create a kubernetes secret with the Client Secret created in the previous step. The secret is an Opaque secret, +and the Client Secret must be stored in the key "client-secret". + +Note: please replace the ${CLIENT_SECRET} with the actual Client Secret that you got from the previous step. + +```shell +kubectl create secret generic my-app-client-secret --from-literal=client-secret=${CLIENT_SECRET} +``` + +### Create a SecurityPolicy + +**Please notice that the `redirectURL` and `logoutPath` must match the target HTTPRoute.** In this example, the target +HTTPRoute is configured to match the host `www.example.com` and the path `/myapp`, so the `redirectURL` must be prefixed +with `https://www.example.com:8443/myapp`, and `logoutPath` must be prefixed with`/myapp`, otherwise the OIDC authentication +will fail because the redirect and logout requests will not match the target HTTPRoute and therefore can't be processed +by the OAuth2 filter on that HTTPRoute. + +Note: please replace the ${CLIENT_ID} in the below yaml snippet with the actual Client ID that you got from the OIDC provider. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the SecurityPolicy configuration: + +```shell +kubectl get securitypolicy/oidc-example -o yaml +``` + +### Testing + +Port forward gateway port to localhost: + +```shell +export ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}') + +kubectl -n envoy-gateway-system port-forward service/${ENVOY_SERVICE} 8443:443 +``` + +Put www.example.com in the /etc/hosts file in your test machine, so we can use this host name to access the gateway from a browser: + +```shell +... +127.0.0.1 www.example.com +``` + +Open a browser and navigate to the `https://www.example.com:8443/myapp` address. You should be redirected to the Google +login page. After you successfully login, you should see the response from the backend service. + +Clean the cookies in the browser and try to access `https://www.example.com:8443/foo` address. You should be able to see +this page since the path `/foo` is not protected by the OIDC policy. + +## OIDC Authentication for a Gateway + +OIDC can be configured at the Gateway level to authenticate all the HTTPRoutes that are associated with the Gateway with +the same OIDC configuration, or at the HTTPRoute level to authenticate each HTTPRoute with different OIDC configurations. + +This section demonstrates how to configure OIDC authentication for a Gateway. + +### Register an OIDC application + +If you haven't registered an OIDC application, follow the steps in the previous section to register an OIDC application. + +### Create a kubernetes secret + +If you haven't created a kubernetes secret, follow the steps in the previous section to create a kubernetes secret. + +### Create a SecurityPolicy + +Create or update the SecurityPolicy to target the Gateway instead of the HTTPRoute. **Please notice that the `redirectURL` +and `logoutPath` must match one of the HTTPRoutes associated with the Gateway.** In this example, the target Gateway has +two HTTPRoutes associated with it, one with the host `www.example.com` and the path `/myapp`, and the other with the host +`www.example.com` and the path `/`. Either one of the HTTPRoutes can be used to match the `redirectURL` and `logoutPath`. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the SecurityPolicy configuration: + +```shell +kubectl get securitypolicy/oidc-example -o yaml +``` + +### Testing + +If you haven't done so, follow the steps in the previous section to port forward gateway port to localhost and put +www.example.com in the /etc/hosts file in your test machine. + +Open a browser and navigate to the `https://www.example.com:8443/foo` address. You should be redirected to the Google +login page. After you successfully login, you should see the response from the backend service. + +You can also try to access `https://www.example.com:8443/myapp` address. You should be able to see this page since the +path `/myapp` is protected by the same OIDC policy. + +## Clean-Up + +Follow the steps from the [Quickstart](../../quickstart) to uninstall Envoy Gateway and the example manifest. + +Delete the SecurityPolicy, the secret and the HTTPRoute: + +```shell +kubectl delete securitypolicy/oidc-example +kubectl delete secret/my-app-client-secret +kubectl delete httproute/myapp +``` + +## Next Steps + +Checkout the [Developer Guide](../../../../contributions/develop) to get involved in the project. + +[oidc]: https://openid.net/connect/ +[google-oidc]: https://developers.google.com/identity/protocols/oauth2/openid-connect +[SecurityPolicy]: ../../../../contributions/design/security-policy +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute diff --git a/site/content/en/docs/tasks/security/private-key-provider.md b/site/content/en/docs/tasks/security/private-key-provider.md new file mode 100644 index 00000000000..cf40a96e9e1 --- /dev/null +++ b/site/content/en/docs/tasks/security/private-key-provider.md @@ -0,0 +1,621 @@ +--- +title: "Accelerating TLS Handshakes using Private Key Provider in Envoy" +--- + +TLS operations can be accelerated or the private key can be protected using specialized hardware. This can be leveraged in Envoy using [Envoy Private Key Provider](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/transport_sockets/tls/v3/common.proto#extensions-transport-sockets-tls-v3-privatekeyprovider) is added to Envoy. + +Today, there are two private key providers implemented in Envoy as contrib extensions: +- [QAT in Envoy 1.24 release](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/private_key_providers/qat/v3alpha/qat.proto#extensions-private-key-providers-qat-v3alpha-qatprivatekeymethodconfig) +- [CryptoMB in Envoy 1.20 release](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/private_key_providers/cryptomb/v3alpha/cryptomb.proto ) + +Both of them are used to accelerate the TLS handshake through the hardware capabilities. + +This task will walk you through the steps required to configure TLS Termination mode for TCP traffic while also using the Envoy Private Key Provider to accelerate the TLS handshake by leveraging QAT and the HW accelerator available on Intel SPR/EMR Xeon server platforms. + +## Prerequisites + +### For QAT + +- Install Linux kernel 5.17 or similar +- Ensure the node has QAT devices by checking the QAT physical function devices presented. [Supported Devices](https://intel.github.io/quickassist/qatlib/requirements.html#qat2-0-qatlib-supported-devices) + + ```shell + echo `(lspci -d 8086:4940 && lspci -d 8086:4941 && lspci -d 8086:4942 && lspci -d 8086:4943 && lspci -d 8086:4946 && lspci -d 8086:4947) | wc -l` supported devices found. + ``` + +- Enable IOMMU from BIOS +- Enable IOMMU for Linux kernel + + Figure out the QAT VF device id + + ```shell + lspci -d 8086:4941 && lspci -d 8086:4943 && lspci -d 8086:4947 + ``` + + Attach the QAT device to vfio-pci through kernel parameter by the device id gotten from previous command. + + ```shell + cat /etc/default/grub: + GRUB_CMDLINE_LINUX="intel_iommu=on vfio-pci.ids=[QAT device id]" + update-grub + reboot + ```` + + Once the system is rebooted, check if the IOMMU has been enabled via the following command: + + ```shell + dmesg| grep IOMMU + [ 1.528237] DMAR: IOMMU enabled + ``` + +- Enable virtual function devices for QAT device + + ```shell + modprobe vfio_pci + rmmod qat_4xxx + modprobe qat_4xxx + qat_device=$(lspci -D -d :[QAT device id] | awk '{print $1}') + for i in $qat_device; do echo 16|sudo tee /sys/bus/pci/devices/$i/sriov_numvfs; done + chmod a+rw /dev/vfio/* + ``` + +- Increase the container runtime memory lock limit (using the containerd as example here) + + ```shell + mkdir /etc/systemd/system/containerd.service.d + cat <>/etc/systemd/system/containerd.service.d/memlock.conf + [Service] + LimitMEMLOCK=134217728 + EOF + ``` + + Restart the container runtime (for containerd, CRIO has similar concept) + + ```shell + systemctl daemon-reload + systemctl restart containerd + ``` + +- Install [Intel® QAT Device Plugin for Kubernetes](https://github.com/intel/intel-device-plugins-for-kubernetes) + + ```shell + kubectl apply -k 'https://github.com/intel/intel-device-plugins-for-kubernetes/deployments/qat_plugin?ref=main' + ``` + + Verification of the plugin deployment and detection of QAT hardware can be confirmed by examining the resource allocations on the nodes: + + ```shell + kubectl get node -o yaml| grep qat.intel.com + ``` + +### For CryptoMB: + +It required the node with 3rd generation Intel Xeon Scalable processor server processors, or later. +- For kubernetes Cluster, if not all nodes that support Intel® AVX-512 in Kubernetes cluster, you need to add some labels to divide these two kinds of nodes manually or using [NFD](https://github.com/kubernetes-sigs/node-feature-discovery). + + ```shell + kubectl apply -k https://github.com/kubernetes-sigs/node-feature-discovery/deployment/overlays/default?ref=v0.15.1 + ``` + +- Checking the available nodes with required cpu instructions: + - Check the node labels if using [NFD](https://github.com/kubernetes-sigs/node-feature-discovery): + + ```shell + kubectl get nodes -l feature.node.kubernetes.io/cpu-cpuid.AVX512F,feature.node.kubernetes.io/cpu-cpuid.AVX512DQ,feature.node.kubernetes.io/cpu-cpuid.AVX512BW,feature.node.kubernetes.io/cpu-cpuid.AVX512VBMI2,feature.node.kubernetes.io/cpu-cpuid.AVX512IFMA + ``` + + - Check CPUIDS manually on the node if without using NFD: + + ```shell + cat /proc/cpuinfo |grep avx512f|grep avx512dq|grep avx512bw|grep avx512_vbmi2|grep avx512ifma + ``` + +## Installation + +* Follow the steps from the [Quickstart](../quickstart) to install Envoy Gateway. + +* Enable the EnvoyPatchPolicy feature, which will allow us to directly configure the Private Key Provider Envoy Filter, since Envoy Gateway does not directly expose this functionality. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +* After updating the `ConfigMap`, you will need to restart the `envoy-gateway` deployment so the configuration kicks in + + ```shell + kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system + ``` + +## Create gateway for TLS termination + +* Follow the instructions in [TLS Termination for TCP](./tls-termination) to setup a TCP gateway to terminate the TLS connection. + +* Update GatewayClass for using the envoyproxy image with contrib extensions and requests required resources. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +### Change EnvoyProxy configuration for QAT + +Using the envoyproxy image with contrib extensions and add qat resources requesting, ensure the k8s scheduler find out a machine with required resource. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +### Change EnvoyProxy configuration for CryptoMB + +Using the envoyproxy image with contrib extensions and add the node affinity to scheduling the Envoy Gateway pod on the machine with required CPU instructions. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Or using `preferredDuringSchedulingIgnoredDuringExecution` for best effort scheduling, or not doing any node affinity, just doing the random scheduling. The CryptoMB private key provider supports software fallback if the required CPU instructions aren't here. + +## Apply EnvoyPatchPolicy to enable private key provider + +### Benchmark before enabling private key provider + +First follow the instructions in [TLS Termination for TCP](./tls-termination) to do the functionality test. + +Ensure the cpu frequency governor set as `performance`. + +```shell +export NUM_CPUS=`lscpu | grep "^CPU(s):"|awk '{print $2}'` +for i in `seq 0 1 $NUM_CPUS`; do sudo cpufreq-set -c $i -g performance; done +``` + +Using the nodeport as the example, fetch the node port from envoy gateway service. + +```shell +export ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}') +export NODE_PORT=$(kubectl -n envoy-gateway-system get svc/$ENVOY_SERVICE -o jsonpath='{.spec.ports[0].nodePort}') +``` + +```shell +echo "127.0.0.1 www.example.com" >> /etc/hosts +``` + +Benchmark the gateway with fortio. + +```shell +fortio load -c 10 -k -qps 0 -t 30s -keepalive=false https://www.example.com:${NODE_PORT} +``` + +### For QAT + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +### For CryptoMB + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +### Benchmark after enabling private key provider + +First follow the instructions in [TLS Termination for TCP](./tls-termination) to do the functionality test again. + +Benchmark the gateway with fortio. + +```shell +fortio load -c 64 -k -qps 0 -t 30s -keepalive=false https://www.example.com:${NODE_PORT} +``` + +You will see a performance boost after private key provider enabled. For example, you will get results as below. + +Without private key provider: + +```shell +All done 43069 calls (plus 10 warmup) 6.966 ms avg, 1435.4 qps +``` + +With CryptoMB private key provider, the QPS is over 2 times than without private key provider. + +```shell +All done 93983 calls (plus 128 warmup) 40.880 ms avg, 3130.5 qps +``` + +With QAT private key provider, the QPS is over 3 times than without private key provider + +```shell +All done 134746 calls (plus 128 warmup) 28.505 ms avg, 4489.6 qps +``` diff --git a/site/content/en/docs/tasks/security/restrict-ip-access.md b/site/content/en/docs/tasks/security/restrict-ip-access.md new file mode 100644 index 00000000000..ba6af118252 --- /dev/null +++ b/site/content/en/docs/tasks/security/restrict-ip-access.md @@ -0,0 +1,197 @@ +--- +title: "IP Allowlist/Denylist" +--- + +This task provides instructions for configuring IP allowlist/denylist on Envoy Gateway. IP allowlist/denylist +checks if an incoming request is from an allowed IP address before routing the request to a backend service. + +Envoy Gateway introduces a new CRD called [SecurityPolicy][SecurityPolicy] that allows the user to configure IP allowlist/denylist. +This instantiated resource can be linked to a [Gateway][Gateway], [HTTPRoute][HTTPRoute] or [GRPCRoute][GRPCRoute] resource. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## Configuration + +### Create a SecurityPolicy + +The below SecurityPolicy restricts access to the backend service by allowing requests only from the IP addresses `10.0.1.0/24`. + +In this example, the default action is set to `Deny`, which means that only requests from the specified IP addresses with `Allow` +action are allowed, and all other requests are denied. You can also change the default action to `Allow` to allow all requests +except those from the specified IP addresses with `Deny` action. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the SecurityPolicy configuration: + +```shell +kubectl get securitypolicy/authorization-client-ip -o yaml +``` + +### Original Source IP + +It's important to note that the IP address used for allowlist/denylist is the original source IP address of the request. +You can use a [ClientTrafficPolicy] to configure how Envoy Gateway should determine the original source IP address. + +For example, the below ClientTrafficPolicy configures Envoy Gateway to use the `X-Forwarded-For` header to determine the original source IP address. +The `numTrustedHops` field specifies the number of trusted hops in the `X-Forwarded-For` header. In this example, the `numTrustedHops` is set to `1`, +which means that the first rightmost IP address in the `X-Forwarded-For` header is used as the original source IP address. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + + +## Testing + +Ensure the `GATEWAY_HOST` environment variable from the [Quickstart](../../quickstart) is set. If not, follow the +Quickstart instructions to set the variable. + +```shell +echo $GATEWAY_HOST +``` + +Send a request to the backend service without the `X-Forwarded-For` header: + +```shell +curl -v -H "Host: www.example.com" "http://${GATEWAY_HOST}/" +``` + +You should see `403 Forbidden` in the response, indicating that the request is not allowed. + +```shell +* Connected to 172.18.255.200 (172.18.255.200) port 80 +> GET /get HTTP/1.1 +> Host: www.example.com +> User-Agent: curl/8.8.0-DEV +> Accept: */* +> +* Request completely sent off +< HTTP/1.1 403 Forbidden +< content-length: 19 +< content-type: text/plain +< date: Mon, 08 Jul 2024 04:23:31 GMT +< +* Connection #0 to host 172.18.255.200 left intact +RBAC: access denied +``` + +Send a request to the backend service with the `X-Forwarded-For` header: + +```shell +curl -v -H "Host: www.example.com" -H "X-Forwarded-For: 10.0.1.1" "http://${GATEWAY_HOST}/" +``` + +The request should be allowed and you should see the response from the backend service. + +## Clean-Up + +Follow the steps from the [Quickstart](../../quickstart) to uninstall Envoy Gateway and the example manifest. + +Delete the SecurityPolicy and the ClientTrafficPolicy + +```shell +kubectl delete securitypolicy/authorization-client-ip +kubectl delete clientTrafficPolicy/enable-client-ip-detection +``` + +## Next Steps + +Checkout the [Developer Guide](../../../contributions/develop) to get involved in the project. + +[SecurityPolicy]: ../../../contributions/design/security-policy +[ClientTrafficPolicy]: ../../../api/extension_types#clienttrafficpolicy +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute +[GRPCRoute]: https://gateway-api.sigs.k8s.io/api-types/grpcroute diff --git a/site/content/en/docs/tasks/security/secure-gateways.md b/site/content/en/docs/tasks/security/secure-gateways.md new file mode 100644 index 00000000000..af5e922412d --- /dev/null +++ b/site/content/en/docs/tasks/security/secure-gateways.md @@ -0,0 +1,520 @@ +--- +title: "Secure Gateways" +--- + +This task will help you get started using secure Gateways. +This task uses a self-signed CA, so it should be used for testing and demonstration purposes only. + +## Prerequisites + +- OpenSSL to generate TLS assets. + +## Installation + +Follow the steps from the [Quickstart](../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## TLS Certificates + +Generate the certificates and keys used by the Gateway to terminate client TLS connections. + +Create a root certificate and private key to sign certificates: + +```shell +openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -subj '/O=example Inc./CN=example.com' -keyout example.com.key -out example.com.crt +``` + +Create a certificate and a private key for `www.example.com`: + +```shell +openssl req -out www.example.com.csr -newkey rsa:2048 -nodes -keyout www.example.com.key -subj "/CN=www.example.com/O=example organization" +openssl x509 -req -days 365 -CA example.com.crt -CAkey example.com.key -set_serial 0 -in www.example.com.csr -out www.example.com.crt +``` + +Store the cert/key in a Secret: + +```shell +kubectl create secret tls example-cert --key=www.example.com.key --cert=www.example.com.crt +``` + +Update the Gateway from the Quickstart to include an HTTPS listener that listens on port `443` and references the +`example-cert` Secret: + +```shell +kubectl patch gateway eg --type=json --patch ' + - op: add + path: /spec/listeners/- + value: + name: https + protocol: HTTPS + port: 443 + tls: + mode: Terminate + certificateRefs: + - kind: Secret + group: "" + name: example-cert + ' +``` + +Verify the Gateway status: + +```shell +kubectl get gateway/eg -o yaml +``` + +## Testing + +{{< tabpane text=true >}} +{{% tab header="With External LoadBalancer Support" %}} + +Get the External IP of the Gateway: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +Query the example app through the Gateway: + +```shell +curl -v -HHost:www.example.com --resolve "www.example.com:443:${GATEWAY_HOST}" \ +--cacert example.com.crt https://www.example.com/get +``` + +{{% /tab %}} +{{% tab header="Without LoadBalancer Support" %}} + +Get the name of the Envoy service created the by the example Gateway: + +```shell +export ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}') +``` + +Port forward to the Envoy service: + +```shell +kubectl -n envoy-gateway-system port-forward service/${ENVOY_SERVICE} 8443:443 & +``` + +Query the example app through Envoy proxy: + +```shell +curl -v -HHost:www.example.com --resolve "www.example.com:8443:127.0.0.1" \ +--cacert example.com.crt https://www.example.com:8443/get +``` + +{{% /tab %}} +{{< /tabpane >}} + + +## Multiple HTTPS Listeners + +Create a TLS cert/key for the additional HTTPS listener: + +```shell +openssl req -out foo.example.com.csr -newkey rsa:2048 -nodes -keyout foo.example.com.key -subj "/CN=foo.example.com/O=example organization" +openssl x509 -req -days 365 -CA example.com.crt -CAkey example.com.key -set_serial 0 -in foo.example.com.csr -out foo.example.com.crt +``` + +Store the cert/key in a Secret: + +```shell +kubectl create secret tls foo-cert --key=foo.example.com.key --cert=foo.example.com.crt +``` + +Create another HTTPS listener on the example Gateway: + +```shell +kubectl patch gateway eg --type=json --patch ' + - op: add + path: /spec/listeners/- + value: + name: https-foo + protocol: HTTPS + port: 443 + hostname: foo.example.com + tls: + mode: Terminate + certificateRefs: + - kind: Secret + group: "" + name: foo-cert + ' +``` + +Update the HTTPRoute to route traffic for hostname `foo.example.com` to the example backend service: + +```shell +kubectl patch httproute backend --type=json --patch ' + - op: add + path: /spec/hostnames/- + value: foo.example.com + ' +``` + +Verify the Gateway status: + +```shell +kubectl get gateway/eg -o yaml +``` + +Follow the steps in the [Testing section](#testing) to test connectivity to the backend app through both Gateway +listeners. Replace `www.example.com` with `foo.example.com` to test the new HTTPS listener. + +## Cross Namespace Certificate References + +A Gateway can be configured to reference a certificate in a different namespace. This is allowed by a [ReferenceGrant][] +created in the target namespace. Without the ReferenceGrant, a cross-namespace reference is invalid. + +Before proceeding, ensure you can query the HTTPS backend service from the [Testing section](#testing). + +To demonstrate cross namespace certificate references, create a ReferenceGrant that allows Gateways from the "default" +namespace to reference Secrets in the "envoy-gateway-system" namespace: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Delete the previously created Secret: + +```shell +kubectl delete secret/example-cert +``` + +The Gateway HTTPS listener should now surface the `Ready: False` status condition and the example HTTPS backend should +no longer be reachable through the Gateway. + +```shell +kubectl get gateway/eg -o yaml +``` + +Recreate the example Secret in the `envoy-gateway-system` namespace: + +```shell +kubectl create secret tls example-cert -n envoy-gateway-system --key=www.example.com.key --cert=www.example.com.crt +``` + +Update the Gateway HTTPS listener with `namespace: envoy-gateway-system`, for example: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The Gateway HTTPS listener status should now surface the `Ready: True` condition and you should once again be able to +query the HTTPS backend through the Gateway. + +Lastly, test connectivity using the above [Testing section](#testing). + +## Clean-Up + +Follow the steps from the [Quickstart](../quickstart) to uninstall Envoy Gateway and the example manifest. + +Delete the Secrets: + +```shell +kubectl delete secret/example-cert +kubectl delete secret/foo-cert +``` + +# RSA + ECDSA Dual stack certificates + +This section gives a walkthrough to generate RSA and ECDSA derived certificates and keys for the Server, which can then be configured in the Gateway listener, to terminate TLS traffic. + +## Prerequisites + +Follow the steps from the [Quickstart](../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +Follow the steps in the [TLS Certificates](#tls-certificates) section to generate self-signed RSA derived Server certificate and private key, and configure those in the Gateway listener configuration to terminate HTTPS traffic. + +## Pre-checks + +While testing in [Cluster without External LoadBalancer Support](#clusters-without-external-loadbalancer-support), we can query the example app through Envoy proxy while enforcing an RSA cipher, as shown below: + +```shell +curl -v -HHost:www.example.com --resolve "www.example.com:8443:127.0.0.1" \ +--cacert example.com.crt https://www.example.com:8443/get -Isv --ciphers ECDHE-RSA-CHACHA20-POLY1305 --tlsv1.2 --tls-max 1.2 +``` + +Since the Secret configured at this point is an RSA based Secret, if we enforce the usage of an ECDSA cipher, the call should fail as follows + +```shell +$ curl -v -HHost:www.example.com --resolve "www.example.com:8443:127.0.0.1" \ +--cacert example.com.crt https://www.example.com:8443/get -Isv --ciphers ECDHE-ECDSA-CHACHA20-POLY1305 --tlsv1.2 --tls-max 1.2 + +* Added www.example.com:8443:127.0.0.1 to DNS cache +* Hostname www.example.com was found in DNS cache +* Trying 127.0.0.1:8443... +* Connected to www.example.com (127.0.0.1) port 8443 (#0) +* ALPN: offers h2 +* ALPN: offers http/1.1 +* Cipher selection: ECDHE-ECDSA-CHACHA20-POLY1305 +* CAfile: example.com.crt +* CApath: none +* (304) (OUT), TLS handshake, Client hello (1): +* error:1404B410:SSL routines:ST_CONNECT:sslv3 alert handshake failure +* Closing connection 0 +``` + +Moving forward in the doc, we will be configuring the existing Gateway listener to accept both kinds of ciphers. + +## TLS Certificates + +Reuse the CA certificate and key pair generated in the [Secure Gateways](#tls-certificates) task and use this CA to sign both RSA and ECDSA Server certificates. +Note the CA certificate and key names are `example.com.crt` and `example.com.key` respectively. + + +Create an ECDSA certificate and a private key for `www.example.com`: + +```shell +openssl ecparam -noout -genkey -name prime256v1 -out www.example.com.ecdsa.key +openssl req -new -SHA384 -key www.example.com.ecdsa.key -nodes -out www.example.com.ecdsa.csr -subj "/CN=www.example.com/O=example organization" +openssl x509 -req -SHA384 -days 365 -in www.example.com.ecdsa.csr -CA example.com.crt -CAkey example.com.key -CAcreateserial -out www.example.com.ecdsa.crt +``` + +Store the cert/key in a Secret: + +```shell +kubectl create secret tls example-cert-ecdsa --key=www.example.com.ecdsa.key --cert=www.example.com.ecdsa.crt +``` + +Patch the Gateway with this additional ECDSA Secret: + +```shell +kubectl patch gateway eg --type=json --patch ' + - op: add + path: /spec/listeners/1/tls/certificateRefs/- + value: + name: example-cert-ecdsa + ' +``` + +Verify the Gateway status: + +```shell +kubectl get gateway/eg -o yaml +``` + +## Testing + +Again, while testing in Cluster without External LoadBalancer Support, we can query the example app through Envoy proxy while enforcing an RSA cipher, which should work as it did before: + +```shell +curl -v -HHost:www.example.com --resolve "www.example.com:8443:127.0.0.1" \ +--cacert example.com.crt https://www.example.com:8443/get -Isv --ciphers ECDHE-RSA-CHACHA20-POLY1305 --tlsv1.2 --tls-max 1.2 +``` + +```shell +... +* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1): +* TLSv1.2 (IN), TLS handshake, Finished (20): +* SSL connection using TLSv1.2 / ECDHE-RSA-CHACHA20-POLY1305 +... +``` + +Additionally, querying the example app while enforcing an ECDSA cipher should also work now: + +```shell +curl -v -HHost:www.example.com --resolve "www.example.com:8443:127.0.0.1" \ +--cacert example.com.crt https://www.example.com:8443/get -Isv --ciphers ECDHE-ECDSA-CHACHA20-POLY1305 --tlsv1.2 --tls-max 1.2 +``` + +```shell +... +* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1): +* TLSv1.2 (IN), TLS handshake, Finished (20): +* SSL connection using TLSv1.2 / ECDHE-ECDSA-CHACHA20-POLY1305 +... +``` + +# SNI based Certificate selection + +This sections gives a walkthrough to generate multiple certificates corresponding to different FQDNs. The same Gateway listener can then be configured to terminate TLS traffic for multiple FQDNs based on the SNI matching. + +## Prerequisites + +Follow the steps from the [Quickstart](../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +Follow the steps in the [TLS Certificates](#tls-certificates) section to generate self-signed RSA derived Server certificate and private key, and configure those in the Gateway listener configuration to terminate HTTPS traffic. + +## Additional Configurations + +Using the [TLS Certificates](#tls-certificates) section, we first generate additional Secret for another Host `www.sample.com`. + +```shell +openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -subj '/O=sample Inc./CN=sample.com' -keyout sample.com.key -out sample.com.crt + +openssl req -out www.sample.com.csr -newkey rsa:2048 -nodes -keyout www.sample.com.key -subj "/CN=www.sample.com/O=sample organization" +openssl x509 -req -days 365 -CA sample.com.crt -CAkey sample.com.key -set_serial 0 -in www.sample.com.csr -out www.sample.com.crt + +kubectl create secret tls sample-cert --key=www.sample.com.key --cert=www.sample.com.crt +``` + +Note that all occurrences of `example.com` were just replaced with `sample.com` + + +Next we update the `Gateway` configuration to accommodate the new Certificate which will be used to Terminate TLS traffic: + +```shell +kubectl patch gateway eg --type=json --patch ' + - op: add + path: /spec/listeners/1/tls/certificateRefs/- + value: + name: sample-cert + ' +``` + +Finally, we update the HTTPRoute to route traffic for hostname `www.sample.com` to the example backend service: + +```shell +kubectl patch httproute backend --type=json --patch ' + - op: add + path: /spec/hostnames/- + value: www.sample.com + ' +``` + +## Testing + +{{< tabpane text=true >}} +{{% tab header="With External LoadBalancer Support" %}} + +Refer to the steps mentioned earlier under [Testing in clusters with External LoadBalancer Support](#testing) + + +{{% /tab %}} +{{% tab header="Without LoadBalancer Support" %}} + +Get the name of the Envoy service created the by the example Gateway: + +```shell +export ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}') +``` + +Port forward to the Envoy service: + +```shell +kubectl -n envoy-gateway-system port-forward service/${ENVOY_SERVICE} 8443:443 & +``` + +Query the example app through Envoy proxy: + +```shell +curl -v -HHost:www.example.com --resolve "www.example.com:8443:127.0.0.1" \ +--cacert example.com.crt https://www.example.com:8443/get -I +``` + +Similarly, query the sample app through the same Envoy proxy: + +```shell +curl -v -HHost:www.sample.com --resolve "www.sample.com:8443:127.0.0.1" \ +--cacert sample.com.crt https://www.sample.com:8443/get -I +``` + +Since the multiple certificates are configured on the same Gateway listener, Envoy was able to provide the client with appropriate certificate based on the SNI in the client request. + +{{% /tab %}} +{{< /tabpane >}} + +## Next Steps + +Checkout the [Developer Guide](../../../contributions/develop) to get involved in the project. + +[ReferenceGrant]: https://gateway-api.sigs.k8s.io/api-types/referencegrant/ diff --git a/site/content/en/docs/tasks/security/threat-model.md b/site/content/en/docs/tasks/security/threat-model.md new file mode 100644 index 00000000000..a16319f9d72 --- /dev/null +++ b/site/content/en/docs/tasks/security/threat-model.md @@ -0,0 +1,665 @@ +--- +title: "Threat Model" +--- + +# Envoy Gateway Threat Model and End User Recommendations + +## About + +This work was performed by [ControlPlane](https://control-plane.io/) and commissioned by the [Linux Foundation](https://www.linuxfoundation.org/). ControlPlane is a global cloud native and open source cybersecurity consultancy, trusted as the partner of choice in securing: multinational banks; major public clouds; international financial institutions; critical national infrastructure programs; multinational oil and gas companies, healthcare and insurance providers; and global media firms. + +## Threat Modelling Team + +James Callaghan, Torin van den Bulk, Eduardo Olarte + +## Reviewers + +Arko Dasgupta, Matt Turner, Zack Butcher, Marco De Benedictis + +## Introduction + +As we embrace the proliferation of microservice-based architectures in the cloud-native landscape, simplicity in setup and configuration becomes paramount as DevOps teams face the challenge of choosing between numerous similar technologies. One such choice which every team deploying to Kubernetes faces is what to use as an ingress controller. With a plethora of options available, and the existence of vendor-specific annotations leading to small inconsistencies between implementations, the [Gateway API](https://gateway-api.sigs.k8s.io/) project was introduced by the SIG-NETWORK community, with the goal of eventually replacing the Ingress resource. + +Envoy Gateway is configured by Gateway API resources, and serves as an intuitive and feature-rich wrapper over the widely acclaimed Envoy Proxy. With a convenient setup based on Kubernetes (K8s) manifests, Envoy Gateway streamlines the management of Envoy Proxy instances in an edge-proxy setting, reducing the operational overhead of managing low-level Envoy configurations. Envoy Gateway benefits cloud-native DevOps teams through its role-oriented configuration, providing granular control based on Role-Based Access Control (RBAC) principles. These features form the basis of our exploration into Envoy Gateway and the rich feature set it brings to the table. + +In this threat model, we aim to provide an analysis of Envoy Gateway's design components and their capabilities (at version 1.0) through a threat-driven approach. It should be noted that this does not constitute a security audit of the Envoy Gateway project, but instead focuses on different possible deployment topologies for Envoy Gateway with the goal of deriving recommendations and best practice guidance for end users. + +The Envoy Gateway project recommends a [multi-tenancy model](../operations/deployment-mode#multi-tenancy) whereby each tenant deploys their own Envoy Gateway controller in a namespace which they own. We will also explore the implications and risks associated with multiple tenants using a shared controller. + +### Scope + +The primary focus of this threat model is to identify and assess security risks associated with deploying and operating Envoy Gateway within a multi-tenant Kubernetes (K8s) cluster. This model aims to provide a comprehensive understanding of the system, its transmission points, and potential vulnerabilities to enumerated threats. + +### In Scope + +**Envoy Gateway**: As the primary focus of this threat model, all aspects of Envoy Gateway, including its configuration, deployment, and operation will be analysed. This includes how the gateway manages TLS certificates, authentication, service-to-service traffic routing, and more. + +**Kubernetes Cluster**: Configuration and operation of the underlying Kubernetes cluster, including how it manages network policies, access control, and resource isolation for different namespaces/tenants in relation to Envoy will be considered. + +**Tenant Workloads**: Tenant workloads (and the pods they run on) will be considered, focusing on how they interact with the Envoy Gateway and potential vulnerabilities that could be exploited. + +#### Out of Scope + +This threat model will not consider security risks associated with the underlying infrastructure (e.g., EC2 compute instances and S3 buckets) or non-Envoy related components within the Kubernetes Cluster. It will focus solely on the Envoy Gateway and its interaction with the Kubernetes cluster and tenant workloads. + +Implementation of Envoy Gateway as an egress traffic controller is out of scope for this threat model and will not be considered in the report's findings. + +### Related Resources + +[Introducing Envoy Gateway](https://blog.envoyproxy.io/introducing-envoy-gateway-ad385cc59532) + +[Envoy Proxy Threat Model](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/security/threat_model#threat-model) + +[Configuring Envoy as an Edge Proxy](https://www.envoyproxy.io/docs/envoy/latest/configuration/best_practices/edge#best-practices-edge) + +[Envoy Gateway Deployment Mode](../operations/deployment-mode) + +[Kubernetes Gateway API Security Model](https://gateway-api.sigs.k8s.io/concepts/security-model/) + +## Architecture Overview + +### Summary + +To provide an in-depth look into both the system design and end-user deployment of Envoy Gateway, we will be focusing on the [Deployment Architecture Diagram](#deployment-architecture-diagram) below. + +The Deployment Architecture Diagram provides a high-level model of an end-user deployment of Envoy Gateway. For simplicity, we will look at different deployment topologies on a single multi-tenant Kubernetes cluster. Envoy Gateway operates as an edge proxy within this environment, handling the traffic flow between external interfaces and services within the cluster. The example will use two Envoy Gateway controllers - one dedicated controller for a single tenant, and one shared controller for two other tenants. Each Envoy Gateway controller will accept a single GatewayClass resource. + +### Deployment Architecture Diagram + +As Envoy Gateway implements the [Kubernetes GatewayAPI](https://gateway-api.sigs.k8s.io/concepts/api-overview/), this threat model will focus on the key objects in the Gateway API resource model: + +1. **GatewayClass:** defines a set of gateways with a commonconfiguration and behaviour. It is a cluster scoped resource. + +2. **Gateway:** requests a point where traffic can be translated to Services within the cluster. + +3. **Routes:** describe how traffic coming via the Gateway maps to theServices. + +At the time of writing, Envoy Gateway only supports a Kubernetes provider. As such, we will consider a reference architecture where multiple teams are working on the same Kubernetes cluster within different namespaces (Tenant A, B, & C). We will assume that some teams have similar security and performance needs, and a decision has been made to use a shared Gateway. However, we will also consider the case that some teams require dedicated Gateways, perhaps for compliance reasons or requirements driven by an internal threat model. + +We will consider the following organisational roles, as per the [Gateway API security model](https://gateway-api.sigs.k8s.io/concepts/security-model/): + +1. **Infrastructure provider**: The infrastructure provider (infra) is responsible for the overall environment that the cluster(s) are operating in. Examples include: the cloud provider (AWS, Azure, GCP, ...) or the PaaS provider in a company. + +2. **Cluster operator**: The cluster operator (ops) is responsible for administration of entire clusters. They manage policies, network access, application permissions. + +3. **Application developer**: The application developer (dev) is responsible for defining their application configuration (e.g. timeouts, request matching/filter) and Service composition (e.g. path routing to backends). + +4. **Application admin**: The application admin has administrative access to some namespaces within a cluster, but not the cluster as a whole. + +Our threat model will be based on the high-level setup shown below, where Envoy is used in an edge-proxy scenario: + +![Architecture](/img/architecture_threat_model.png) + +The following use cases will be considered, in line with the [Envoy Gateway tasks](../quickstart): + +1. Routing and controlling traffic, including: + a. HTTP \ + b. TCP \ + c. UDP \ + d. gRPC \ + e.TLS passthrough +2. TLS termination +3. Request Authentication +4. Rate Limiting + +## Key Assumptions + +This section outlines the foundational premises that shape our analysis and recommendations for the deployment and management of Envoy Gateway within an organisation. The key assumptions are as follows: + +**1. Kubernetes Provider**: For the purposes of this analysis, we assume that a K8s provider will be used to host the cluster. + +**2. Multi-tenant cluster**: In order to produce a broad set of recommendations, it is assumed that within the single cluster, there is: + +- A dedicated cluster operation (ops) team responsible for maintaining the core cluster infrastructure. + +- Multiple application teams who wish to define their own Gateway resources, which will route traffic to their respective applications. + +**3. Soft multi-tenancy model**: It is assumed that co-tenants will have some level of trust between themselves, and will not act in an overtly hostile manner to each other. + +**4. Ingress Control**: It's assumed that Envoy Gateway is the only ingress controller in the K8s cluster as multiple controllers can lead to complex routing challenges and introduce out-of-scope security vulnerabilities. + +**5. Container Security**: This threat model focuses on evaluating the security of the Envoy Gateway and Envoy Proxy images. All other container images running in tenant clusters, not associated with the edge proxy deployment, are assumed to be secure and obtained from trusted registries such as Docker Hub or Google Container Registry (GCR). + +**6. Cloud Provider Security**: It is assumed that the K8s cluster is running on secure cloud infrastructure provided by a trusted Cloud Service Provider (CSP) such as AWS, GCP, or Azure Cloud. + +## Data + +### Data Dictionary + +Ultimately, the data of interest in a threat model is the business data processed by the system in question. However, in the case of this threat model, we are looking at a generic deployment architecture involving Envoy Gateway in order to draw out a set of generalised threats which can be considered by teams looking to adopt an implementation of Gateway API. As such, we do not know the business impacts of a compromise of confidentiality, integrity or availability that would typically be captured in a data impact assessment. Instead, will we base our threat assessment on high-level groupings of data structures used in the configuration and operation of the general use cases considered (e.g. HTTP routing, TLS termination, request authentication etc.). We will then assign a confidentiality, integrity and availability impact based on a worst-case scenario of how each compromise could potentially affect business data processed by the generic deployment. + +| Data Name / Type | Notes | Confidentiality | Integrity | Availability | +| ------------ | ------------ | ------------ |--------------- | ------------ | +| Static Configuration Data | Static configuration data is used to configure Envoy Gateway at startup. This data structure allows for a Provider to be set, which Envoy Gateway calls to establish its runtime configuration, resolve services and persist data. Unauthorised modification of static configuration data could enable the Envoy Gateway admin interface to be configured, logging parameters to be modified, global rate limiting configuration to be misconfigured, or malicious extensions registered for the Envoy Gateway Control Plane. A compromise of confidentiality could potentially give an attacker some useful reconnaissance information. A compromise of the availability of this information at startup time would result in Envoy Gateway starting with default parameters. | Medium | High | Low | +| Dynamic Configuration Data | Dynamic configuration data represents the desired state of the Data Plane, and is defined through Envoy Gateway and Gateway API Kubernetes resources. Unauthorised modification of this data could lead to vulnerabilities in an organisation’s Data Plane infrastructure via misconfiguration of an EnvoyProxy custom resource. Misconfiguration of Gateway API objects such as HTTPRoutes or TLSRoutes could result in traffic being directed to incorrect backends. A compromise of confidentiality could potentially give an attacker some useful reconnaissance information. A compromise of the availability of this information could result in tenant application traffic not being routable until the configuration is recovered and reapplied. | Medium | High | Medium | +| TLS Private Keys | TLS Private Keys, typically in PEM format, are used to initiate secure connections and encrypt communications. In the context of this threat model, private keys will be associated with the server side of an inbound TLS connection being terminated at a secure gateway configured through Envoy Gateway. Unauthorised exposure could lead to security threats such as person-in-the-middle attacks, whereby the confidentiality or integrity of business data could be compromised. A compromise of integrity may lead to similar consequences if an attacker could insert their own key material. An availability compromise could lead to tenant services being unavailable until new key material is generated and an appropriate CSR submitted. | High | High | Medium | +| TLS Certificates | X.509 certificates represent the binding of a public key (associated with the private key described above) to an identity in a TLS handshake. If an attacker could compromise the integrity of a certificate, they may be able to bind the identity of a TLS termination point to a key pair under their control, enabling person-in-the middle attacks. An availability compromise could lead to tenant services being unavailable until new key material is generated and an appropriate CSR submitted. | Low | High | Medium | +| JWKs | JWK (JSON Web Key) containing a public key used to validate JWTs for the client authentication use case considered in this threat model. If an attacker could compromise the integrity of a JWK or JSON web key set (JWKS), they could potentially authenticate to a service maliciously. Unavailability of an endpoint exposing JWKs could lead to client requests which require authentication being denied. | Low | High | Medium | +| JWTs | JWTs, formatted as compact, URL-safe JSON data structures, are utilised for the client authentication use case considered in this threat model. Maintaining their confidentiality and integrity is vital to prevent unauthorised access and ensure correct user identification. | High | High | Low | +| OIDC credentials | In OIDC authentication scenarios, the application credentials are represented by a client ID and a client secret. A compromise of its confidentiality or integrity could allow malicious actors to impersonate the application, potentially being able to access resources on behalf of the application and request ID tokens on behalf of users. Unavailability of this data would produce a rejection of the requests coming from legitimate users. | High | High | Medium | +| Basic authentiation password hashes | In basic authentication scenarios, passwords are stored as Kubernetes secrets in [htpasswd](https://httpd.apache.org/docs/current/programs/htpasswd.html) format, where each entry is formed by the username and the hashed password. A compromise of these credentials' confidentiality and integrity could lead to unauthorised access to the application. Unavailability of these credentials will cause login failures from the application users. | High | High | Medium | + +### CIA Impact Assessment + +| Priority | Description | +| --- | --- | +| **Confidentiality** | | +| High | Compromise of sensitive client data | +| Medium | Information leaked which could be useful for attacker reconnaissance | +| Low | Non-sensitive information leakage | +| **Integrity** | | +| High | Compromise of source code repositories and gateway deployments | +| Medium | Traffic routing fails due to misconfiguration / invalid configuration | +| Low | Non-critical operation is blocked due to misconfiguration / invalid configuration | +| **Availability** | | +| High | Large scale DoS | +| Medium | Tenant application is blocked for a significant period | +| Low | Tenant application is blocked for a short period | + +### Data Flow Diagrams + +The Data Flow Diagrams (DFDs) below describe the flow of data between the various processes, entities and data stores in a system, as well as the trust boundaries between different user roles and network interfaces. The DFDs are drawn at two different levels, starting at L0 (high-level system view) and increasing in granularity (to L1). + +### DFD L0 + +![DFD L0](/img/DFDL0.png) + +### DFD L1 + +![DFD L1](/img/DFDL1.png) + +## Key Threats and Recommendations + +The scope of this threat model led to us categorising threats into priorities of High, Medium or Low; notably in a production implementation some of the threats' prioritisation may be upgraded or downgraded depending on the business context and data classification. + +### Risk vs. Threat + +For every finding, the risk and threat are stated. Risk defines the potential for negative outcome while threat defines the event that causes the negative outcome. + +### Threat Categorization + +Throughout this threat model, we categorised threats into different areas based on their origin and the segment of the system that they impact. Here's an overview of each category: + +**Container Security (CS)**: These threats are general to containerised applications. Therefore, they are not associated with Envoy Gateway or the Gateway API and could occur in most containerised workloads. They can originate from misconfigurations or vulnerabilities in the orchestrator or the container. + +**Gateway API (GW)**: These are threats related to the Gateway API that could affect any of its implementations. Malicious actors could benefit from misconfigurations or excessive permissions on the Gateway API resources (e.g. xRoutes or Gateways) to compromise the confidentiality, integrity, or availability of the application. + +**Envoy Gateway (EG)**: These threats are associated with specific configurations or features from Envoy Gateway or Envoy Proxy. If not set properly, these features could be leveraged to gain unauthorised access to protected resources. + +### Threat Actors + +In order to provide a realistic set of threats that is applicable to most organisations, we de-scoped the most advanced and hard to mitigate threat actors as described below: + +#### In Scope Threat Actors + +When considering internal threat actors, we chose to follow the [security model](https://gateway-api.sigs.k8s.io/concepts/security-model/) of the Kubernetes Gateway API. + +##### Internal Attacker + +- Cluster Operator: The cluster operator (ops) is responsible for administration of entire clusters. They manage policies, network access, application permissions. + +- Application Developer: The application developer (dev) is responsible for defining their application configuration (e.g. timeouts, request matching/filter) and Service composition (e.g. path routing to backends). + +- Application Administrator: The application admin has administrative access to some namespaces within a cluster, but not the cluster as a whole. + +##### External Attacker + +- Vandal: Script kiddie, trespasser + +- Motivated Individual: Political activist, thief, terrorist + +- Organised Crime: Syndicates, state-affiliated groups + +#### Out of Scope Threat Actors + +##### External Actors + +- Infrastructure Provider: The infrastructure provider (infra) is responsible for the overall environment that the cluster(s) are operating in. Examples include: the cloud provider, or the PaaS provider in a company. + +- Cloud Service Insider: Employee, external contractor, temporary worker + +- Foreign Intelligence Services (FIS): Nation states + +## High Priority Findings + +### EGTM-001 Usage of self-signed certificates + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-001|EGTM-GW-001|Gateway API|High| + + **Risk**: Self-signed certificates (which do not comply with PKI best practices) could lead to unauthorised access to the private key associated with the certificate used for inbound TLS termination at Envoy Proxy, compromising the confidentiality and integrity of proxied traffic. + + **Threat**: Compromise of the private key associated with the certificate used for inbound TLS terminating at Envoy Proxy. + + **Recommendation**: The Envoy Gateway quickstart demonstrates how to set up a Secure Gateway using an example where a self-signed root certificate is created using openssl. As stated in the Envoy Gateway documentation, this is not a suitable configuration for Production usage. It is recommended that PKI best practices are followed, whereby certificates are signed by an Intermediary CA which sits underneath an organisational \'offline\' Root CA. + + PKI best practices should also apply to the management of client certificates when using mTLS. The Envoy Gateway [mTLS](../security/mutual-tls) task shows how to set up client certificates using self-signed certificates. In the same way as gateway certificates and, as mentioned in the documentation, this configuration should not be used in production environments. + +### EGTM-002 Private keys are stored as Kubernetes secrets + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|--------------|------------------------|-----------------| +|EGTM-002|EGTM-CS-001|Container Security|High| + + **Risk**: There is a risk that a threat actor could compromise the Kubernetes secret containing the Envoy private key, allowing the attacker to decrypt Envoy Proxy traffic, compromising the confidentiality of proxied traffic. + + **Threat**: Kubernetes secret containing the Envoy private key is compromised and used to decrypt proxied traffic. + + **Recommendation**: Certificate management best practices mandate short-lived key material where practical, meaning that a mechanism for rotation of private keys and certificates is required, along with a way for certificates to be mounted into Envoy containers. If Kubernetes secrets are used, when a certificate expires, the associated secret must be updated, and Envoy containers must be redeployed. Instead of a manual configuration, it is recommended that [cert-manager](https://github.com/cert-manager/cert-manager) is used. + +### EGTM-004 Usage of ClusterRoles with wide permissions + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|--------------|------------------------|-----------------| +|EGTM-004|EGTM-K8-002|Container Security|High| + + **Risk**: There is a risk that a threat actor could abuse misconfigured RBAC to access the Envoy Gateway ClusterRole (envoy-gateway-role) and use it to expose all secrets across the cluster, thus compromising the confidentiality and integrity of tenant data. + + **Threat**: Compromised Envoy Gateway or misconfigured ClusterRoleBinding (envoy-gateway-rolebinding) to Envoy Gateway ClusterRole (envoy-gateway-role), provides access to resources and secrets in different namespaces. + + **Recommendation**: Users should be aware that Envoy Gateway uses a ClusterRole (envoy-gateway-role) when deployed via the Helm chart, to allow management of Envoy Proxies across different namespaces. This ClusterRole is powerful and includes the ability to read secrets in namespaces which may not be within the purview of Envoy Gateway. + + Kubernetes best-practices involve restriction of ClusterRoleBindings, with the use of RoleBindings where possible to limit access per namespace by specifying the namespace in metadata. Namespace isolation reduces the impact of compromise from cluster-scoped roles. Ideally, fine-grained K8s roles should be created per the principle of least privilege to ensure they have the minimum access necessary for role functions. + + The pull request \#[1656](https://github.com/envoyproxy/gateway/pull/1656) introduced the use of Roles and RoleBindings in [namespaced mode](https://gateway.envoyproxy.io/latest/api/extension_types/#kuberneteswatchmode). This feature can be leveraged to reduce the amount of permissions required by the Envoy Gateway. + +### EGTM-007 Misconfiguration of Envoy Gateway dynamic config + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-007|EGTM-EG-002|Envoy Gateway|High| + + **Risk**: There is a risk that a threat actor could exploit misconfigured Kubernetes RBAC to create or modify Gateway API resources with no business need, potentially leading to the compromise of the confidentiality, integrity, and availability of resources and traffic within the cluster. + + **Threat**: Unauthorised creation or misconfiguration of Gateway API resources by a threat actor with cluster-scoped access. + + **Recommendation**: Configure the apiGroup and resource fields in RBAC policies to restrict access to [Gateway](https://gateway-api.sigs.k8s.io/) and [GatewayClass](https://gateway-api.sigs.k8s.io/api-types/gatewayclass/) resources. Enable namespace isolation by using the namespace field, preventing unauthorised access to gateways in other namespaces. + +### EGTM-009 Co-tenant misconfigures resource across namespaces + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-009|EGTM-GW-002|Gateway API|High| + + **Risk**: There is a risk that a co-tenant misconfigures Gateway or Route resources, compromising the confidentiality, integrity, and availability of routed traffic through Envoy Gateway. + + **Threat**: Malicious or accidental co-tenant misconfiguration of Gateways and Routes associated with other application teams. + + **Recommendation**: Dedicated Envoy Gateways should be provided to each tenant within their respective namespace. A one-to-one relationship should be established between GatewayClass and Gateway resources, meaning that each tenant namespace should have their own GatewayClass watched by a unique Envoy Gateway Controller as defined here in the [Deployment Mode](../operations/deployment-mode) documentation. + + Application Admins should have write permissions on the Gateway resource, but only in their specific namespaces, and Application Developers should only hold write permissions on Route resources. To enact this access control schema, follow the [Write Permissions for Advanced 4 Tier Model](https://gateway-api.sigs.k8s.io/concepts/security-model/#write-permissions-for-advanced-4-tier-model) described in the Kubernetes Gateway API security model. Examples of secured gateway-route topologies can be found [here](https://gateway-api.sigs.k8s.io/concepts/api-overview/#attaching-routes-to-gateways) within the Kubernetes Gateway API docs. + + Optionally, consider a GitOps model, where only the GitOps operator has the permission to deploy or modify custom resources in production. + +### EGTM-014 Malicious image admission + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-014|EGTM-CS-006|Container Security|High| + + **Risk**: There is a risk that a supply chain attack on Envoy Gateway results in an arbitrary compromise of the confidentiality, integrity or availability of tenant data. + + **Threat**: Supply chain threat actor introduces malicious code into Envoy Gateway or Proxy. + + **Recommendation**: The Envoy Gateway project should continue to work towards conformance with supply-chain security best practices throughout the project lifecycle (for example, as set out in the [CNCF Software Supply Chain Best Practices Whitepaper](https://github.com/cncf/tag-security/blob/main/supply-chain-security/supply-chain-security-paper/CNCF_SSCP_v1.pdf)). Adherence to [Supply-chain Levels for Software Artefacts](https://slsa.dev/) (SLSA) standards is crucial for maintaining the security of the system. Employ version control systems to monitor the source and build platforms and assign responsibility to a specific stakeholder. + + Integrate a supply chain security tool such as Sigstore, which provides native capabilities for signing and verifying container images and software artefacts. [Software Bill of Materials](https://www.cisa.gov/sbom) (SBOM), [Vulnerability Exploitability eXchange](https://www.ntia.gov/files/ntia/publications/vex_one-page_summary.pdf) (VEX), and signed artefacts should also be incorporated into the security protocol. + +### EGTM-020 Out of date or misconfigured Envoy Proxy image + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-020|EGTM-CS-009|Container Security|High| + + **Risk**: There is a risk that a threat actor exploits an Envoy Proxy vulnerability to remote code execution (RCE) due to out of date or misconfigured Envoy Proxy pod deployment, compromising the confidentiality and integrity of Envoy Proxy along with the availability of the proxy service. + + **Threat**: Deployment of an Envoy Proxy or Gateway image containing exploitable CVEs. + + **Recommendation**: Always use the latest version of the Envoy Proxy image. Regularly check for updates and patch the system as soon as updates become available. Implement a CI/CD pipeline that includes security checks for images and prevents deployment of insecure configurations. A suitable tool should be chosen to provide container vulnerability scanning to mitigate the risk of known vulnerabilities. + + Utilise the [Pod Security Admission](https://kubernetes.io/docs/concepts/security/pod-security-admission/) controller to enforce [Pod Security Standards](https://kubernetes.io/docs/concepts/security/pod-security-standards/) and configure the [pod security context](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/) to limit its capabilities per the principle of least privilege. + +### EGTM-022 Credentials are stored as Kubernetes Secrets + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-022|EGTM-CS-010|Container Security|High| + + **Risk**: There is a risk that the OIDC client secret (for OIDC authentication) and user password hashes (for basic authentication) get leaked due to misconfigured RBAC permissions. + + **Threat**: Unauthorised access to the application due to credential leakage. + + **Recommendation**: Ensure that only authorised users and service accounts are able to access secrets. This is especially important in namespaces where SecurityPolicy objects are configured, since those namespaces are the ones to store secrets containing the client secret (in OIDC scenarios) and user password hashes (in basic authentication scenarios). + + To do so, minimise the use of ClusterRoles and Roles allowing listing and getting secrets. Perform periodic audits of RBAC permissions. + +### EGTM-023 Weak Authentication + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-023|EGTM-EG-007|Envoy Gateway|High| + + **Risk**: There is a risk of unauthorised access due to the use of basic authentication, which does not enforce any password restriction in terms of complexity and length. In addition, password hashes are stored in SHA1 format, which is a deprecated hashing function. + + **Threat**: Unauthorised access to the application due to weak authentication mechanisms. + + **Recommendation**: It is recommended to make use of stronger authentication mechanisms (i.e. JWT authentication and OIDC authentication) instead of basic authentication. These authentication mechanisms have many advantages, such as the use of short-lived credentials and a central management of security policies through the identity provider. + +## Medium Priority Findings + +### EGTM-008 Misconfiguration of Envoy Gateway static config + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-008|EGTM-EG-003|Envoy Gateway|Medium| + + **Risk**: There is a risk of a threat actor misconfiguring static config and compromising the integrity of Envoy Gateway, ultimately leading to the compromised confidentiality, integrity, or availability of tenant data and cluster resources. + + **Threat**: Accidental or deliberate misconfiguration of static configuration leads to a misconfigured deployment of Envoy Gateway, for example logging parameters could be modified or global rate limiting configuration misconfigured. + + **Recommendation**: Implement a GitOps model, utilising Kubernetes\' Role-Based Access Control (RBAC) and adhering to the principle of least privilege to minimise human intervention on the cluster. For instance, tools like [Flux](https://fluxcd.io/) and [ArgoCD](https://argo-cd.readthedocs.io/en/stable/) can be used for declarative GitOps deployments, ensuring all changes are tracked and reviewed. Additionally, configure your source control management (SCM) system to include mandatory pull request (PR) reviews, commit signing, and protected branches to ensure only authorised changes can be committed to the start-up configuration. + +### EGTM-010 Weak pod security contexts and policies + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-010|EGTM-CS-005|Container Security|Medium| + + **Risk**: There is a risk that a threat actor exploits a weak pod security context, compromising the CIA of a node and the resources / services which run on it. + + **Threat**: Threat Actor who has compromised a pod exploits weak security context to escape to a node, potentially leading to the compromise of Envoy Proxy or Gateway running on the same node. + + **Recommendation**: To mitigate this risk, apply [Pod Security Standards](https://kubernetes.io/docs/concepts/security/pod-security-standards/) at a minimum of [Baseline](https://kubernetes.io/docs/concepts/security/pod-security-standards/#baseline) level to all namespaces, especially those containing Envoy Gateway and Proxy Pods. Pod security standards are implemented through K8s [Pod Security Admission](https://kubernetes.io/docs/concepts/security/pod-security-admission/) to provide [admission control modes](https://kubernetes.io/docs/concepts/security/pod-security-admission/#pod-security-admission-labels-for-namespaces) (enforce, audit, and warn) for namespaces. Pod security standards can be enforced by namespace labels as shown [here](https://kubernetes.io/docs/tasks/configure-pod-container/enforce-standards-namespace-labels/), to enforce a baseline level of pod security to specific namespaces. + + Further enhance the security by implementing a sandboxing solution such as [gVisor](https://gvisor.dev/) for Envoy Gateway and Proxy Pods to isolate the application from the host kernel. This can be set within the runtimeClassName of the Pod specification. + +### EGTM-012 ClusterRoles and Roles with permission to deploy ReferenceGrants + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|----------------|----------------------|-----------------| +|EGTM-012|EGTM-GW-004|Gateway API|Medium| + + **Risk**: There is a risk that a threat actor could abuse excessive RBAC privileges to create ReferenceGrant resources. These resources could then be used to create cross-namespace communication, leading to unauthorised access to the application. This could compromise the confidentiality and integrity of resources and configuration in the affected namespaces and potentially disrupt the availability of services that rely on these object references. + + **Threat**: A ReferenceGrant is created, which validates traffic to cross namespace trust boundaries without a valid business reason, such as a route in one tenant\'s namespace referencing a backend in another. + + **Recommendation**: Ensure that the ability to create ReferenceGrant resources is restricted to the minimum number of people. Pay special attention to ClusterRoles that allow that action. + +### EGTM-018 Network Denial of Service (DoS) + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|----------------|----------------------|-----------------| +|EGTM-018|EGTM-GW-006|Gateway API|Medium| + + **Risk**: There is a risk that malicious requests could lead to a Denial of Service (DoS) attack, thereby reducing API gateway availability due to misconfigurations in rate-limiting or load balancing controls, or a lack of route timeout enforcement. + + **Threat**: Reduced API gateway availability due to an attacker\'s maliciously crafted request (e.g., QoD) potentially inducing a Denial of Service (DoS) attack. + + **Recommendation**: To ensure high availability and mitigate potential security threats, follow the guidelines in the Envoy Gateway documentation for configuring [local rate limit](../traffic/local-rate-limit) filters, [global rate limit](../traffic/global-rate-limit) filters, and load balancing. + + Further, adhere to best practices for configuring Envoy Proxy as an edge proxy documented [here](https://www.envoyproxy.io/docs/envoy/latest/configuration/best_practices/edge#configuring-envoy-as-an-edge-proxy) within the EnvoyProxy docs. This involves configuring TCP and HTTP proxies with specific settings, including restricting access to the admin endpoint, setting the [overload manager](https://www.envoyproxy.io/docs/envoy/latest/configuration/operations/overload_manager/overload_manager#config-overload-manager) and [listener](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) / [cluster](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) buffer limits, enabling [use_remote_address](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-use-remote-address), setting [connection and stream timeouts](https://www.envoyproxy.io/docs/envoy/latest/faq/configuration/timeouts#faq-configuration-timeouts), limiting [maximum concurrent streams](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-field-config-core-v3-http2protocoloptions-max-concurrent-streams), setting [initial stream window size limit](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-field-config-core-v3-http2protocoloptions-initial-stream-window-size), and configuring action on [headers_with_underscores](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-field-config-core-v3-httpprotocoloptions-headers-with-underscores-action). + + [Path normalisation](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-normalize-path) should be enabled to minimise path confusion vulnerabilities. These measures help protect against volumetric threats such as Denial of Service (DoS) attacks. Utilise custom resources to implement policy attachment, thereby exposing request limit configuration for route types. + +### EGTM-019 JWT-based authentication replay attacks + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-019|EGTM-DP-004|Container Security|Medium| + + **Risk**: There is a risk that replay attacks using stolen or reused JSON Web Tokens (JWTs) can compromise transmission integrity, thereby undermining the confidentiality and integrity of the data plane. + + **Threat**: Transmission integrity is compromised due to replay attacks using stolen or reused JSON Web Tokens (JWTs). + + **Recommendation**: Comply with JWT best practices for enhanced security, paying special attention to the use of short-lived tokens, which reduce the window of opportunity for a replay attack. The [exp](https://datatracker.ietf.org/doc/html/rfc7519#page-9) claim can be used to set token expiration times. + +### EGTM-024 Excessive privileges via extension policies + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-024|EGTM-EG-008|Envoy Gateway|Medium| + + **Risk**: There is a risk of developers getting more privileges than required due to the use of SecurityPolicy, ClientTrafficPolicy, EnvoyPatchPolicy and BackendTrafficPolicy. These resources can be attached to a Gateway resource. Therefore, a developer with permission to deploy them would be able to modify a Gateway configuration by targeting the gateway in the policy manifest. This conflicts with the [Advanced 4 Tier Model](https://gateway-api.sigs.k8s.io/concepts/security-model/#write-permissions-for-advanced-4-tier-model), where developers do not have write permissions on Gateways. + + **Threat**: Excessive developer permissions lead to a misconfiguration and/or unauthorised access. + + **Recommendation**: Considering the Tenant C scenario (represented in the Architecture Diagram), if a developer can create SecurityPolicy, ClientTrafficPolicy, EnvoyPatchPolicy or BackendTrafficPolicy objects in namespace C, they would be able to modify a Gateway configuration by attaching the policy to the gateway. In such scenarios, it is recommended to either: + + a. Create a separate namespace, where developers have no permissions, to host tenant C\'s gateway. Note that, due to design decisions, the SecurityPolicy/EnvoyPatchPolicy/ClientTrafficPolicy/BackendTrafficPolicy object can only target resources deployed in the same namespace. Therefore, having a separate namespace for the gateway would prevent developers from attaching the policy to the gateway. + + b. Forbid the creation of these policies for developers in namespace C. + + On the other hand, in scenarios similar to tenants A and B, where a shared gateway namespace is in place, this issue is more limited. Note that in this scenario, developers don\'t have access to the shared gateway namespace. + + In addition, it is important to mention that EnvoyPatchPolicy resources can also be attached to GatewayClass resources. This means that, in order to comply with the Advanced 4 Tier model, individuals with the Application Administrator role should not have access to this resource either. + +## Low Priority Findings + +### EGTM-003 Misconfiguration leads to insecure TLS settings + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|--------------|------------------------|-----------------| +|EGTM-003|EGTM-EG-001|Envoy Gateway|Low| + + **Risk**: There is a risk that a threat actor could downgrade the security of proxied connections by configuring a weak set of cipher suites, compromising the confidentiality and integrity of proxied traffic. + + **Threat**: Exploit weak cipher suite configuration to downgrade security of proxied connections. + + **Recommendation**: Users operating in highly regulated environments may need to tightly control the TLS protocol and associated cipher suites, blocking non-conforming incoming connections to the gateway. + + EnvoyProxy bootstrap config can be customised as per the [customise EnvoyProxy](../operations/customize-envoyproxy) documentation. In addition, from v.1.0.0, it is possible to configure common TLS properties for a Gateway or XRoute through the [ClientTrafficPolicy](https://gateway.envoyproxy.io/latest/api/extension_types/#clienttrafficpolicy) object. + +### EGTM-005 Envoy Gateway Helm chart deployment does not set AppArmor and Seccomp profiles + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-005|EGTM-CP-002|Container Security|Low| + + **Risk**: Threat actor who has obtained access to Envoy Gateway pod could exploit the lack of AppArmor and Seccomp profiles in the Envoy Gateway deployment to attempt a container breakout, given the presence of an exploitable vulnerability, potentially impacting the confidentiality and integrity node resources. + + **Threat**: Unauthorised syscalls and malicious code running in the Envoy Gateway pod. + + **Recommendation**: Implement [AppArmor](https://kubernetes.io/docs/tutorials/security/apparmor/) policies by setting \: \ within container.apparmor.security.beta.kubernetes.io (note, this config is set *per container*). Well-defined AppArmor policies may provide greater protection from unknown threats. + + Enforce [Seccomp](https://kubernetes.io/docs/tutorials/security/seccomp/) profiles by setting the seccompProfile under securityContext. Ideally, a [fine-grained](https://kubernetes.io/docs/tutorials/security/seccomp/#create-pod-with-a-seccomp-profile-that-only-allows-necessary-syscalls) profile should be used to restrict access to only necessary syscalls, however the \--seccomp-default flag can be set to resort to [RuntimeDefault](https://kubernetes.io/docs/tutorials/security/seccomp/#create-pod-that-uses-the-container-runtime-default-seccomp-profile) which provides a container runtime specific. Example seccomp profiles can be found [here](https://kubernetes.io/docs/tutorials/security/seccomp/#download-profiles). + + To further enhance pod security, consider implementing [SELinux](https://en.wikipedia.org/wiki/Security-Enhanced_Linux) via seLinuxOptions for additional syscall attack surface reduction. Setting readOnlyRootFilesystem == true enforces an immutable root filesystem, preventing the addition of malicious binaries to the PATH and increasing the attack cost. Together, these configuration items improve the pods [Security Context](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/). + +### EGTM-006 Envoy Proxy pods deployed with a shell enabled + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-006|EGTM-CS-004|Container Security|Low| + + **Risk**: There is a risk that a threat actor exploits a vulnerability in Envoy Proxy to expose a reverse shell, enabling them to compromise the confidentiality, integrity and availability of tenant data via a secondary attack. + + **Threat**: If an external attacker managed to exploit a vulnerability in Envoy, the presence of a shell would be greatly helpful for the attacker in terms of potentially pivoting, escalating, or establishing some form of persistence. + + **Recommendation**: By default, Envoy uses a [distroless](https://github.com/GoogleContainerTools/distroless) image since v.0.6.0, which does not ship a shell. Therefore, ensure EnvoyProxy image is up-to-date and patched with the latest stable version. + + If using private EnvoyProxy images, use a lightweight EnvoyProxy image without a shell or debugging tool(s) which may be useful for an attacker. + + An [AuditPolicy](https://kubernetes.io/docs/tasks/debug/debug-cluster/audit/#audit-policy) (audit.k8s.io/v1beta1) can be configured to record API calls made within your cluster, allowing for identification of malicious traffic and enabling incident response. Requests are recorded based on stages which delineate between the lifecycle stage of the request made (e.g., RequestReceived, ResponseStarted, & ResponseComplete). + +### EGTM-011 Route Bindings on custom labels + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-011|EGTM-GW-003|Gateway API|Low| + + **Risk**: There is a risk that a gateway owner (or someone with the ability to set namespace labels) maliciously or accidentally binds routes across namespace boundaries, potentially compromising the confidentiality and integrity of traffic in a multitenant scenario. + + **Threat**: If a Route Binding within a Gateway Listener is configured based on a custom label, it could allow a malicious internal actor with the ability to label namespaces to change the set of namespaces supported by the Gateway. + + **Recommendation**: Consider the use of custom admission control to restrict what labels can be set on namespaces through tooling such as [Kubewarden](https://kyverno.io/policies/pod-security/), [Kyverno](https://github.com/kubewarden), and [OPA Gatekeeper](https://github.com/open-policy-agent/gatekeeper). Route binding should follow the Kubernetes Gateway API security model, as shown [here](https://gateway-api.sigs.k8s.io/concepts/security-model/#1-route-binding), to connect gateways in different namespaces. + +### EGTM-013 GatewayClass namespace validation is not configured + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-013|EGTM-GW-005|Gateway API|Low| + + **Risk**: There is a risk that an unauthorised actor deploys an unauthorised GatewayClass due to GatewayClass namespace validation not being configured, leading to non-compliance with business and security requirements. + + **Threat**: Unauthorised deployment of Gateway resource via GatewayClass template which crosses namespace trust boundaries. + + **Recommendation**: Leverage GatewayClass namespace validation to limit the namespaces where GatewayClasses can be run through a tool such as [OPA Gatekeeper](https://github.com/open-policy-agent/gatekeeper). Reference pull request \#[24](https://github.com/open-policy-agent/gatekeeper-library/pull/24) within gatekeeper-library which outlines how to add GatewayClass namespace validation through a GatewayClassNamespaces API resource kind within the constraints.gatekeeper.sh/v1beta1 apiGroup. + +### EGTM-015 ServiceAccount token authentication + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-015|EGTM-CS-007|Container Security|Low| + + **Risk**: There is a risk that threat actors could exploit ServiceAccount tokens for illegitimate authentication, thereby leading to privilege escalation and the undermining of gateway API resources\' integrity, confidentiality, and availability. + + **Threat**: The threat arises from threat actors impersonating the envoy-gateway ServiceAccount through the replay of ServiceAccount tokens, thereby achieving escalated privileges and gaining unauthorised access to Kubernetes resources. + + **Recommendation**: Limit the creation of ServiceAccounts to only when necessary, specifically refraining from using default service account tokens, especially for high-privilege service accounts. For legacy clusters running Kubernetes version 1.21 or earlier, note that ServiceAccount tokens are long-lived by default. To disable the automatic mounting of the service account token, set automountServiceAccountToken: false in the PodSpec. + +### EGTM-016 Misconfiguration leads to lack of Envoy Proxy access activity visibility + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-016|EGTM-EG-004|Envoy Gateway|Low| + + **Risk**: There is a risk that threat actors establish persistence and move laterally through the cluster unnoticed due to limited visibility into access and application-level activity. + + **Threat**: Threat actors establish persistence and move laterally through the cluster unnoticed. + + **Recommendation**: Configure [access logging](../../../contributions/design/proxy-accesslog) in the EnvoyProxy. Use [ProxyAccessLogFormatType](../../api/extension_types#proxyaccesslogformattype) (Text or JSON) to specify the log format and ensure that the logs are sent to the desired sink types by setting the [ProxyAccessLogSinkType](https://gateway.envoyproxy.io/latest/api/extension_types/#proxyaccesslogsinktype). Make use of [FileEnvoyProxyAccessLog](https://gateway.envoyproxy.io/latest/api/extension_types/#fileenvoyproxyaccesslog) or [OpenTelemetryEnvoyProxyAccessLog](https://gateway.envoyproxy.io/latest/api/extension_types/#opentelemetryenvoyproxyaccesslog) to configure File and OpenTelemetry sinks, respectively. If the settings aren\'t defined, the default format is sent to stdout. + + Additionally, consider leveraging a central logging mechanism such as [Fluentd](https://github.com/fluent/fluentd) to enhance visibility into access activity and enable effective incident response (IR). + +### EGTM-017 Misconfiguration leads to lack of Envoy Gateway activity visibility + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-017|EGTM-EG-005|Envoy Gateway|Low| + + **Risk**: There is a risk that an insider misconfigures an envoy gateway component and goes unnoticed due to a low-touch logging configuration (via default) which responsible stakeholders are not aptly aware of or have immediate access to. + + **Threat**: The threat emerges from an insider misconfiguring an Envoy Gateway component without detection. + + **Recommendation**: Configure the logging level of the Envoy Gateway using the \'level\' field in [EnvoyGatewayLogging](https://gateway.envoyproxy.io/latest/api/extension_types/#envoygatewaylogging). Ensure the appropriate logging levels are set for relevant components such as \'gateway-api\', \'xds-translator\', or \'global-ratelimit\'. If left unspecified, the logging level defaults to \"info\", which may not provide sufficient detail for security monitoring. + + Employ a centralised logging mechanism, like [Fluentd](https://github.com/fluent/fluentd), to enhance visibility into application-level activity and to enable efficient incident response. + +### EGTM-021 Exposed Envoy Proxy admin interface + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-021|EGTM-EG-006|Envoy Gateway|Low| + + **Risk**: There is a risk that the admin interface is exposed without valid business reason, increasing the attack surface. + + **Threat**: Exposed admin interfaces give internal attackers the option to affect production traffic in unauthorised ways, and the option to exploit any vulnerabilities which may be present in the admin interface (e.g. by orchestrating malicious GET requests to the admin interface through CSRF, compromising Envoy Proxy global configuration or shutting off the service entirely e.g. /quitquitquit). + + **Recommendation**: The Envoy Proxy admin interface is only exposed to localhost, meaning that it is secure by default. However, due to the risk of misconfiguration, this recommendation is included. + + Due to the importance of the admin interface, it is recommended to ensure that Envoy Proxies have not been accidentally misconfigured to expose the admin interface to untrusted networks. + +### EGTM-025 Envoy Proxy pods deployed running as root user in the container + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|--------------|------------------------|-----------------| +|EGTM-025|EGTM-CS-011|Container Security|Low| + +**Risk**: The presence of a vulnerability, be it in the kernel or another system component, when coupled with containers running as root, could enable a threat actor to escape the container, thereby compromising the confidentiality, integrity, or availability of cluster resources + + **Threat**: The Envoy Proxy container's root-user configuration can be leveraged by an attacker to escalate privileges, execute a container breakout, and traverse across trust boundaries. + + **Recommendation**: By default, Envoy Gateway deployments do not use root users. Nonetheless, in case a custom image or deployment manifest is to be used, make sure Envoy Proxy pods run as a non-root user with a high UID within the container. + +Set runAsUser and runAsGroup security context options to specific UIDs (e.g., runAsUser: 1000 & runAsGroup: 3000) to ensure the container operates with the stipulated non-root user and group ID. If using helm chart deployment, define the user and group ID in the values.yaml file or via the command line during helm install / upgrade. + +## Appendix + +### In Scope Threat Actor Details + +|Threat Actor | Capability | Personal Motivation | Envoy Gateway Attack Samples| +|-|-|-|-| +|Application Developer | Leverage internal knowledge and personal access to the Envoy Gateway infrastructure to move laterally and transit trust boundaries | Disgruntled / personal grievances.

Financial incentives | Misconfigure XRoute resources to expose internal applications.

Misconfigure SecurityPolicy objects, reducing the security posture of an application.| +|Application Administrator | Abuse privileged status to disrupt operations and tenant cluster services through Envoy Gateway misconfig | Disgruntled / personal grievances.

Financial incentives | Create malicious routes to internal applications.

Introduce malicious Envoy Proxy images.

Expose the Envoy Proxy Admin interface.| +|Cluster Operator | Alter application-level deployments by misconfiguring resource dependencies & SCM to introduce vulnerabilities | Disgruntled / personal grievances.

Financial incentives.

Notoriety | Deploy malicious resources to expose internal applications.

Access authentication secrets.

Fall victim to phishing attacks and inadvertently share authentication credentials to cloud infrastructure or Kubernetes clusters.| +|Vandal: Script Kiddie, Trespasser | Uses publicly available tools and applications (Nmap,Metasploit, CVE PoCs) | Curiosity.

Personal fame through defacement / denial of service of prominent public facing web resources | Small scale DOS.

Launches prepackaged exploits, runs crypto mining tools.

Exploit public-facing application services such as the bastion host to gain an initial foothold in the environment| +|Motivated individual: Political activist, Thief, Terrorist | Write tools and exploits required for their means if sufficiently motivated.

Tend to use these in a targeted fashion against specific organisations. May combine publicly available exploits in a targeted fashion. Tamper with open source supply chains | Personal Gain (Political or Ideological) | Phishing, DDOS, exploit known vulnerabilities.

Compromise third-party components such as Helm charts and container images to inject malicious codes to propagate access throughout the environment.| +|Organised crime: syndicates, state-affiliated groups | Write tools and exploits required for their means.

Tend to use these in a non-targeted fashion, unless motivation is sufficiently high.

Devotes considerable resources, writes exploits, can bribe/coerce, can launch targeted attacks | Ransom.

Mass extraction of PII / credentials / PCI data.

Financial incentives | Social Engineering, phishing, ransomware, coordinated attacks.

Intercept and replay JWT tokens (via MiTM) between tenant user(s) and envoy gateway to modify app configs in-transit| + +### Identified Threats by Priority + +|ID|UID|Category|Risk|Threat|Priority| Recommendation | +|-|-|-|-|-|-|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|EGTM-001|EGTM-GW-001|Gateway API| Self-signed certificates (which do not comply with PKI best practices) could lead to unauthorised access to the private key associated with the certificate used for inbound TLS termination at Envoy Proxy, compromising the confidentiality and integrity of proxied traffic.

| Compromise of the private key associated with the certificate used for inbound TLS terminating at Envoy Proxy.

|High| The Envoy Gateway quickstart demonstrates how to set up a Secure Gateway using an example where a self-signed root certificate is created using openssl. As stated in the Envoy Gateway documentation, this is not a suitable configuration for Production usage. It is recommended that PKI best practices are followed, whereby certificates are signed by an Intermediary CA which sits underneath an organisational \'offline\' Root CA.

PKI best practices should also apply to the management of client certificates when using mTLS. The Envoy Gateway [mTLS](../security/mutual-tls) task shows how to set up client certificates using self-signed certificates. In the same way as gateway certificates and, as mentioned in the documentation, this configuration should not be used in production environments. | +|EGTM-002|EGTM-CS-001|Container Security| There is a risk that a threat actor could compromise the Kubernetes secret containing the Envoy private key, allowing the attacker to decrypt Envoy Proxy traffic, compromising the confidentiality of proxied traffic.

| Kubernetes secret containing the Envoy private key is compromised and used to decrypt proxied traffic.

|High| Certificate management best practices mandate short-lived key material where practical, meaning that a mechanism for rotation of private keys and certificates is required, along with a way for certificates to be mounted into Envoy containers. If Kubernetes secrets are used, when a certificate expires, the associated secret must be updated, and Envoy containers must be redeployed. Instead of a manual configuration, it is recommended that [cert-manager](https://github.com/cert-manager/cert-manager) is used. | +|EGTM-004|EGTM-K8-002|Container Security| There is a risk that a threat actor could abuse misconfigured RBAC to access the Envoy Gateway ClusterRole (envoy-gateway-role) and use it to expose all secrets across the cluster, thus compromising the confidentiality and integrity of tenant data.

| Compromised Envoy Gateway or misconfigured ClusterRoleBinding (envoy-gateway-rolebinding) to Envoy Gateway ClusterRole (envoy-gateway-role), provides access to resources and secrets in different namespaces.

|High| Users should be aware that Envoy Gateway uses a ClusterRole (envoy-gateway-role) when deployed via the Helm chart, to allow management of Envoy Proxies across different namespaces. This ClusterRole is powerful and includes the ability to read secrets in namespaces which may not be within the purview of Envoy Gateway.

Kubernetes best-practices involve restriction of ClusterRoleBindings, with the use of RoleBindings where possible to limit access per namespace by specifying the namespace in metadata. Namespace isolation reduces the impact of compromise from cluster-scoped roles. Ideally, fine-grained K8s roles should be created per the principle of least privilege to ensure they have the minimum access necessary for role functions.

The pull request \#[1656](https://github.com/envoyproxy/gateway/pull/1656) introduced the use of Roles and RoleBindings in [namespaced mode](https://gateway.envoyproxy.io/latest/api/extension_types/#kuberneteswatchmode). This feature can be leveraged to reduce the amount of permissions required by the Envoy Gateway. | +|EGTM-007|EGTM-EG-002|Envoy Gateway| There is a risk that a threat actor could exploit misconfigured Kubernetes RBAC to create or modify Gateway API resources with no business need, potentially leading to the compromise of the confidentiality, integrity, and availability of resources and traffic within the cluster.

| Unauthorised creation or misconfiguration of Gateway API resources by a threat actor with cluster-scoped access.

|High| Configure the apiGroup and resource fields in RBAC policies to restrict access to [Gateway](https://gateway-api.sigs.k8s.io/) and [GatewayClass](https://gateway-api.sigs.k8s.io/api-types/gatewayclass/) resources. Enable namespace isolation by using the namespace field, preventing unauthorised access to gateways in other namespaces. | +|EGTM-009|EGTM-GW-002|Gateway API| There is a risk that a co-tenant misconfigures Gateway or Route resources, compromising the confidentiality, integrity, and availability of routed traffic through Envoy Gateway.

| Malicious or accidental co-tenant misconfiguration of Gateways and Routes associated with other application teams.

|High| Dedicated Envoy Gateways should be provided to each tenant within their respective namespace. A one-to-one relationship should be established between GatewayClass and Gateway resources, meaning that each tenant namespace should have their own GatewayClass watched by a unique Envoy Gateway Controller as defined here in the [Deployment Mode](../operations/deployment-mode) documentation.

Application Admins should have write permissions on the Gateway resource, but only in their specific namespaces, and Application Developers should only hold write permissions on Route resources. To enact this access control schema, follow the [Write Permissions for Advanced 4 Tier Model](https://gateway-api.sigs.k8s.io/concepts/security-model/#write-permissions-for-advanced-4-tier-model) described in the Kubernetes Gateway API security model. Examples of secured gateway-route topologies can be found [here](https://gateway-api.sigs.k8s.io/concepts/api-overview/#attaching-routes-to-gateways) within the Kubernetes Gateway API docs.

Optionally, consider a GitOps model, where only the GitOps operator has the permission to deploy or modify custom resources in production. | +|EGTM-014|EGTM-CS-006|Container Security| There is a risk that a supply chain attack on Envoy Gateway results in an arbitrary compromise of the confidentiality, integrity or availability of tenant data.

| Supply chain threat actor introduces malicious code into Envoy Gateway or Proxy.

|High| The Envoy Gateway project should continue to work towards conformance with supply-chain security best practices throughout the project lifecycle (for example, as set out in the [CNCF Software Supply Chain Best Practices Whitepaper](https://github.com/cncf/tag-security/blob/main/supply-chain-security/supply-chain-security-paper/CNCF_SSCP_v1.pdf). Adherence to [Supply-chain Levels for Software Artefacts](https://slsa.dev/) (SLSA) standards is crucial for maintaining the security of the system. Employ version control systems to monitor the source and build platforms and assign responsibility to a specific stakeholder.

Integrate a supply chain security tool such as Sigstore, which provides native capabilities for signing and verifying container images and software artefacts. [Software Bill of Materials](https://www.cisa.gov/sbom) (SBOM), [Vulnerability Exploitability eXchange](https://www.ntia.gov/files/ntia/publications/vex_one-page_summary.pdf) (VEX), and signed artefacts should also be incorporated into the security protocol. | +|EGTM-020|EGTM-CS-009|Container Security| There is a risk that a threat actor exploits an Envoy Proxy vulnerability to remote code execution (RCE) due to out of date or misconfigured Envoy Proxy pod deployment, compromising the confidentiality and integrity of Envoy Proxy along with the availability of the proxy service.

| Deployment of an Envoy Proxy or Gateway image containing exploitable CVEs.

|High| Always use the latest version of the Envoy Proxy image. Regularly check for updates and patch the system as soon as updates become available. Implement a CI/CD pipeline that includes security checks for images and prevents deployment of insecure configurations. A tool such as Snyk can be used to provide container vulnerability scanning to mitigate the risk of known vulnerabilities.

Utilise the [Pod Security Admission](https://kubernetes.io/docs/concepts/security/pod-security-admission/) controller to enforce [Pod Security Standards](https://kubernetes.io/docs/concepts/security/pod-security-standards/) and configure the [pod security context](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/) to limit its capabilities per the principle of least privilege. | +|EGTM-022|EGTM-CS-010|Container Security| There is a risk that the OIDC client secret (for OIDC authentication) and user password hashes (for basic authentication) get leaked due to misconfigured RBAC permissions.

| Unauthorised access to the application due to credential leakage.

|High| Ensure that only authorised users and service accounts are able to access secrets. This is especially important in namespaces where SecurityPolicy objects are configured, since those namespaces are the ones to store secrets containing the client secret (in OIDC scenarios) and user password hashes (in basic authentication scenarios).

To do so, minimise the use of ClusterRoles and Roles allowing listing and getting secrets. Perform periodic audits of RBAC permissions. | +|EGTM-023|EGTM-EG-007|Envoy Gateway| There is a risk of unauthorised access due to the use of basic authentication, which does not enforce any password restriction in terms of complexity and length. In addition, password hashes are stored in SHA1 format, which is a deprecated hashing function.

| Unauthorised access to the application due to weak authentication mechanisms.

|High| It is recommended to make use of stronger authentication mechanisms (i.e. JWT authentication and OIDC authentication) instead of basic authentication. These authentication mechanisms have many advantages, such as the use of short-lived credentials and a central management of security policies through the identity provider. | +|EGTM-008|EGTM-EG-003|Envoy Gateway| There is a risk of a threat actor misconfiguring static config and compromising the integrity of Envoy Gateway, ultimately leading to the compromised confidentiality, integrity, or availability of tenant data and cluster resources.

| Accidental or deliberate misconfiguration of static configuration leads to a misconfigured deployment of Envoy Gateway, for example logging parameters could be modified or global rate limiting configuration misconfigured.

|Medium| Implement a GitOps model, utilising Kubernetes\' Role-Based Access Control (RBAC) and adhering to the principle of least privilege to minimise human intervention on the cluster. For instance, tools like [ArgoCD](https://argo-cd.readthedocs.io/en/stable/) can be used for declarative GitOps deployments, ensuring all changes are tracked and reviewed. Additionally, configure your source control management (SCM) system to include mandatory pull request (PR) reviews, commit signing, and protected branches to ensure only authorised changes can be committed to the start-up configuration. | +|EGTM-010|EGTM-CS-005|Container Security| There is a risk that a threat actor exploits a weak pod security context, compromising the CIA of a node and the resources / services which run on it.

| Threat Actor who has compromised a pod exploits weak security context to escape to a node, potentially leading to the compromise of Envoy Proxy or Gateway running on the same node.

|Medium| To mitigate this risk, apply [Pod Security Standards](https://kubernetes.io/docs/concepts/security/pod-security-standards/) at a minimum of [Baseline](https://kubernetes.io/docs/concepts/security/pod-security-standards/#baseline) level to all namespaces, especially those containing Envoy Gateway and Proxy Pods. Pod security standards are implemented through K8s [Pod Security Admission](https://kubernetes.io/docs/concepts/security/pod-security-admission/) to provide [admission control modes](https://kubernetes.io/docs/concepts/security/pod-security-admission/#pod-security-admission-labels-for-namespaces) (enforce, audit, and warn) for namespaces. Pod security standards can be enforced by namespace labels as shown [here](https://kubernetes.io/docs/tasks/configure-pod-container/enforce-standards-namespace-labels/), to enforce a baseline level of pod security to specific namespaces.

Further enhance the security by implementing a sandboxing solution such as [gVisor](https://gvisor.dev/) for Envoy Gateway and Proxy Pods to isolate the application from the host kernel. This can be set within the runtimeClassName of the Pod specification. | +|EGTM-012|EGTM-GW-004|Gateway API| There is a risk that a threat actor could abuse excessive RBAC privileges to create ReferenceGrant resources. These resources could then be used to create cross-namespace communication, leading to unauthorised access to the application. This could compromise the confidentiality and integrity of resources and configuration in the affected namespaces and potentially disrupt the availability of services that rely on these object references.

| A ReferenceGrant is created, which validates traffic to cross namespace trust boundaries without a valid business reason, such as a route in one tenant\'s namespace referencing a backend in another.

|Medium| Ensure that the ability to create ReferenceGrant resources is restricted to the minimum number of people. Pay special attention to ClusterRoles that allow that action. | +|EGTM-018|EGTM-GW-006|Gateway API| There is a risk that malicious requests could lead to a Denial of Service (DoS) attack, thereby reducing API gateway availability due to misconfigurations in rate-limiting or load balancing controls, or a lack of route timeout enforcement.

| Reduced API gateway availability due to an attacker\'s maliciously crafted request (e.g., QoD) potentially inducing a Denial of Service (DoS) attack.

|Medium| To ensure high availability and to mitigate potential security threats, adhere to the Envoy Gateway documentation for the configuration of a [rate-limiting](../traffic/global-rate-limit) filter and load balancing.

Further, adhere to best practices for configuring Envoy Proxy as an edge proxy documented [here](https://www.envoyproxy.io/docs/envoy/latest/configuration/best_practices/edge#configuring-envoy-as-an-edge-proxy) within the EnvoyProxy docs. This involves configuring TCP and HTTP proxies with specific settings, including restricting access to the admin endpoint, setting the [overload manager](https://www.envoyproxy.io/docs/envoy/latest/configuration/operations/overload_manager/overload_manager#config-overload-manager) and [listener](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) / [cluster](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) buffer limits, enabling [use_remote_address](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-use-remote-address), setting [connection and stream timeouts](https://www.envoyproxy.io/docs/envoy/latest/faq/configuration/timeouts#faq-configuration-timeouts), limiting [maximum concurrent streams](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-field-config-core-v3-http2protocoloptions-max-concurrent-streams), setting [initial stream window size limit](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-field-config-core-v3-http2protocoloptions-initial-stream-window-size), and configuring action on [headers_with_underscores](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-field-config-core-v3-httpprotocoloptions-headers-with-underscores-action).

[Path normalisation](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-normalize-path) should be enabled to minimise path confusion vulnerabilities. These measures help protect against volumetric threats such as Denial of Service (DoS)nattacks. Utilise custom resources to implement policy attachment, thereby exposing request limit configuration for route types. | +|EGTM-019|EGTM-DP-004|Container Security| There is a risk that replay attacks using stolen or reused JSON Web Tokens (JWTs) can compromise transmission integrity, thereby undermining the confidentiality and integrity of the data plane.

| Transmission integrity is compromised due to replay attacks using stolen or reused JSON Web Tokens (JWTs).

|Medium| Comply with JWT best practices for enhanced security, paying special attention to the use of short-lived tokens, which reduce the window of opportunity for a replay attack. The [exp](https://datatracker.ietf.org/doc/html/rfc7519#page-9) claim can be used to set token expiration times. | +|EGTM-024|EGTM-EG-008|Envoy Gateway| There is a risk of developers getting more privileges than required due to the use of SecurityPolicy, ClientTrafficPolicy, EnvoyPatchPolicy and BackendTrafficPolicy. These resources can be attached to a Gateway resource. Therefore, a developer with permission to deploy them would be able to modify a Gateway configuration by targeting the gateway in the policy manifest. This conflicts with the [Advanced 4 Tier Model](https://gateway-api.sigs.k8s.io/concepts/security-model/#write-permissions-for-advanced-4-tier-model), where developers do not have write permissions on Gateways.

| Excessive developer permissions lead to a misconfiguration and/or unauthorised access.

|Medium| Considering the Tenant C scenario (represented in the Architecture Diagram), if a developer can create SecurityPolicy, ClientTrafficPolicy, EnvoyPatchPolicy or BackendTrafficPolicy objects in namespace C, they would be able to modify a Gateway configuration by attaching the policy to the gateway. In such scenarios, it is recommended to either:

a. Create a separate namespace, where developers have no permissions, > to host tenant C\'s gateway. Note that, due to design decisions, > the > SecurityPolicy/EnvoyPatchPolicy/ClientTrafficPolicy/BackendTrafficPolicy > object can only target resources deployed in the same namespace. > Therefore, having a separate namespace for the gateway would > prevent developers from attaching the policy to the gateway.

b. Forbid the creation of these policies for developers in namespace C.

On the other hand, in scenarios similar to tenants A and B, where a shared gateway namespace is in place, this issue is more limited. Note that in this scenario, developers don\'t have access to the shared gateway namespace.

In addition, it is important to mention that EnvoyPatchPolicy resources can also be attached to GatewayClass resources. This means that, in order to comply with the Advanced 4 Tier model, individuals with the Application Administrator role should not have access to this resource either. | +|EGTM-003|EGTM-EG-001|Envoy Gateway| There is a risk that a threat actor could downgrade the security of proxied connections by configuring a weak set of cipher suites, compromising the confidentiality and integrity of proxied traffic.

| Exploit weak cipher suite configuration to downgrade security of proxied connections.

|Low| Users operating in highly regulated environments may need to tightly control the TLS protocol and associated cipher suites, blocking non-conforming incoming connections to the gateway.

EnvoyProxy bootstrap config can be customised as per the [customise EnvoyProxy](../operations/customize-envoyproxy) documentation. In addition, from v.1.0.0, it is possible to configure common TLS properties for a Gateway or XRoute through the [ClientTrafficPolicy](https://gateway.envoyproxy.io/latest/api/extension_types/#clienttrafficpolicy) object. | +|EGTM-005|EGTM-CP-002|Container Security| Threat actor who has obtained access to Envoy Gateway pod could exploit the lack of AppArmor and Seccomp profiles in the Envoy Gateway deployment to attempt a container breakout, given the presence of an exploitable vulnerability, potentially impacting the confidentiality and integrity of namespace resources.

| Unauthorised syscalls and malicious code running in the Envoy Gateway pod.

|Low| Implement [AppArmor](https://kubernetes.io/docs/tutorials/security/apparmor/) policies by setting \: \ within container.apparmor.security.beta.kubernetes.io (note, this config is set *per container*). Well-defined AppArmor policies may provide greater protection from unknown threats.

Enforce [Seccomp](https://kubernetes.io/docs/tutorials/security/seccomp/) profiles by setting the seccompProfile under securityContext. Ideally, a [fine-grained](https://kubernetes.io/docs/tutorials/security/seccomp/#create-pod-with-a-seccomp-profile-that-only-allows-necessary-syscalls) profile should be used to restrict access to only necessary syscalls, however the \--seccomp-default flag can be set to resort to [RuntimeDefault](https://kubernetes.io/docs/tutorials/security/seccomp/#create-pod-that-uses-the-container-runtime-default-seccomp-profile) which provides a container runtime specific. Example seccomp profiles can be found [here](https://kubernetes.io/docs/tutorials/security/seccomp/#download-profiles).

To further enhance pod security, consider implementing [SELinux](https://en.wikipedia.org/wiki/Security-Enhanced_Linux) via seLinuxOptions for additional syscall attack surface reduction. Setting readOnlyRootFilesystem == true enforces an immutable root filesystem, preventing the addition of malicious binaries to the PATH and increasing the attack cost. Together, these configuration items improve the pods [Security Context](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/). | +|EGTM-006|EGTM-CS-004|Container Security| There is a risk that a threat actor exploits a vulnerability in Envoy Proxy to expose a reverse shell, enabling them to compromise the confidentiality, integrity and availability of tenant data via a secondary attack.

| If an external attacker managed to exploit a vulnerability in Envoy, the presence of a shell would be greatly helpful for the attacker in terms of potentially pivoting, escalating, or establishing some form of persistence.

|Low| By default, Envoy uses a [distroless](https://github.com/GoogleContainerTools/distroless) image since v.0.6.0, which does not ship a shell. Therefore, ensure EnvoyProxy image is up-to-date and patched with the latest stable version.

If using private EnvoyProxy images, use a lightweight EnvoyProxy image without a shell or debugging tool(s) which may be useful for an attacker.

An [AuditPolicy](https://kubernetes.io/docs/tasks/debug/debug-cluster/audit/#audit-policy) (audit.k8s.io/v1beta1) can be configured to record API calls made within your cluster, allowing for identification of malicious traffic and enabling incident response. Requests are recorded based on stages which delineate between the lifecycle stage of the request made (e.g., RequestReceived, ResponseStarted, & ResponseComplete). | +|EGTM-011|EGTM-GW-003|Gateway API| There is a risk that a gateway owner (or someone with the ability to set namespace labels) maliciously or accidentally binds routes across namespace boundaries, potentially compromising the confidentiality and integrity of traffic in a multitenant scenario.

| If a Route Binding within a Gateway Listener is configured based on a custom label, it could allow a malicious internal actor with the ability to label namespaces to change the set of namespaces supported by the Gateway

|Low| Consider the use of custom admission control to restrict what labels can be set on namespaces through tooling such as [Kubewarden](https://kyverno.io/policies/pod-security/), [Kyverno](https://github.com/kubewarden), and [OPA Gatekeeper](https://github.com/open-policy-agent/gatekeeper). Route binding should follow the Kubernetes Gateway API security model, as shown [here](https://gateway-api.sigs.k8s.io/concepts/security-model/#1-route-binding), to connect gateways in different namespaces. | +|EGTM-013|EGTM-GW-005|Gateway API| There is a risk that an unauthorised actor deploys an unauthorised GatewayClass due to GatewayClass namespace validation not being configured, leading to non-compliance with business and security requirements.

| Unauthorised deployment of Gateway resource via GatewayClass template which crosses namespace trust boundaries.

|Low| Leverage GatewayClass namespace validation to limit the namespaces where GatewayClasses can be run through a tool such as using [OPA Gatekeeper](https://github.com/open-policy-agent/gatekeeper). Reference pull request \#[24](https://github.com/open-policy-agent/gatekeeper-library/pull/24) within gatekeeper-library which outlines how to add GatewayClass namespace validation through a GatewayClassNamespaces API resource kind within the constraints.gatekeeper.sh/v1beta1 apiGroup. | +|EGTM-015|EGTM-CS-007|Container Security| There is a risk that threat actors could exploit ServiceAccount tokens for illegitimate authentication, thereby leading to privilege escalation and the undermining of gateway API resources\' integrity, confidentiality, and availability.

| The threat arises from threat actors impersonating the envoy-gateway ServiceAccount through the replay of ServiceAccount tokens, thereby achieving escalated privileges and gaining unauthorised access to Kubernetes resources.

|Low| Limit the creation of ServiceAccounts to only when necessary, specifically refraining from using default service account tokens, especially for high-privilege service accounts. For legacy clusters running Kubernetes version 1.21 or earlier, note that ServiceAccount tokens are long-lived by default. To disable the automatic mounting of the service account token, set automountServiceAccountToken: false in the PodSpec. | +|EGTM-016|EGTM-EG-004|Envoy Gateway| There is a risk that threat actors establish persistence and move laterally through the cluster unnoticed due to limited visibility into access and application-level activity.

| Threat actors establish persistence and move laterally through the cluster unnoticed.

|Low| Configure [access logging](../../../contributions/design/proxy-accesslog) in the EnvoyProxy. Use [ProxyAccessLogFormatType](../../api/extension_types#proxyaccesslogformattype) (Text or JSON) to specify the log format and ensure that the logs are sent to the desired sink types by setting the [ProxyAccessLogSinkType](https://gateway.envoyproxy.io/latest/api/extension_types/#proxyaccesslogsinktype). Make use of [FileEnvoyProxyAccessLog](https://gateway.envoyproxy.io/latest/api/extension_types/#fileenvoyproxyaccesslog) or [OpenTelemetryEnvoyProxyAccessLog](https://gateway.envoyproxy.io/latest/api/extension_types/#opentelemetryenvoyproxyaccesslog) to configure File and OpenTelemetry sinks, respectively. If the settings aren\'t defined, the default format is sent to stdout.

Additionally, consider leveraging a central logging mechanism such as [Fluentd](https://github.com/fluent/fluentd) to enhance visibility into access activity and enable effective incident response (IR). | +|EGTM-017|EGTM-EG-005|Envoy Gateway| There is a risk that an insider misconfigures an envoy gateway component and goes unnoticed due to a low-touch logging configuration (via default) which responsible stakeholders are not aptly aware of or have immediate access to.

| The threat emerges from an insider misconfiguring an Envoy Gateway component without detection.

|Low| Configure the logging level of the Envoy Gateway using the \'level\' field in [EnvoyGatewayLogging](https://gateway.envoyproxy.io/latest/api/extension_types/#envoygatewaylogging). Ensure the appropriate logging levels are set for relevant components such as \'gateway-api\', \'xds-translator\', or \'global-ratelimit\'. If left unspecified, the logging level defaults to \"info\", which may not provide sufficient detail for security monitoring.

Employ a centralised logging mechanism, like [Fluentd](https://github.com/fluent/fluentd), to enhance visibility into application-level activity and to enable efficient incident response. | +|EGTM-021|EGTM-EG-006|Envoy Gateway| There is a risk that the admin interface is exposed without valid business reason, increasing the attack surface.

| Exposed admin interfaces give internal attackers the option to affect production traffic in unauthorised ways, and the option to exploit any vulnerabilities which may be present in the admin interface (e.g. by orchestrating malicious GET requests to the admin interface through CSRF, compromising Envoy Proxy global configuration or shutting off the service entirely (e.g., /quitquitquit).

|Low| The Envoy Proxy admin interface is only exposed to localhost, meaning that it is secure by default. However, due to the risk of misconfiguration, this recommendation is included.

Due to the importance of the admin interface, it is recommended to ensure that Envoy Proxies have not been accidentally misconfigured to expose the admin interface to untrusted networks. | +|EGTM-025 | EGTM-CS-011 | Container Security | The presence of a vulnerability, be it in the kernel or another system component, when coupled with containers running as root, could enable a threat actor to escape the container, thereby compromising the confidentiality, integrity, or availability of cluster resources. | The Envoy Proxy container's root-user configuration can be leveraged by an attacker to escalate privileges, execute a container breakout, and traverse across trust boundaries. | Low | By default, Envoy Gateway deployments do not use root users. Nonetheless, in case a custom image or deployment manifest is to be used, make sure Envoy Proxy pods run as a non-root user with a high UID within the container. Set runAsUser and runAsGroup security context options to specific UIDs (e.g., runAsUser: 1000 & runAsGroup: 3000) to ensure the container operates with the stipulated non-root user and group ID. If using helm chart deployment, define the user and group ID in the values.yaml file or via the command line during helm install / upgrade. | + + +## Attack Trees + +Attack trees offer a methodical way of describing the security of systems, based on varying attack patterns. It's important to approach the review of attack trees from a top-down perspective. The top node, also known as the root node, symbolises the attacker's primary objective. This goal is then broken down into subsidiary aims, each reflecting a different strategy to attain the root objective. This deconstruction persists until reaching the lowest level objectives or 'leaf nodes', which depict attacks that can be directly launched. + +It is essential to note that attack trees presented here are speculative paths for potential exploitation. The Envoy Gateway project is in a continuous development cycle, and as the project evolves, new vulnerabilities may be exposed, or additional controls could be introduced. Therefore, the threats illustrated in the attack trees should be perceived as point-in-time reflections of the project’s current state at the time of writing this threat model. + +### Node ID Schema + +Each node in the attack tree is assigned a unique identifier following the AT#-## schema. This allows easy reference to specific nodes in the attack trees throughout the threat model. The first part of the ID (AT#) signifies the attack tree number, while the second part (##) represents the node number within that tree. + +### Logical Operators + +Logical AND/OR operators are used to represent the relationship between parent and child nodes. An AND operator means that all child nodes must be achieved to satisfy the parent node. An OR operator between a parent node and its child nodes means that any of the child nodes can be achieved to satisfy the parent node. + +### Attack Tree Node Legend + +![AT Legend](/img/AT-legend.png) + +### AT0 + +![AT0](/img/AT0.png) + +### AT1 + +![AT1](/img/AT1.png) + +### AT2 + +![AT2](/img/AT2.png) + +### AT3 + +![AT3](/img/AT3.png) + +### AT4 + +![AT4](/img/AT4.png) + +### AT5 + +![AT5](/img/AT5.png) + +### AT6 + +![AT6](/img/AT6.png) diff --git a/site/content/en/docs/tasks/security/tls-cert-manager.md b/site/content/en/docs/tasks/security/tls-cert-manager.md new file mode 100644 index 00000000000..d51fa469e8c --- /dev/null +++ b/site/content/en/docs/tasks/security/tls-cert-manager.md @@ -0,0 +1,436 @@ +--- +title: "Using cert-manager For TLS Termination" +--- + +This task shows how to set up [cert-manager](https://cert-manager.io/) to automatically create certificates and secrets for use by Envoy Gateway. +It will first show how to enable the self-sign issuer, which is useful to test that cert-manager and Envoy Gateway can talk to each other. +Then it shows how to use [Let's Encrypt's staging environment](https://letsencrypt.org/docs/staging-environment/). +Changing to the Let's Encrypt production environment is straight-forward after that. + +## Prerequisites + +* A Kubernetes cluster and a configured `kubectl`. +* The `helm` command. +* The `curl` command or similar for testing HTTPS requests. +* For the ACME HTTP-01 challenge to work + * your Gateway must be reachable on the public Internet. + * the domain name you use (we use `www.example.com`) must point to the Gateway's external IP(s). + +## Installation + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## Deploying cert-manager + +*This is a summary of [cert-manager Installation with Helm](https://cert-manager.io/docs/installation/helm/).* + +Installing cert-manager is straight-forward, but currently (v1.12) requires setting a feature gate to enable the Gateway API support. + +```console +$ helm repo add jetstack https://charts.jetstack.io +$ helm upgrade --install --create-namespace --namespace cert-manager --set installCRDs=true --set featureGates=ExperimentalGatewayAPISupport=true cert-manager jetstack/cert-manager +``` + +You should now have `cert-manager` running with nothing to do: + +```console +$ kubectl wait --for=condition=Available deployment -n cert-manager --all +deployment.apps/cert-manager condition met +deployment.apps/cert-manager-cainjector condition met +deployment.apps/cert-manager-webhook condition met + +$ kubectl get -n cert-manager deployment +NAME READY UP-TO-DATE AVAILABLE AGE +cert-manager 1/1 1 1 42m +cert-manager-cainjector 1/1 1 1 42m +cert-manager-webhook 1/1 1 1 42m +``` + +## A Self-Signing Issuer + +cert-manager can have any number of *issuer* configurations. +The simplest issuer type is [SelfSigned](https://cert-manager.io/docs/configuration/selfsigned/). +It simply takes the certificate request and signs it with the private key it generates for the TLS Secret. + +``` +Self-signed certificates don't provide any help in establishing trust between certificates. +However, they are great for initial testing, due to their simplicity. +``` + +To install self-signing, run + +```console +$ kubectl apply -f - <}} +{{% tab header="With External LoadBalancer Support" %}} + +You can also test the same functionality by sending traffic to the External IP of the Gateway: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +Curl the example app through the Gateway, e.g. Envoy proxy: + +```shell +curl -v -HHost:passthrough.example.com --resolve "passthrough.example.com:6443:${GATEWAY_HOST}" \ +--cacert example.com.crt https://passthrough.example.com:6443/get +``` + +{{% /tab %}} +{{% tab header="Without LoadBalancer Support" %}} + +Get the name of the Envoy service created the by the example Gateway: + +```shell +export ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}') +``` + +Port forward to the Envoy service: + +```shell +kubectl -n envoy-gateway-system port-forward service/${ENVOY_SERVICE} 6043:6443 & +``` + +Curl the example app through Envoy proxy: + +```shell +curl -v --resolve "passthrough.example.com:6043:127.0.0.1" https://passthrough.example.com:6043 \ +--cacert passthrough.example.com.crt +``` + +{{% /tab %}} +{{< /tabpane >}} + +## Clean-Up + +Follow the steps from the [Quickstart](../../quickstart) to uninstall Envoy Gateway and the example manifest. + +Delete the Secret: + +```shell +kubectl delete secret/server-certs +``` + +## Next Steps + +Checkout the [Developer Guide](../../../contributions/develop) to get involved in the project. diff --git a/site/content/en/docs/tasks/security/tls-termination.md b/site/content/en/docs/tasks/security/tls-termination.md new file mode 100644 index 00000000000..e4534dd57e1 --- /dev/null +++ b/site/content/en/docs/tasks/security/tls-termination.md @@ -0,0 +1,91 @@ +--- +title: "TLS Termination for TCP" +--- + +This task will walk through the steps required to configure TLS Terminate mode for TCP traffic via Envoy Gateway. +This task uses a self-signed CA, so it should be used for testing and demonstration purposes only. + +## Prerequisites + +- OpenSSL to generate TLS assets. + +## Installation + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway. + +## TLS Certificates +Generate the certificates and keys used by the Gateway to terminate client TLS connections. + +Create a root certificate and private key to sign certificates: + +```shell +openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -subj '/O=example Inc./CN=example.com' -keyout example.com.key -out example.com.crt +``` + +Create a certificate and a private key for `www.example.com`: + +```shell +openssl req -out www.example.com.csr -newkey rsa:2048 -nodes -keyout www.example.com.key -subj "/CN=www.example.com/O=example organization" +openssl x509 -req -days 365 -CA example.com.crt -CAkey example.com.key -set_serial 0 -in www.example.com.csr -out www.example.com.crt +``` + +Store the cert/key in a Secret: + +```shell +kubectl create secret tls example-cert --key=www.example.com.key --cert=www.example.com.crt +``` + +Install the TLS Termination for TCP example resources: + +```shell +kubectl apply -f https://raw.githubusercontent.com/envoyproxy/gateway/latest/examples/kubernetes/tls-termination.yaml +``` + +Verify the Gateway status: + +```shell +kubectl get gateway/eg -o yaml +``` + +## Testing + +{{< tabpane text=true >}} +{{% tab header="With External LoadBalancer Support" %}} + +Get the External IP of the Gateway: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +Query the example app through the Gateway: + +```shell +curl -v -HHost:www.example.com --resolve "www.example.com:443:${GATEWAY_HOST}" \ +--cacert example.com.crt https://www.example.com/get +``` + +{{% /tab %}} +{{% tab header="Without LoadBalancer Support" %}} + +Get the name of the Envoy service created the by the example Gateway: + +```shell +export ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}') +``` + +Port forward to the Envoy service: + +```shell +kubectl -n envoy-gateway-system port-forward service/${ENVOY_SERVICE} 8443:443 & +``` + +Query the example app through Envoy proxy: + +```shell +curl -v -HHost:www.example.com --resolve "www.example.com:8443:127.0.0.1" \ +--cacert example.com.crt https://www.example.com:8443/get +``` + +{{% /tab %}} +{{< /tabpane >}} diff --git a/site/content/en/v1.0.2/tasks/traffic/_index.md b/site/content/en/docs/tasks/traffic/_index.md similarity index 100% rename from site/content/en/v1.0.2/tasks/traffic/_index.md rename to site/content/en/docs/tasks/traffic/_index.md diff --git a/site/content/en/docs/tasks/traffic/backend.md b/site/content/en/docs/tasks/traffic/backend.md new file mode 100644 index 00000000000..02de7161fe2 --- /dev/null +++ b/site/content/en/docs/tasks/traffic/backend.md @@ -0,0 +1,211 @@ +--- +title: "Backend Routing" +--- + +Envoy Gateway supports routing to native K8s resources such as `Service` and `ServiceImport`. The `Backend` API is a custom Envoy Gateway [extension resource][] that can used in Gateway-API [BackendObjectReference][]. + +## Motivation +The Backend API was added to support several use cases: +- Allowing users to integrate Envoy with services (Ext Auth, Rate Limit, ALS, ...) using Unix Domain Sockets, which are currently not supported by K8s. +- Simplify [routing to cluster-external backends][], which currently requires users to maintain both K8s `Service` and `EndpointSlice` resources. + +## Warning + +Similar to the K8s EndpointSlice API, the Backend API can be misused to allow traffic to be sent to otherwise restricted destinations, as described in [CVE-2021-25740][]. +A Backend resource can be used to: +- Expose a Service or Pod that should not be accessible +- Reference a Service or Pod by a Route without appropriate Reference Grants +- Expose the Envoy Proxy localhost (including the Envoy admin endpoint) + +For these reasons, the Backend API is disabled by default in Envoy Gateway configuration. Envoy Gateway admins are advised to follow [upstream recommendations][] and restrict access to the Backend API using K8s RBAC. + +## Restrictions + +The Backend API is currently supported only in the following BackendReferences: +- [HTTPRoute]: IP and FQDN endpoints +- [Envoy Extension Policy] (ExtProc): IP, FQDN and unix domain socket endpoints + +The Backend API supports attachment the following policies: +- [Backend TLS Policy][] + +Certain restrictions apply on the value of hostnames and addresses. For example, the loopback IP address range and the localhost hostname are forbidden. + +Envoy Gateway does not manage the lifecycle of unix domain sockets referenced by the Backend resource. Envoy Gateway admins are responsible for creating and mounting the sockets into the envoy proxy pod. The latter can be achieved by patching the envoy deployment using the [EnvoyProxy][] resource. + +## Quickstart + +### Prerequisites + +* Follow the steps from the [Quickstart](../../quickstart) task to install Envoy Gateway and the example manifest. + Before proceeding, you should be able to query the example backend using HTTP. + +### Enable Backend + +* By default [Backend][] is disabled. Lets enable it in the [EnvoyGateway][] startup configuration + +* The default installation of Envoy Gateway installs a default [EnvoyGateway][] configuration and attaches it + using a `ConfigMap`. In the next step, we will update this resource to enable Backend. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +* After updating the `ConfigMap`, you will need to restart the `envoy-gateway` deployment so the configuration kicks in + +```shell +kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system +``` + +## Testing + +### Route to External Backend + +* In the following example, we will create a `Backend` resource that routes to httpbin.org:80 and a `HTTPRoute` that references this backend. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Get the Gateway address: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +Send a request and view the response: + +```shell +curl -I -HHost:www.example.com http://${GATEWAY_HOST}/headers +``` + +[Backend]: ../../../api/extension_types#backend +[routing to cluster-external backends]: ./../../tasks/traffic/routing-outside-kubernetes.md +[BackendObjectReference]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.BackendObjectReference +[extension resource]: https://gateway-api.sigs.k8s.io/guides/migrating-from-ingress/#approach-to-extensibility +[CVE-2021-25740]: https://nvd.nist.gov/vuln/detail/CVE-2021-25740 +[upstream recommendations]: https://github.com/kubernetes/kubernetes/issues/103675 +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute +[Envoy Extension Policy]: ../../../api/extension_types#envoyextensionpolicy +[Backend TLS Policy]: https://gateway-api.sigs.k8s.io/api-types/backendtlspolicy/ +[EnvoyProxy]: ../../../api/extension_types#envoyproxy +[EnvoyGateway]: ../../../api/extension_types#envoygateway diff --git a/site/content/en/docs/tasks/traffic/circuit-breaker.md b/site/content/en/docs/tasks/traffic/circuit-breaker.md new file mode 100644 index 00000000000..9480a86a2cd --- /dev/null +++ b/site/content/en/docs/tasks/traffic/circuit-breaker.md @@ -0,0 +1,150 @@ +--- +title: "Circuit Breakers" +--- + +[Envoy circuit breakers] can be used to fail quickly and apply back-pressure in response to upstream service degradation. + +Envoy Gateway supports the following circuit breaker thresholds: +- **Concurrent Connections**: limit the connections that Envoy can establish to the upstream service. When this threshold is met, new connections will not be established, and some requests will be queued until an existing connection becomes available. +- **Concurrent Requests**: limit on concurrent requests in-flight from Envoy to the upstream service. When this threshold is met, requests will be queued. +- **Pending Requests**: limit the pending request queue size. When this threshold is met, overflowing requests will be terminated with a `503` status code. + +Envoy's circuit breakers are distributed: counters are not synchronized across different Envoy processes. The default Envoy and Envoy Gateway circuit breaker threshold values (1024) may be too strict for high-throughput systems. + +Envoy Gateway introduces a new CRD called [BackendTrafficPolicy][] that allows the user to describe their desired circuit breaker thresholds. +This instantiated resource can be linked to a [Gateway][], [HTTPRoute][] or [GRPCRoute][] resource. + +**Note**: There are distinct circuit breaker counters for each `BackendReference` in an `xRoute` rule. Even if a `BackendTrafficPolicy` targets a `Gateway`, each `BackendReference` in that gateway still has separate circuit breaker counter. + +## Prerequisites + +### Install Envoy Gateway + +* Follow the installation step from the [Quickstart](../../quickstart) to install Envoy Gateway and sample resources. + +### Install the hey load testing tool +* The `hey` CLI will be used to generate load and measure response times. Follow the installation instruction from the [Hey project] docs. + +## Test and customize circuit breaker settings + +This example will simulate a degraded backend that responds within 10 seconds by adding the `?delay=10s` query parameter to API calls. The hey tool will be used to generate 100 concurrent requests. + +```shell +hey -n 100 -c 100 -host "www.example.com" http://${GATEWAY_HOST}/?delay=10s +``` + +```console +Summary: + Total: 10.3426 secs + Slowest: 10.3420 secs + Fastest: 10.0664 secs + Average: 10.2145 secs + Requests/sec: 9.6687 + + Total data: 36600 bytes + Size/request: 366 bytes + +Response time histogram: + 10.066 [1] |■■■■ + 10.094 [4] |■■■■■■■■■■■■■■■ + 10.122 [9] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 10.149 [10] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 10.177 [10] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 10.204 [11] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 10.232 [11] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 10.259 [11] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 10.287 [11] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 10.314 [11] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 10.342 [11] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ +``` + +The default circuit breaker threshold (1024) is not met. As a result, requests do not overflow: all requests are proxied upstream and both Envoy and clients wait for 10s. + +In order to fail fast, apply a `BackendTrafficPolicy` that limits concurrent requests to 10 and pending requests to 0. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Execute the load simulation again. + +```shell +hey -n 100 -c 100 -host "www.example.com" http://${GATEWAY_HOST}/?delay=10s +``` + +```console +Summary: + Total: 10.1230 secs + Slowest: 10.1224 secs + Fastest: 0.0529 secs + Average: 1.0677 secs + Requests/sec: 9.8785 + + Total data: 10940 bytes + Size/request: 109 bytes + +Response time histogram: + 0.053 [1] | + 1.060 [89] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 2.067 [0] | + 3.074 [0] | + 4.081 [0] | + 5.088 [0] | + 6.095 [0] | + 7.102 [0] | + 8.109 [0] | + 9.115 [0] | + 10.122 [10] |■■■■ +``` + +With the new circuit breaker settings, and due to the slowness of the backend, only the first 10 concurrent requests were proxied, while the other 90 overflowed. +* Overflowing Requests failed fast, reducing proxy resource consumption. +* Upstream traffic was limited, alleviating the pressure on the degraded service. + +[Envoy Circuit Breakers]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/circuit_breaking +[BackendTrafficPolicy]: ../../../api/extension_types#backendtrafficpolicy +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway/ +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute/ +[GRPCRoute]: https://gateway-api.sigs.k8s.io/api-types/grpcroute/ +[Hey project]: https://github.com/rakyll/hey diff --git a/site/content/en/docs/tasks/traffic/client-traffic-policy.md b/site/content/en/docs/tasks/traffic/client-traffic-policy.md new file mode 100644 index 00000000000..37d85e5f8e1 --- /dev/null +++ b/site/content/en/docs/tasks/traffic/client-traffic-policy.md @@ -0,0 +1,688 @@ +--- +title: "Client Traffic Policy" +--- + +This task explains the usage of the [ClientTrafficPolicy][] API. + + +## Introduction + +The [ClientTrafficPolicy][] API allows system administrators to configure +the behavior for how the Envoy Proxy server behaves with downstream clients. + +## Motivation + +This API was added as a new policy attachment resource that can be applied to Gateway resources and it is meant to hold settings for configuring behavior of the connection between the downstream client and Envoy Proxy listener. It is the counterpart to the [BackendTrafficPolicy][] API resource. + +## Quickstart + +### Prerequisites + +* Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +### Support TCP keepalive for downstream client + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify that ClientTrafficPolicy is Accepted: + +```shell +kubectl get clienttrafficpolicies.gateway.envoyproxy.io -n default +``` + +You should see the policy marked as accepted like this: + +```shell +NAME STATUS AGE +enable-tcp-keepalive-policy Accepted 5s +``` + +Curl the example app through Envoy proxy once again: + +```shell +curl --verbose --header "Host: www.example.com" http://$GATEWAY_HOST/get --next --header "Host: www.example.com" http://$GATEWAY_HOST/get +``` + +You should see the output like this: + +```shell +* Trying 172.18.255.202:80... +* Connected to 172.18.255.202 (172.18.255.202) port 80 (#0) +> GET /get HTTP/1.1 +> Host: www.example.com +> User-Agent: curl/8.1.2 +> Accept: */* +> +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< date: Fri, 01 Dec 2023 10:17:04 GMT +< content-length: 507 +< x-envoy-upstream-service-time: 0 +< server: envoy +< +{ + "path": "/get", + "host": "www.example.com", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/8.1.2" + ], + "X-Envoy-Expected-Rq-Timeout-Ms": [ + "15000" + ], + "X-Envoy-Internal": [ + "true" + ], + "X-Forwarded-For": [ + "172.18.0.2" + ], + "X-Forwarded-Proto": [ + "http" + ], + "X-Request-Id": [ + "4d0d33e8-d611-41f0-9da0-6458eec20fa5" + ] + }, + "namespace": "default", + "ingress": "", + "service": "", + "pod": "backend-58d58f745-2zwvn" +* Connection #0 to host 172.18.255.202 left intact +}* Found bundle for host: 0x7fb9f5204ea0 [serially] +* Can not multiplex, even if we wanted to +* Re-using existing connection #0 with host 172.18.255.202 +> GET /headers HTTP/1.1 +> Host: www.example.com +> User-Agent: curl/8.1.2 +> Accept: */* +> +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< date: Fri, 01 Dec 2023 10:17:04 GMT +< content-length: 511 +< x-envoy-upstream-service-time: 0 +< server: envoy +< +{ + "path": "/headers", + "host": "www.example.com", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/8.1.2" + ], + "X-Envoy-Expected-Rq-Timeout-Ms": [ + "15000" + ], + "X-Envoy-Internal": [ + "true" + ], + "X-Forwarded-For": [ + "172.18.0.2" + ], + "X-Forwarded-Proto": [ + "http" + ], + "X-Request-Id": [ + "9a8874c0-c117-481c-9b04-933571732ca5" + ] + }, + "namespace": "default", + "ingress": "", + "service": "", + "pod": "backend-58d58f745-2zwvn" +* Connection #0 to host 172.18.255.202 left intact +} +``` + +You can see keepalive connection marked by the output in: + +```shell +* Connection #0 to host 172.18.255.202 left intact +* Re-using existing connection #0 with host 172.18.255.202 +``` + +### Enable Proxy Protocol for downstream client + +This example configures Proxy Protocol for downstream clients. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify that ClientTrafficPolicy is Accepted: + +```shell +kubectl get clienttrafficpolicies.gateway.envoyproxy.io -n default +``` + +You should see the policy marked as accepted like this: + +```shell +NAME STATUS AGE +enable-proxy-protocol-policy Accepted 5s +``` + +Try the endpoint without using PROXY protocol with curl: + +```shell +curl -v --header "Host: www.example.com" http://$GATEWAY_HOST/get +``` + +```shell +* Trying 172.18.255.202:80... +* Connected to 172.18.255.202 (172.18.255.202) port 80 (#0) +> GET /get HTTP/1.1 +> Host: www.example.com +> User-Agent: curl/8.1.2 +> Accept: */* +> +* Recv failure: Connection reset by peer +* Closing connection 0 +curl: (56) Recv failure: Connection reset by peer +``` + +Curl the example app through Envoy proxy once again, now sending HAProxy PROXY protocol header at the beginning of the connection with --haproxy-protocol flag: + +```shell +curl --verbose --haproxy-protocol --header "Host: www.example.com" http://$GATEWAY_HOST/get +``` + +You should now expect 200 response status and also see that source IP was preserved in the X-Forwarded-For header. + +```shell +* Trying 172.18.255.202:80... +* Connected to 172.18.255.202 (172.18.255.202) port 80 (#0) +> GET /get HTTP/1.1 +> Host: www.example.com +> User-Agent: curl/8.1.2 +> Accept: */* +> +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< date: Mon, 04 Dec 2023 21:11:43 GMT +< content-length: 510 +< x-envoy-upstream-service-time: 0 +< server: envoy +< +{ + "path": "/get", + "host": "www.example.com", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/8.1.2" + ], + "X-Envoy-Expected-Rq-Timeout-Ms": [ + "15000" + ], + "X-Envoy-Internal": [ + "true" + ], + "X-Forwarded-For": [ + "192.168.255.6" + ], + "X-Forwarded-Proto": [ + "http" + ], + "X-Request-Id": [ + "290e4b61-44b7-4e5c-a39c-0ec76784e897" + ] + }, + "namespace": "default", + "ingress": "", + "service": "", + "pod": "backend-58d58f745-2zwvn" +* Connection #0 to host 172.18.255.202 left intact +} +``` + +### Configure Client IP Detection + +This example configures the number of additional ingress proxy hops from the right side of XFF HTTP headers to trust when determining the origin client's IP address and determines whether or not `x-forwarded-proto` headers will be trusted. Refer to https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers#x-forwarded-for for details. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify that ClientTrafficPolicy is Accepted: + +```shell +kubectl get clienttrafficpolicies.gateway.envoyproxy.io -n default +``` + +You should see the policy marked as accepted like this: + +```shell +NAME STATUS AGE +http-client-ip-detection Accepted 5s +``` + +Open port-forward to the admin interface port: + +```shell +kubectl port-forward deploy/${ENVOY_DEPLOYMENT} -n envoy-gateway-system 19000:19000 +``` + +Curl the admin interface port to fetch the configured value for `xff_num_trusted_hops`: + +```shell +curl -s 'http://localhost:19000/config_dump?resource=dynamic_listeners' \ + | jq -r '.configs[0].active_state.listener.default_filter_chain.filters[0].typed_config + | with_entries(select(.key | match("xff|remote_address|original_ip")))' +``` + +You should expect to see the following: + +```json +{ + "use_remote_address": true, + "xff_num_trusted_hops": 2 +} +``` + +Curl the example app through Envoy proxy: + +```shell +curl -v http://$GATEWAY_HOST/get \ + -H "Host: www.example.com" \ + -H "X-Forwarded-Proto: https" \ + -H "X-Forwarded-For: 1.1.1.1,2.2.2.2" +``` + +You should expect 200 response status, see that `X-Forwarded-Proto` was preserved and `X-Envoy-External-Address` was set to the leftmost address in the `X-Forwarded-For` header: + +```shell +* Trying [::1]:8888... +* Connected to localhost (::1) port 8888 +> GET /get HTTP/1.1 +> Host: www.example.com +> User-Agent: curl/8.4.0 +> Accept: */* +> X-Forwarded-Proto: https +> X-Forwarded-For: 1.1.1.1,2.2.2.2 +> +Handling connection for 8888 +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< date: Tue, 30 Jan 2024 15:19:22 GMT +< content-length: 535 +< x-envoy-upstream-service-time: 0 +< server: envoy +< +{ + "path": "/get", + "host": "www.example.com", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/8.4.0" + ], + "X-Envoy-Expected-Rq-Timeout-Ms": [ + "15000" + ], + "X-Envoy-External-Address": [ + "1.1.1.1" + ], + "X-Forwarded-For": [ + "1.1.1.1,2.2.2.2,10.244.0.9" + ], + "X-Forwarded-Proto": [ + "https" + ], + "X-Request-Id": [ + "53ccfad7-1899-40fa-9322-ddb833aa1ac3" + ] + }, + "namespace": "default", + "ingress": "", + "service": "", + "pod": "backend-58d58f745-8psnc" +* Connection #0 to host localhost left intact +} +``` + +### Enable HTTP Request Received Timeout + +This feature allows you to limit the time taken by the Envoy Proxy fleet to receive the entire request from the client, which is useful in preventing certain clients from consuming too much memory in Envoy +This example configures the HTTP request timeout for the client, please check out the details [here](https://www.envoyproxy.io/docs/envoy/latest/faq/configuration/timeouts#stream-timeouts). + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Curl the example app through Envoy proxy: + +```shell +curl -v http://$GATEWAY_HOST/get \ + -H "Host: www.example.com" \ + -H "Content-Length: 10000" +``` + +You should expect `428` response status after 2s: + +```shell +curl -v http://$GATEWAY_HOST/get \ + -H "Host: www.example.com" \ + -H "Content-Length: 10000" +* Trying 172.18.255.200:80... +* Connected to 172.18.255.200 (172.18.255.200) port 80 +> GET /get HTTP/1.1 +> Host: www.example.com +> User-Agent: curl/8.4.0 +> Accept: */* +> Content-Length: 10000 +> +< HTTP/1.1 408 Request Timeout +< content-length: 15 +< content-type: text/plain +< date: Tue, 27 Feb 2024 07:38:27 GMT +< connection: close +< +* Closing connection +request timeout +``` + +### Configure Client HTTP Idle Timeout + +The idle timeout is defined as the period in which there are no active requests. When the idle timeout is reached the connection will be closed. +For more details see [here](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-field-config-core-v3-httpprotocoloptions-idle-timeout:~:text=...%7D%0A%7D-,idle_timeout,-(Duration)%20The). + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Curl the example app through Envoy proxy: + +```shell +openssl s_client -crlf -connect $GATEWAY_HOST:443 +``` + +You should expect the connection to be closed after 5s. + +You can also check the number of connections closed due to idle timeout by using the following query: + +```shell +envoy_http_downstream_cx_idle_timeout{envoy_http_conn_manager_prefix=""} +``` + +The number of connections closed due to idle timeout should be increased by 1. + + +### Configure Downstream Per Connection Buffer Limit + +This feature allows you to set a soft limit on size of the listener’s new connection read and write buffers. +The size is configured using the `resource.Quantity` format, see examples [here](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#meaning-of-memory). + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +[ClientTrafficPolicy]: ../../../api/extension_types#clienttrafficpolicy +[BackendTrafficPolicy]: ../../../api/extension_types#backendtrafficpolicy diff --git a/site/content/en/docs/tasks/traffic/connection-limit.md b/site/content/en/docs/tasks/traffic/connection-limit.md new file mode 100644 index 00000000000..aac8437dc45 --- /dev/null +++ b/site/content/en/docs/tasks/traffic/connection-limit.md @@ -0,0 +1,137 @@ +--- +title: "Connection Limit" +--- + +The connection limit features allows users to limit the number of concurrently active TCP connections on a [Gateway][] or a [Listener][]. +When the [connection limit][] is reached, new connections are closed immediately by Envoy proxy. It's possible to configure a delay for connection rejection. + +Users may want to limit the number of connections for several reasons: +* Protect resources like CPU and Memory. +* Ensure that different listeners can receive a fair share of global resources. +* Protect from malicious activity like DoS attacks. + +Envoy Gateway introduces a new CRD called [Client Traffic Policy][] that allows the user to describe their desired connection limit settings. +This instantiated resource can be linked to a [Gateway][]. + +The Envoy [connection limit][] implementation is distributed: counters are not synchronized between different envoy proxies. + +When a [Client Traffic Policy][] is attached to a gateway, the connection limit will apply differently based on the +[Listener][] protocol in use: +- HTTP: all HTTP listeners in a [Gateway][] will share a common connection counter, and a limit defined by the policy. +- HTTPS/TLS: each HTTPS/TLS listener will have a dedicated connection counter, and a limit defined by the policy. + + +## Prerequisites + +### Install Envoy Gateway + +* Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the HTTPRoute example manifest. + Before proceeding, you should be able to query the example backend using HTTP. + +### Install the hey load testing tool +* The `hey` CLI will be used to generate load and measure response times. Follow the installation instruction from the [Hey project] docs. + +## Test and customize connection limit settings + +This example we use `hey` to open 10 connections and execute 1 RPS per connection for 10 seconds. + +```shell +hey -c 10 -q 1 -z 10s -host "www.example.com" http://${GATEWAY_HOST}/get +``` + +```console +Summary: + Total: 10.0058 secs + Slowest: 0.0275 secs + Fastest: 0.0029 secs + Average: 0.0111 secs + Requests/sec: 9.9942 + +[...] + +Status code distribution: + [200] 100 responses +``` + +There are no connection limits, and so all 100 requests succeed. + +Next, we apply a limit of 5 connections. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Execute the load simulation again. + +```shell +hey -c 10 -q 1 -z 10s -host "www.example.com" http://${GATEWAY_HOST}/get +``` + +```console +Summary: + Total: 11.0327 secs + Slowest: 0.0361 secs + Fastest: 0.0013 secs + Average: 0.0088 secs + Requests/sec: 9.0640 + +[...] + +Status code distribution: + [200] 50 responses + +Error distribution: + [50] Get "http://localhost:8888/get": EOF +``` + +With the new connection limit, only 5 of 10 connections are established, and so only 50 requests succeed. + + +[Client Traffic Policy]: ../../../api/extension_types#clienttrafficpolicy +[Hey project]: https://github.com/rakyll/hey +[connection limit]: https://www.envoyproxy.io/docs/envoy/latest/configuration/listeners/network_filters/connection_limit_filter +[listener]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Listener +[gateway]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.Gateway diff --git a/site/content/en/docs/tasks/traffic/fault-injection.md b/site/content/en/docs/tasks/traffic/fault-injection.md new file mode 100644 index 00000000000..d4f536dbb33 --- /dev/null +++ b/site/content/en/docs/tasks/traffic/fault-injection.md @@ -0,0 +1,381 @@ +--- +title: "Fault Injection" +--- + +[Envoy fault injection] can be used to inject delays and abort requests to mimic failure scenarios such as service failures and overloads. + +Envoy Gateway supports the following fault scenarios: +- **delay fault**: inject a custom fixed delay into the request with a certain probability to simulate delay failures. +- **abort fault**: inject a custom response code into the response with a certain probability to simulate abort failures. + +Envoy Gateway introduces a new CRD called [BackendTrafficPolicy][] that allows the user to describe their desired fault scenarios. +This instantiated resource can be linked to a [Gateway][], [HTTPRoute][] or [GRPCRoute][] resource. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +For GRPC - follow the steps from the [GRPC Routing](../grpc-routing) example. +Before proceeding, you should be able to query the example backend using HTTP or GRPC. + +### Install the hey load testing tool +* The `hey` CLI will be used to generate load and measure response times. Follow the installation instruction from the [Hey project] docs. + +## Configuration + +Allow requests with a valid faultInjection by creating an [BackendTrafficPolicy][BackendTrafficPolicy] and attaching it to the example HTTPRoute or GRPCRoute. + +### HTTPRoute + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + + +Two HTTPRoute resources were created, one for `/foo` and another for `/bar`. `fault-injection-abort` BackendTrafficPolicy has been created and targeted HTTPRoute foo to abort requests for `/foo`. `fault-injection-delay` BackendTrafficPolicy has been created and targeted HTTPRoute foo to delay `2s` requests for `/bar`. + +Verify the HTTPRoute configuration and status: + +```shell +kubectl get httproute/foo -o yaml +kubectl get httproute/bar -o yaml +``` + +Verify the BackendTrafficPolicy configuration: + +```shell +kubectl get backendtrafficpolicy/fault-injection-50-percent-abort -o yaml +kubectl get backendtrafficpolicy/fault-injection-delay -o yaml +``` + +### GRPCRoute + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +A BackendTrafficPolicy has been created and targeted GRPCRoute yages to abort requests for `yages` service.. + +Verify the GRPCRoute configuration and status: + +```shell +kubectl get grpcroute/yages -o yaml +``` + +Verify the SecurityPolicy configuration: + +```shell +kubectl get backendtrafficpolicy/fault-injection-abort -o yaml +``` + +## Testing + +Ensure the `GATEWAY_HOST` environment variable from the [Quickstart](../../quickstart) is set. If not, follow the +Quickstart instructions to set the variable. + +```shell +echo $GATEWAY_HOST +``` + +### HTTPRoute + +Verify that requests to `foo` route are aborted. + +```shell +hey -n 1000 -c 100 -host "www.example.com" http://${GATEWAY_HOST}/foo +``` + +```console +Status code distribution: + [200] 501 responses + [501] 499 responses +``` + +Verify that requests to `bar` route are delayed. + +```shell +hey -n 1000 -c 100 -host "www.example.com" http://${GATEWAY_HOST}/bar +``` + +```console +Summary: + Total: 20.1493 secs + Slowest: 2.1020 secs + Fastest: 1.9940 secs + Average: 2.0123 secs + Requests/sec: 49.6295 + + Total data: 557000 bytes + Size/request: 557 bytes + +Response time histogram: + 1.994 [1] | + 2.005 [475] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 2.016 [419] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 2.026 [5] | + 2.037 [0] | + 2.048 [0] | + 2.059 [30] |■■■ + 2.070 [0] | + 2.080 [0] | + 2.091 [11] |■ + 2.102 [59] |■■■■■ +``` + +### GRPCRoute + +Verify that requests to `yages`service are aborted. + +```shell +grpcurl -plaintext -authority=grpc-example.com ${GATEWAY_HOST}:80 yages.Echo/Ping +``` + +You should see the below response + +```shell +Error invoking method "yages.Echo/Ping": rpc error: code = Unavailable desc = failed to query for service descriptor "yages.Echo": fault filter abort +``` + +## Clean-Up + +Follow the steps from the [Quickstart](../../quickstart) to uninstall Envoy Gateway and the example manifest. + +Delete the BackendTrafficPolicy: + +```shell +kubectl delete BackendTrafficPolicy/fault-injection-abort +``` + +[Envoy fault injection]: https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/fault_filter.html +[BackendTrafficPolicy]: ../../../api/extension_types#backendtrafficpolicy +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway/ +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute/ +[GRPCRoute]: https://gateway-api.sigs.k8s.io/api-types/grpcroute/ +[Hey project]: https://github.com/rakyll/hey diff --git a/site/content/en/v1.0.2/tasks/traffic/gateway-address.md b/site/content/en/docs/tasks/traffic/gateway-address.md similarity index 100% rename from site/content/en/v1.0.2/tasks/traffic/gateway-address.md rename to site/content/en/docs/tasks/traffic/gateway-address.md diff --git a/site/content/en/docs/tasks/traffic/gatewayapi-support.md b/site/content/en/docs/tasks/traffic/gatewayapi-support.md new file mode 100644 index 00000000000..779cce6fb12 --- /dev/null +++ b/site/content/en/docs/tasks/traffic/gatewayapi-support.md @@ -0,0 +1,120 @@ +--- +title: "Gateway API Support" +--- + +As mentioned in the [system design][] document, Envoy Gateway's managed data plane is configured dynamically through +Kubernetes resources, primarily [Gateway API][] objects. Envoy Gateway supports configuration using the following Gateway API resources. + +## GatewayClass + +A [GatewayClass][] represents a "class" of gateways, i.e. which Gateways should be managed by Envoy Gateway. +Envoy Gateway supports managing __a single__ GatewayClass resource that matches its configured `controllerName` and +follows Gateway API guidelines for [resolving conflicts][] when multiple GatewayClasses exist with a matching +`controllerName`. + +__Note:__ If specifying GatewayClass [parameters reference][], it must refer to an [EnvoyProxy][] resource. + +## Gateway + +When a [Gateway][] resource is created that references the managed GatewayClass, Envoy Gateway will create and manage a +new Envoy Proxy deployment. Gateway API resources that reference this Gateway will configure this managed Envoy Proxy +deployment. + +## HTTPRoute + +A [HTTPRoute][] configures routing of HTTP traffic through one or more Gateways. The following HTTPRoute filters are +supported by Envoy Gateway: + +- `requestHeaderModifier`: [RequestHeaderModifiers][http-filter] + can be used to modify or add request headers before the request is proxied to its destination. +- `responseHeaderModifier`: [ResponseHeaderModifiers][http-filter] + can be used to modify or add response headers before the response is sent back to the client. +- `requestMirror`: [RequestMirrors][http-filter] + configure destinations where the requests should also be mirrored to. Responses to mirrored requests will be ignored. +- `requestRedirect`: [RequestRedirects][http-filter] + configure policied for how requests that match the HTTPRoute should be modified and then redirected. +- `urlRewrite`: [UrlRewrites][http-filter] + allow for modification of the request's hostname and path before it is proxied to its destination. +- `extensionRef`: [ExtensionRefs][] are used by Envoy Gateway to implement extended filters. Currently, Envoy Gateway + supports rate limiting and request authentication filters. For more information about these filters, refer to the + [rate limiting][] and [request authentication][] documentation. + +__Notes:__ +- The only [BackendRef][] kind supported by Envoy Gateway is a [Service][]. Routing traffic to other destinations such + as arbitrary URLs is not possible. +- Only `requestHeaderModifier` and `responseHeaderModifier` filters are currently supported within [HTTPBackendRef][]. + +## TCPRoute + +A [TCPRoute][] configures routing of raw TCP traffic through one or more Gateways. Traffic can be forwarded to the +desired BackendRefs based on a TCP port number. + +__Note:__ A TCPRoute only supports proxying in non-transparent mode, i.e. the backend will see the source IP and port of +the Envoy Proxy instance instead of the client. + +## UDPRoute + +A [UDPRoute][] configures routing of raw UDP traffic through one or more Gateways. Traffic can be forwarded to the +desired BackendRefs based on a UDP port number. + +__Note:__ Similar to TCPRoutes, UDPRoutes only support proxying in non-transparent mode i.e. the backend will see the +source IP and port of the Envoy Proxy instance instead of the client. + +## GRPCRoute + +A [GRPCRoute][] configures routing of [gRPC][] requests through one or more Gateways. They offer request matching by +hostname, gRPC service, gRPC method, or HTTP/2 Header. Envoy Gateway supports the following filters on GRPCRoutes to +provide additional traffic processing: + +- `requestHeaderModifier`: [RequestHeaderModifiers][grpc-filter] + can be used to modify or add request headers before the request is proxied to its destination. +- `responseHeaderModifier`: [ResponseHeaderModifiers][grpc-filter] + can be used to modify or add response headers before the response is sent back to the client. +- `requestMirror`: [RequestMirrors][grpc-filter] + configure destinations where the requests should also be mirrored to. Responses to mirrored requests will be ignored. + +__Notes:__ +- The only [BackendRef][grpc-filter] kind supported by Envoy Gateway is a [Service][]. Routing traffic to other + destinations such as arbitrary URLs is not currently possible. +- Only `requestHeaderModifier` and `responseHeaderModifier` filters are currently supported within [GRPCBackendRef][]. + +## TLSRoute + +A [TLSRoute][] configures routing of TCP traffic through one or more Gateways. However, unlike TCPRoutes, TLSRoutes +can match against TLS-specific metadata. + +## ReferenceGrant + +A [ReferenceGrant][] is used to allow a resource to reference another resource in a different namespace. Normally an +HTTPRoute created in namespace `foo` is not allowed to reference a Service in namespace `bar`. A ReferenceGrant permits +these types of cross-namespace references. Envoy Gateway supports the following ReferenceGrant use-cases: + +- Allowing an HTTPRoute, GRPCRoute, TLSRoute, UDPRoute, or TCPRoute to reference a Service in a different namespace. +- Allowing an HTTPRoute's `requestMirror` filter to include a BackendRef that references a Service in a different + namespace. +- Allowing a Gateway's [SecretObjectReference][] to reference a secret in a different namespace. + +[system design]: ../../../contributions/design/system-design +[Gateway API]: https://gateway-api.sigs.k8s.io/ +[GatewayClass]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.GatewayClass +[parameters reference]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.ParametersReference +[Gateway]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Gateway +[HTTPRoute]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRoute +[Service]: https://kubernetes.io/docs/concepts/services-networking/service/ +[BackendRef]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.BackendRef +[HTTPBackendRef]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPBackendRef +[TCPRoute]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.TCPRoute +[UDPRoute]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.UDPRoute +[GRPCRoute]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.GRPCRoute +[GRPCBackendRef]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.GRPCBackendRef +[gRPC]: https://grpc.io/ +[TLSRoute]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.TLSRoute +[ReferenceGrant]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.ReferenceGrant +[SecretObjectReference]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.SecretObjectReference +[rate limiting]: ../../../contributions/design/rate-limit +[request authentication]: ../security/jwt-authentication +[EnvoyProxy]: ../../../api/extension_types#envoyproxy +[resolving conflicts]: https://gateway-api.sigs.k8s.io/concepts/guidelines/?h=conflict#conflicts +[ExtensionRefs]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRouteFilterType +[grpc-filter]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.GRPCRouteFilter +[http-filter]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRouteFilter diff --git a/site/content/en/docs/tasks/traffic/global-rate-limit.md b/site/content/en/docs/tasks/traffic/global-rate-limit.md new file mode 100644 index 00000000000..3f1bfa4f301 --- /dev/null +++ b/site/content/en/docs/tasks/traffic/global-rate-limit.md @@ -0,0 +1,1312 @@ +--- +title: "Global Rate Limit" +--- + +Rate limit is a feature that allows the user to limit the number of incoming requests to a predefined value based on attributes within the traffic flow. + +Here are some reasons why you may want to implement Rate limits + +* To prevent malicious activity such as DDoS attacks. +* To prevent applications and its resources (such as a database) from getting overloaded. +* To create API limits based on user entitlements. + +Envoy Gateway supports two types of rate limiting: [Global rate limiting][] and [Local rate limiting][]. + +[Global rate limiting][] applies a shared rate limit to the traffic flowing through all the instances of Envoy proxies where it is configured. +i.e. if the data plane has 2 replicas of Envoy running, and the rate limit is 10 requests/second, this limit is shared and will be hit +if 5 requests pass through the first replica and 5 requests pass through the second replica within the same second. + +Envoy Gateway introduces a new CRD called [BackendTrafficPolicy][] that allows the user to describe their rate limit intent. This instantiated resource +can be linked to a [Gateway][], [HTTPRoute][] or [GRPCRoute][] resource. + +**Note:** Limit is applied per route. Even if a [BackendTrafficPolicy][] targets a gateway, each route in that gateway +still has a separate rate limit bucket. For example, if a gateway has 2 routes, and the limit is 100r/s, then each route +has its own 100r/s rate limit bucket. + +## Prerequisites + +### Install Envoy Gateway + +* Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the HTTPRoute example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +### Install Redis + +* The global rate limit feature is based on [Envoy Ratelimit][] which requires a Redis instance as its caching layer. +Lets install a Redis deployment in the `redis-system` namespce. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +### Enable Global Rate limit in Envoy Gateway + +* The default installation of Envoy Gateway installs a default [EnvoyGateway][] configuration and attaches it +using a `ConfigMap`. In the next step, we will update this resource to enable rate limit in Envoy Gateway +as well as configure the URL for the Redis instance used for Global rate limiting. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +* After updating the `ConfigMap`, you will need to restart the `envoy-gateway` deployment so the configuration kicks in + +```shell +kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system +``` + +## Rate Limit Specific User + +Here is an example of a rate limit implemented by the application developer to limit a specific user by matching on a custom `x-user-id` header +with a value set to `one`. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +### HTTPRoute + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The HTTPRoute status should indicate that it has been accepted and is bound to the example Gateway. + +```shell +kubectl get httproute/http-ratelimit -o yaml +``` + +Get the Gateway's address: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +Let's query `ratelimit.example/get` 4 times. We should receive a `200` response from the example Gateway for the first 3 requests +and then receive a `429` status code for the 4th request since the limit is set at 3 requests/Hour for the request which contains the header `x-user-id` +and value `one`. + +```shell +for i in {1..4}; do curl -I --header "Host: ratelimit.example" --header "x-user-id: one" http://${GATEWAY_HOST}/get ; sleep 1; done +``` + +```console +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:31 GMT +content-length: 460 +x-envoy-upstream-service-time: 4 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:32 GMT +content-length: 460 +x-envoy-upstream-service-time: 2 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:33 GMT +content-length: 460 +x-envoy-upstream-service-time: 0 +server: envoy + +HTTP/1.1 429 Too Many Requests +x-envoy-ratelimited: true +date: Wed, 08 Feb 2023 02:33:34 GMT +server: envoy +transfer-encoding: chunked + +``` + +You should be able to send requests with the `x-user-id` header and a different value and receive successful responses from the server. + +```shell +for i in {1..4}; do curl -I --header "Host: ratelimit.example" --header "x-user-id: two" http://${GATEWAY_HOST}/get ; sleep 1; done +``` + +```console +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:34:36 GMT +content-length: 460 +x-envoy-upstream-service-time: 0 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:34:37 GMT +content-length: 460 +x-envoy-upstream-service-time: 0 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:34:38 GMT +content-length: 460 +x-envoy-upstream-service-time: 0 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:34:39 GMT +content-length: 460 +x-envoy-upstream-service-time: 0 +server: envoy + +``` + +## Rate Limit Distinct Users + +Here is an example of a rate limit implemented by the application developer to limit distinct users who can be differentiated based on the +value in the `x-user-id` header. Here, user `one` (recognised from the traffic flow using the header `x-user-id` and value `one`) will be rate limited at 3 requests/hour +and so will user `two` (recognised from the traffic flow using the header `x-user-id` and value `two`). + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +### HTTPRoute + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Lets run the same command again with the header `x-user-id` and value `one` set in the request. We should the first 3 requests succeeding and +the 4th request being rate limited. + +```shell +for i in {1..4}; do curl -I --header "Host: ratelimit.example" --header "x-user-id: one" http://${GATEWAY_HOST}/get ; sleep 1; done +``` + +```console +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:31 GMT +content-length: 460 +x-envoy-upstream-service-time: 4 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:32 GMT +content-length: 460 +x-envoy-upstream-service-time: 2 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:33 GMT +content-length: 460 +x-envoy-upstream-service-time: 0 +server: envoy + +HTTP/1.1 429 Too Many Requests +x-envoy-ratelimited: true +date: Wed, 08 Feb 2023 02:33:34 GMT +server: envoy +transfer-encoding: chunked + +``` + +You should see the same behavior when the value for header `x-user-id` is set to `two` and 4 requests are sent. + +```shell +for i in {1..4}; do curl -I --header "Host: ratelimit.example" --header "x-user-id: two" http://${GATEWAY_HOST}/get ; sleep 1; done +``` + +```console +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:31 GMT +content-length: 460 +x-envoy-upstream-service-time: 4 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:32 GMT +content-length: 460 +x-envoy-upstream-service-time: 2 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:33 GMT +content-length: 460 +x-envoy-upstream-service-time: 0 +server: envoy + +HTTP/1.1 429 Too Many Requests +x-envoy-ratelimited: true +date: Wed, 08 Feb 2023 02:33:34 GMT +server: envoy +transfer-encoding: chunked + +``` + +## Rate Limit All Requests + +This example shows you how to rate limit all requests matching the HTTPRoute rule at 3 requests/Hour by leaving the `clientSelectors` field unset. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +### HTTPRoute + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +```shell +for i in {1..4}; do curl -I --header "Host: ratelimit.example" http://${GATEWAY_HOST}/get ; sleep 1; done +``` + +```console +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:31 GMT +content-length: 460 +x-envoy-upstream-service-time: 4 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:32 GMT +content-length: 460 +x-envoy-upstream-service-time: 2 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:33 GMT +content-length: 460 +x-envoy-upstream-service-time: 0 +server: envoy + +HTTP/1.1 429 Too Many Requests +x-envoy-ratelimited: true +date: Wed, 08 Feb 2023 02:33:34 GMT +server: envoy +transfer-encoding: chunked + +``` + +## Rate Limit Client IP Addresses + +Here is an example of a rate limit implemented by the application developer to limit distinct users who can be differentiated based on their + IP address (also reflected in the `X-Forwarded-For` header). + +Note: EG supports two kinds of rate limit for the IP address: Exact and Distinct. +* Exact means that all IP addresses within the specified Source IP CIDR share the same rate limit bucket. +* Distinct means that each IP address within the specified Source IP CIDR has its own rate limit bucket. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +```shell +for i in {1..4}; do curl -I --header "Host: ratelimit.example" http://${GATEWAY_HOST}/get ; sleep 1; done +``` + +```console +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Tue, 28 Mar 2023 08:28:45 GMT +content-length: 512 +x-envoy-upstream-service-time: 0 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Tue, 28 Mar 2023 08:28:46 GMT +content-length: 512 +x-envoy-upstream-service-time: 0 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Tue, 28 Mar 2023 08:28:48 GMT +content-length: 512 +x-envoy-upstream-service-time: 0 +server: envoy + +HTTP/1.1 429 Too Many Requests +x-envoy-ratelimited: true +date: Tue, 28 Mar 2023 08:28:48 GMT +server: envoy +transfer-encoding: chunked + +``` + +## Rate Limit Jwt Claims + +Here is an example of a rate limit implemented by the application developer to limit distinct users who can be differentiated based on the value of the Jwt claims carried. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Get the JWT used for testing request authentication: + +```shell +TOKEN=$(curl https://raw.githubusercontent.com/envoyproxy/gateway/main/examples/kubernetes/jwt/test.jwt -s) && echo "$TOKEN" | cut -d '.' -f2 - | base64 --decode - +``` + +```shell +TOKEN1=$(curl https://raw.githubusercontent.com/envoyproxy/gateway/main/examples/kubernetes/jwt/with-different-claim.jwt -s) && echo "$TOKEN1" | cut -d '.' -f2 - | base64 --decode - +``` + +### Rate limit by carrying `TOKEN` + +```shell +for i in {1..4}; do curl -I --header "Host: ratelimit.example" --header "Authorization: Bearer $TOKEN" http://${GATEWAY_HOST}/foo ; sleep 1; done +``` + +```console +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Mon, 12 Jun 2023 12:00:25 GMT +content-length: 561 +x-envoy-upstream-service-time: 0 +server: envoy + + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Mon, 12 Jun 2023 12:00:26 GMT +content-length: 561 +x-envoy-upstream-service-time: 0 +server: envoy + + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Mon, 12 Jun 2023 12:00:27 GMT +content-length: 561 +x-envoy-upstream-service-time: 0 +server: envoy + + +HTTP/1.1 429 Too Many Requests +x-envoy-ratelimited: true +date: Mon, 12 Jun 2023 12:00:28 GMT +server: envoy +transfer-encoding: chunked + +``` + +### No Rate Limit by carrying `TOKEN1` + +```shell +for i in {1..4}; do curl -I --header "Host: ratelimit.example" --header "Authorization: Bearer $TOKEN1" http://${GATEWAY_HOST}/foo ; sleep 1; done +``` + +```console +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Mon, 12 Jun 2023 12:02:34 GMT +content-length: 556 +x-envoy-upstream-service-time: 0 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Mon, 12 Jun 2023 12:02:35 GMT +content-length: 556 +x-envoy-upstream-service-time: 0 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Mon, 12 Jun 2023 12:02:36 GMT +content-length: 556 +x-envoy-upstream-service-time: 1 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Mon, 12 Jun 2023 12:02:37 GMT +content-length: 556 +x-envoy-upstream-service-time: 0 +server: envoy + +``` + +### (Optional) Editing Kubernetes Resources settings for the Rate Limit Service + +* The default installation of Envoy Gateway installs a default [EnvoyGateway][] configuration and provides the initial rate +limit kubernetes resources settings. such as `replicas` is 1, requests resources cpu is `100m`, memory is `512Mi`. the others +like container `image`, `securityContext`, `env` and pod `annotations` and `securityContext` can be modified by modifying the `ConfigMap`. + +* `tls.certificateRef` set the client certificate for redis server TLS connections. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +* After updating the `ConfigMap`, you will need to restart the `envoy-gateway` deployment so the configuration kicks in + +```shell +kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system +``` + +[Global Rate Limiting]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_features/global_rate_limiting +[Local rate limiting]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_features/local_rate_limiting +[BackendTrafficPolicy]: ../../../api/extension_types#backendtrafficpolicy +[Envoy Ratelimit]: https://github.com/envoyproxy/ratelimit +[EnvoyGateway]: ../../api/extension_types#envoygateway +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway/ +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute/ +[GRPCRoute]: https://gateway-api.sigs.k8s.io/api-types/grpcroute/ diff --git a/site/content/en/docs/tasks/traffic/grpc-routing.md b/site/content/en/docs/tasks/traffic/grpc-routing.md new file mode 100644 index 00000000000..7c41b54c885 --- /dev/null +++ b/site/content/en/docs/tasks/traffic/grpc-routing.md @@ -0,0 +1,272 @@ +--- +title: "GRPC Routing" +--- + +The [GRPCRoute][] resource allows users to configure gRPC routing by matching HTTP/2 traffic and forwarding it to backend gRPC servers. +To learn more about gRPC routing, refer to the [Gateway API documentation][]. + +## Prerequisites + +Follow the steps from the [Quickstart](../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## Installation + +Install the gRPC routing example resources: + +```shell +kubectl apply -f https://raw.githubusercontent.com/envoyproxy/gateway/latest/examples/kubernetes/grpc-routing.yaml +``` + +The manifest installs a [GatewayClass][], [Gateway][], a Deployment, a Service, and a GRPCRoute resource. +The GatewayClass is a cluster-scoped resource that represents a class of Gateways that can be instantiated. + +__Note:__ Envoy Gateway is configured by default to manage a GatewayClass with +`controllerName: gateway.envoyproxy.io/gatewayclass-controller`. + +## Verification + +Check the status of the GatewayClass: + +```shell +kubectl get gc --selector=example=grpc-routing +``` + +The status should reflect "Accepted=True", indicating Envoy Gateway is managing the GatewayClass. + +A Gateway represents configuration of infrastructure. When a Gateway is created, [Envoy proxy][] infrastructure is +provisioned or configured by Envoy Gateway. The `gatewayClassName` defines the name of a GatewayClass used by this +Gateway. Check the status of the Gateway: + +```shell +kubectl get gateways --selector=example=grpc-routing +``` + +The status should reflect "Ready=True", indicating the Envoy proxy infrastructure has been provisioned. The status also +provides the address of the Gateway. This address is used later to test connectivity to proxied backend services. + +Check the status of the GRPCRoute: + +```shell +kubectl get grpcroutes --selector=example=grpc-routing -o yaml +``` + +The status for the GRPCRoute should surface "Accepted=True" and a `parentRef` that references the example Gateway. +The `example-route` matches any traffic for "grpc-example.com" and forwards it to the "yages" Service. + +## Testing the Configuration + +Before testing GRPC routing to the `yages` backend, get the Gateway's address. + +```shell +export GATEWAY_HOST=$(kubectl get gateway/example-gateway -o jsonpath='{.status.addresses[0].value}') +``` + +Test GRPC routing to the `yages` backend using the [grpcurl][] command. + +```shell +grpcurl -plaintext -authority=grpc-example.com ${GATEWAY_HOST}:80 yages.Echo/Ping +``` + +You should see the below response + +```shell +{ + "text": "pong" +} +``` + +Envoy Gateway also supports [gRPC-Web][] requests for this configuration. The below `curl` command can be used to send a grpc-Web request with over HTTP/2. You should receive the same response seen in the previous command. + +The data in the body `AAAAAAA=` is a base64 encoded representation of an empty message (data length 0) that the Ping RPC accepts. + +```shell +curl --http2-prior-knowledge -s ${GATEWAY_HOST}:80/yages.Echo/Ping -H 'Host: grpc-example.com' -H 'Content-Type: application/grpc-web-text' -H 'Accept: application/grpc-web-text' -XPOST -d'AAAAAAA=' | base64 -d +``` + +## GRPCRoute Match +The `matches` field can be used to restrict the route to a specific set of requests based on GRPC's service and/or method names. +It supports two match types: `Exact` and `RegularExpression`. + +### Exact + +`Exact` match is the default match type. + +The following example shows how to match a request based on the service and method names for `grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo`, +as well as a match for all services with a method name `Ping` which matches `yages.Echo/Ping` in our deployment. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the GRPCRoute status: + +```shell +kubectl get grpcroutes --selector=example=grpc-routing -o yaml +``` + +Test GRPC routing to the `yages` backend using the [grpcurl][] command. + +```shell +grpcurl -plaintext -authority=grpc-example.com ${GATEWAY_HOST}:80 yages.Echo/Ping +``` + +### RegularExpression + +The following example shows how to match a request based on the service and method names +with match type `RegularExpression`. It matches all the services and methods with pattern +`/.*.Echo/Pin.+`, which matches `yages.Echo/Ping` in our deployment. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the GRPCRoute status: + +```shell +kubectl get grpcroutes --selector=example=grpc-routing -o yaml +``` + +Test GRPC routing to the `yages` backend using the [grpcurl][] command. + +```shell +grpcurl -plaintext -authority=grpc-example.com ${GATEWAY_HOST}:80 yages.Echo/Ping +``` + +[GRPCRoute]: https://gateway-api.sigs.k8s.io/api-types/grpcroute/ +[Gateway API documentation]: https://gateway-api.sigs.k8s.io/ +[GatewayClass]: https://gateway-api.sigs.k8s.io/api-types/gatewayclass/ +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway/ +[Envoy proxy]: https://www.envoyproxy.io/ +[grpcurl]: https://github.com/fullstorydev/grpcurl +[gRPC-Web]: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md#protocol-differences-vs-grpc-over-http2 diff --git a/site/content/en/v1.0.2/tasks/traffic/http-redirect.md b/site/content/en/docs/tasks/traffic/http-redirect.md similarity index 100% rename from site/content/en/v1.0.2/tasks/traffic/http-redirect.md rename to site/content/en/docs/tasks/traffic/http-redirect.md diff --git a/site/content/en/docs/tasks/traffic/http-request-headers.md b/site/content/en/docs/tasks/traffic/http-request-headers.md new file mode 100644 index 00000000000..7bc709c49c6 --- /dev/null +++ b/site/content/en/docs/tasks/traffic/http-request-headers.md @@ -0,0 +1,449 @@ +--- +title: "HTTP Request Headers" +--- + +The [HTTPRoute][] resource can modify the headers of a request before forwarding it to the upstream service. HTTPRoute +rules cannot use both filter types at once. Currently, Envoy Gateway only supports __core__ [HTTPRoute filters][] which +consist of `RequestRedirect` and `RequestHeaderModifier` at the time of this writing. To learn more about HTTP routing, +refer to the [Gateway API documentation][]. + +A [`RequestHeaderModifier` filter][req_filter] instructs Gateways to modify the headers in requests that match the rule +before forwarding the request upstream. Note that the `RequestHeaderModifier` filter will only modify headers before the +request is sent from Envoy to the upstream service and will not affect response headers returned to the downstream +client. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## Adding Request Headers + +The `RequestHeaderModifier` filter can add new headers to a request before it is sent to the upstream. If the request +does not have the header configured by the filter, then that header will be added to the request. If the request already +has the header configured by the filter, then the value of the header in the filter will be appended to the value of the +header in the request. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + + +The HTTPRoute status should indicate that it has been accepted and is bound to the example Gateway. + +```shell +kubectl get httproute/http-headers -o yaml +``` + +Get the Gateway's address: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +Querying `headers.example/get` should result in a `200` response from the example Gateway and the output from the +example app should indicate that the upstream example app received the header `add-header` with the value: +`something,foo` + +```console +$ curl -vvv --header "Host: headers.example" "http://${GATEWAY_HOST}/get" --header "add-header: something" +... +> GET /get HTTP/1.1 +> Host: headers.example +> User-Agent: curl/7.81.0 +> Accept: */* +> add-header: something +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< content-length: 474 +< x-envoy-upstream-service-time: 0 +< server: envoy +< +... + "headers": { + "Accept": [ + "*/*" + ], + "Add-Header": [ + "something", + "foo" + ], +... +``` + +## Setting Request Headers + +Setting headers is similar to adding headers. If the request does not have the header configured by the filter, then it +will be added, but unlike [adding request headers](#adding-request-headers) which will append the value of the header if +the request already contains it, setting a header will cause the value to be replaced by the value configured in the +filter. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Querying `headers.example/get` should result in a `200` response from the example Gateway and the output from the +example app should indicate that the upstream example app received the header `add-header` with the original value +`something` replaced by `foo`. + +```console +$ curl -vvv --header "Host: headers.example" "http://${GATEWAY_HOST}/get" --header "set-header: something" +... +> GET /get HTTP/1.1 +> Host: headers.example +> User-Agent: curl/7.81.0 +> Accept: */* +> add-header: something +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< content-length: 474 +< x-envoy-upstream-service-time: 0 +< server: envoy +< + "headers": { + "Accept": [ + "*/*" + ], + "Set-Header": [ + "foo" + ], +... +``` + +## Removing Request Headers + +Headers can be removed from a request by simply supplying a list of header names. + +Setting headers is similar to adding headers. If the request does not have the header configured by the filter, then it +will be added, but unlike [adding request headers](#adding-request-headers) which will append the value of the header if +the request already contains it, setting a header will cause the value to be replaced by the value configured in the +filter. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Querying `headers.example/get` should result in a `200` response from the example Gateway and the output from the +example app should indicate that the upstream example app received the header `add-header`, but the header +`remove-header` that was sent by curl was removed before the upstream received the request. + +```console +$ curl -vvv --header "Host: headers.example" "http://${GATEWAY_HOST}/get" --header "add-header: something" --header "remove-header: foo" +... +> GET /get HTTP/1.1 +> Host: headers.example +> User-Agent: curl/7.81.0 +> Accept: */* +> add-header: something +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< content-length: 474 +< x-envoy-upstream-service-time: 0 +< server: envoy +< + + "headers": { + "Accept": [ + "*/*" + ], + "Add-Header": [ + "something" + ], +... +``` + +## Combining Filters + +Headers can be added/set/removed in a single filter on the same HTTPRoute and they will all perform as expected + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute/ +[HTTPRoute filters]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRouteFilter +[Gateway API documentation]: https://gateway-api.sigs.k8s.io/ +[req_filter]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPHeaderFilter diff --git a/site/content/en/docs/tasks/traffic/http-request-mirroring.md b/site/content/en/docs/tasks/traffic/http-request-mirroring.md new file mode 100644 index 00000000000..f22ef51da36 --- /dev/null +++ b/site/content/en/docs/tasks/traffic/http-request-mirroring.md @@ -0,0 +1,447 @@ +--- +title: "HTTPRoute Request Mirroring" +--- + +The [HTTPRoute][] resource allows one or more [backendRefs][] to be provided. Requests will be routed to these upstreams. It is possible to divide the traffic between these backends using [Traffic Splitting][], but it is also possible to mirror requests to another Service instead. Request mirroring is accomplished using Gateway API's [HTTPRequestMirrorFilter][] on the `HTTPRoute`. + +When requests are made to a `HTTPRoute` that uses a `HTTPRequestMirrorFilter`, the response will never come from the `backendRef` defined in the filter. Responses from the mirror `backendRef` are always ignored. + +## Installation + +Follow the steps from the [Quickstart][] to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## Mirroring the Traffic + +Next, create a new `Deployment` and `Service` to mirror requests to. The following example will use +a second instance of the application deployed in the quickstart. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} +Then create an `HTTPRoute` that uses a `HTTPRequestMirrorFilter` to send requests to the original +service from the quickstart, and mirror request to the service that was just deployed. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The HTTPRoute status should indicate that it has been accepted and is bound to the example Gateway. + +```shell +kubectl get httproute/http-mirror -o yaml +``` + +Get the Gateway's address: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +Querying `backends.example/get` should result in a `200` response from the example Gateway and the output from the +example app should indicate which pod handled the request. There is only one pod in the deployment for the example app +from the quickstart, so it will be the same on all subsequent requests. + +```console +$ curl -v --header "Host: backends.example" "http://${GATEWAY_HOST}/get" +... +> GET /get HTTP/1.1 +> Host: backends.example +> User-Agent: curl/7.81.0 +> Accept: */* +> add-header: something +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< content-length: 474 +< x-envoy-upstream-service-time: 0 +< server: envoy +< +... + + "namespace": "default", + "ingress": "", + "service": "", + "pod": "backend-79665566f5-s589f" +... +``` + +Check the logs of the pods and you will see that the original deployment and the new deployment each got a request: + +```shell +$ kubectl logs deploy/backend && kubectl logs deploy/backend-2 +... +Starting server, listening on port 3000 (http) +Echoing back request made to /get to client (10.42.0.10:41566) +Starting server, listening on port 3000 (http) +Echoing back request made to /get to client (10.42.0.10:45096) +``` + +## Multiple BackendRefs + +When an `HTTPRoute` has multiple `backendRefs` and an `HTTPRequestMirrorFilter`, traffic splitting will still behave the same as it normally would for the main `backendRefs` while the `backendRef` of the `HTTPRequestMirrorFilter` will continue receiving mirrored copies of the incoming requests. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +## Multiple HTTPRequestMirrorFilters + +Multiple `HTTPRequestMirrorFilters` are not supported on the same `HTTPRoute` `rule`. When attempting to do so, the admission webhook will reject the configuration. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +```console +Error from server: error when creating "STDIN": admission webhook "validate.gateway.networking.k8s.io" denied the request: spec.rules[0].filters: Invalid value: "RequestMirror": cannot be used multiple times in the same rule +``` + +[Quickstart]: ../../quickstart/ +[Traffic Splitting]: ../http-traffic-splitting/ +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute/ +[backendRefs]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.BackendRef +[HTTPRequestMirrorFilter]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRequestMirrorFilter diff --git a/site/content/en/docs/tasks/traffic/http-response-headers.md b/site/content/en/docs/tasks/traffic/http-response-headers.md new file mode 100644 index 00000000000..60121674b00 --- /dev/null +++ b/site/content/en/docs/tasks/traffic/http-response-headers.md @@ -0,0 +1,446 @@ +--- +title: "HTTP Response Headers" +--- + +The [HTTPRoute][] resource can modify the headers of a response before responding it to the downstream service. To learn +more about HTTP routing, refer to the [Gateway API documentation][]. + +A [`ResponseHeaderModifier` filter][req_filter] instructs Gateways to modify the headers in responses that match the +rule before responding to the downstream. Note that the `ResponseHeaderModifier` filter will only modify headers before +the response is returned from Envoy to the downstream client and will not affect request headers forwarding to the +upstream service. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## Adding Response Headers + +The `ResponseHeaderModifier` filter can add new headers to a response before it is sent to the upstream. If the response +does not have the header configured by the filter, then that header will be added to the response. If the response +already has the header configured by the filter, then the value of the header in the filter will be appended to the +value of the header in the response. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The HTTPRoute status should indicate that it has been accepted and is bound to the example Gateway. + +```shell +kubectl get httproute/http-headers -o yaml +``` + +Get the Gateway's address: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +Querying `headers.example/get` should result in a `200` response from the example Gateway and the output from the +example app should indicate that the downstream client received the header `add-header` with the value: `foo` + +```console +$ curl -vvv --header "Host: headers.example" "http://${GATEWAY_HOST}/get" -H 'X-Echo-Set-Header: X-Foo: value1' +... +> GET /get HTTP/1.1 +> Host: headers.example +> User-Agent: curl/7.81.0 +> Accept: */* +> X-Echo-Set-Header: X-Foo: value1 +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< content-length: 474 +< x-envoy-upstream-service-time: 0 +< server: envoy +< x-foo: value1 +< add-header: foo +< +... + "headers": { + "Accept": [ + "*/*" + ], + "X-Echo-Set-Header": [ + "X-Foo: value1" + ] +... +``` + +## Setting Response Headers + +Setting headers is similar to adding headers. If the response does not have the header configured by the filter, then it +will be added, but unlike [adding response headers](#adding-response-headers) which will append the value of the header +if the response already contains it, setting a header will cause the value to be replaced by the value configured in the +filter. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Querying `headers.example/get` should result in a `200` response from the example Gateway and the output from the +example app should indicate that the downstream client received the header `set-header` with the original value `value1` +replaced by `foo`. + +```console +$ curl -vvv --header "Host: headers.example" "http://${GATEWAY_HOST}/get" -H 'X-Echo-Set-Header: set-header: value1' +... +> GET /get HTTP/1.1 +> Host: headers.example +> User-Agent: curl/7.81.0 +> Accept: */* +> X-Echo-Set-Header: set-header: value1 +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< content-length: 474 +< x-envoy-upstream-service-time: 0 +< server: envoy +< set-header: foo +< + "headers": { + "Accept": [ + "*/*" + ], + "X-Echo-Set-Header": [ + "set-header": value1" + ] +... +``` + +## Removing Response Headers + +Headers can be removed from a response by simply supplying a list of header names. + +Setting headers is similar to adding headers. If the response does not have the header configured by the filter, then it +will be added, but unlike [adding response headers](#adding-response-headers) which will append the value of the header +if the response already contains it, setting a header will cause the value to be replaced by the value configured in the +filter. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Querying `headers.example/get` should result in a `200` response from the example Gateway and the output from the +example app should indicate that the header `remove-header` that was sent by curl was removed before the upstream +received the response. + +```console +$ curl -vvv --header "Host: headers.example" "http://${GATEWAY_HOST}/get" -H 'X-Echo-Set-Header: remove-header: value1' +... +> GET /get HTTP/1.1 +> Host: headers.example +> User-Agent: curl/7.81.0 +> Accept: */* +> X-Echo-Set-Header: remove-header: value1 +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< content-length: 474 +< x-envoy-upstream-service-time: 0 +< server: envoy +< + + "headers": { + "Accept": [ + "*/*" + ], + "X-Echo-Set-Header": [ + "remove-header": value1" + ] +... +``` + +## Combining Filters + +Headers can be added/set/removed in a single filter on the same HTTPRoute and they will all perform as expected + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute/ +[Gateway API documentation]: https://gateway-api.sigs.k8s.io/ +[req_filter]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPHeaderFilter diff --git a/site/content/en/docs/tasks/traffic/http-routing.md b/site/content/en/docs/tasks/traffic/http-routing.md new file mode 100644 index 00000000000..aba57adc9b2 --- /dev/null +++ b/site/content/en/docs/tasks/traffic/http-routing.md @@ -0,0 +1,302 @@ +--- +title: "HTTP Routing" +--- + +The [HTTPRoute][] resource allows users to configure HTTP routing by matching HTTP traffic and forwarding it to +Kubernetes backends. Currently, the only supported backend supported by Envoy Gateway is a Service resource. This task +shows how to route traffic based on host, header, and path fields and forward the traffic to different Kubernetes +Services. To learn more about HTTP routing, refer to the [Gateway API documentation][]. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## Installation + +Install the HTTP routing example resources: + +```shell +kubectl apply -f https://raw.githubusercontent.com/envoyproxy/gateway/latest/examples/kubernetes/http-routing.yaml +``` + +The manifest installs a [GatewayClass][], [Gateway][], four Deployments, four Services, and three HTTPRoute resources. +The GatewayClass is a cluster-scoped resource that represents a class of Gateways that can be instantiated. + +__Note:__ Envoy Gateway is configured by default to manage a GatewayClass with +`controllerName: gateway.envoyproxy.io/gatewayclass-controller`. + +## Verification + +Check the status of the GatewayClass: + +```shell +kubectl get gc --selector=example=http-routing +``` + +The status should reflect "Accepted=True", indicating Envoy Gateway is managing the GatewayClass. + +A Gateway represents configuration of infrastructure. When a Gateway is created, [Envoy proxy][] infrastructure is +provisioned or configured by Envoy Gateway. The `gatewayClassName` defines the name of a GatewayClass used by this +Gateway. Check the status of the Gateway: + +```shell +kubectl get gateways --selector=example=http-routing +``` + +The status should reflect "Ready=True", indicating the Envoy proxy infrastructure has been provisioned. The status also +provides the address of the Gateway. This address is used later to test connectivity to proxied backend +services. + +The three HTTPRoute resources create routing rules on the Gateway. In order to receive traffic from a Gateway, +an HTTPRoute must be configured with `parentRefs` which reference the parent Gateway(s) that it should be attached to. +An HTTPRoute can match against a [single set of hostnames][spec]. These hostnames are matched before any other matching +within the HTTPRoute takes place. Since `example.com`, `foo.example.com`, and `bar.example.com` are separate hosts with +different routing requirements, each is deployed as its own HTTPRoute - `example-route, ``foo-route`, and `bar-route`. + +Check the status of the HTTPRoutes: + +```shell +kubectl get httproutes --selector=example=http-routing -o yaml +``` + +The status for each HTTPRoute should surface "Accepted=True" and a `parentRef` that references the example Gateway. +The `example-route` matches any traffic for "example.com" and forwards it to the "example-svc" Service. + +## Testing the Configuration + +Before testing HTTP routing to the `example-svc` backend, get the Gateway's address. + +```shell +export GATEWAY_HOST=$(kubectl get gateway/example-gateway -o jsonpath='{.status.addresses[0].value}') +``` + +Test HTTP routing to the `example-svc` backend. + +```shell +curl -vvv --header "Host: example.com" "http://${GATEWAY_HOST}/" +``` + +A `200` status code should be returned and the body should include `"pod": "example-backend-*"` indicating the traffic +was routed to the example backend service. If you change the hostname to a hostname not represented in any of the +HTTPRoutes, e.g. "www.example.com", the HTTP traffic will not be routed and a `404` should be returned. + +The `foo-route` matches any traffic for `foo.example.com` and applies its routing rules to forward the traffic to the +"foo-svc" Service. Since there is only one path prefix match for `/login`, only `foo.example.com/login/*` traffic will +be forwarded. Test HTTP routing to the `foo-svc` backend. + +```shell +curl -vvv --header "Host: foo.example.com" "http://${GATEWAY_HOST}/login" +``` + +A `200` status code should be returned and the body should include `"pod": "foo-backend-*"` indicating the traffic +was routed to the foo backend service. Traffic to any other paths that do not begin with `/login` will not be matched by +this HTTPRoute. Test this by removing `/login` from the request. + +```shell +curl -vvv --header "Host: foo.example.com" "http://${GATEWAY_HOST}/" +``` + +The HTTP traffic will not be routed and a `404` should be returned. + +Similarly, the `bar-route` HTTPRoute matches traffic for `bar.example.com`. All traffic for this hostname will be +evaluated against the routing rules. The most specific match will take precedence which means that any traffic with the +`env:canary` header will be forwarded to `bar-svc-canary` and if the header is missing or not `canary` then it'll be +forwarded to `bar-svc`. Test HTTP routing to the `bar-svc` backend. + +```shell +curl -vvv --header "Host: bar.example.com" "http://${GATEWAY_HOST}/" +``` + +A `200` status code should be returned and the body should include `"pod": "bar-backend-*"` indicating the traffic +was routed to the foo backend service. + +Test HTTP routing to the `bar-canary-svc` backend by adding the `env: canary` header to the request. + +```shell +curl -vvv --header "Host: bar.example.com" --header "env: canary" "http://${GATEWAY_HOST}/" +``` + +A `200` status code should be returned and the body should include `"pod": "bar-canary-backend-*"` indicating the +traffic was routed to the foo backend service. + +### JWT Claims Based Routing + +Users can route to a specific backend by matching on JWT claims. +This can be achieved, by defining a SecurityPolicy with a jwt configuration that does the following +* Converts jwt claims to headers, which can be used for header based routing +* Sets the recomputeRoute field to `true`. This is required so that the incoming request matches on a fallback/catch all route where the JWT can be authenticated, the claims from the JWT can be converted to headers, and then the route match can be recomputed to match based on the updated headers. + +For this feature to work please make sure +* you have a fallback route rule defined, the backend for this route rule can be invalid. +* The SecurityPolicy is applied to both the fallback route as well as the route with the claim header matches, to avoid spoofing. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Get the JWT used for testing request authentication: + +```shell +TOKEN=$(curl https://raw.githubusercontent.com/envoyproxy/gateway/main/examples/kubernetes/jwt/test.jwt -s) && echo "$TOKEN" | cut -d '.' -f2 - | base64 --decode - +``` + +Test routing to the `foo-svc` backend by specifying a JWT Token with a claim `name: John Doe`. + +```shell +curl -sS -H "Host: foo.example.com" -H "Authorization: Bearer $TOKEN" "http://${GATEWAY_HOST}/login" | jq .pod +"foo-backend-6df8cc6b9f-fmwcg" +``` + +Get another JWT used for testing request authentication: + +```shell +TOKEN=$(curl https://raw.githubusercontent.com/envoyproxy/gateway/main/examples/kubernetes/jwt/with-different-claim.jwt -s) && echo "$TOKEN" | cut -d '.' -f2 - | base64 --decode - +``` + +Test HTTP routing to the `bar-svc` backend by specifying a JWT Token with a claim `name: Tom`. + +```shell +curl -sS -H "Host: bar.example.com" -H "Authorization: Bearer $TOKEN" "http://${GATEWAY_HOST}/" | jq .pod +"bar-backend-6688b8944c-s8htr" +``` + +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute/ +[Gateway API documentation]: https://gateway-api.sigs.k8s.io/ +[GatewayClass]: https://gateway-api.sigs.k8s.io/api-types/gatewayclass/ +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway/ +[Envoy proxy]: https://www.envoyproxy.io/ +[spec]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRouteSpec diff --git a/site/content/en/v1.0.2/tasks/traffic/http-timeouts.md b/site/content/en/docs/tasks/traffic/http-timeouts.md similarity index 100% rename from site/content/en/v1.0.2/tasks/traffic/http-timeouts.md rename to site/content/en/docs/tasks/traffic/http-timeouts.md diff --git a/site/content/en/docs/tasks/traffic/http-traffic-splitting.md b/site/content/en/docs/tasks/traffic/http-traffic-splitting.md new file mode 100644 index 00000000000..06e4a236589 --- /dev/null +++ b/site/content/en/docs/tasks/traffic/http-traffic-splitting.md @@ -0,0 +1,527 @@ +--- +title: "HTTPRoute Traffic Splitting" +--- + +The [HTTPRoute][] resource allows one or more [backendRefs][] to be provided. Requests will be routed to these upstreams +if they match the rules of the HTTPRoute. If an invalid backendRef is configured, then HTTP responses will be returned +with status code `500` for all requests that would have been sent to that backend. + +## Installation + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## Single backendRef + +When a single backendRef is configured in a HTTPRoute, it will receive 100% of the traffic. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The HTTPRoute status should indicate that it has been accepted and is bound to the example Gateway. + +```shell +kubectl get httproute/http-headers -o yaml +``` + +Get the Gateway's address: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +Querying `backends.example/get` should result in a `200` response from the example Gateway and the output from the +example app should indicate which pod handled the request. There is only one pod in the deployment for the example app +from the quickstart, so it will be the same on all subsequent requests. + +```console +$ curl -vvv --header "Host: backends.example" "http://${GATEWAY_HOST}/get" +... +> GET /get HTTP/1.1 +> Host: backends.example +> User-Agent: curl/7.81.0 +> Accept: */* +> add-header: something +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< content-length: 474 +< x-envoy-upstream-service-time: 0 +< server: envoy +< +... + "namespace": "default", + "ingress": "", + "service": "", + "pod": "backend-79665566f5-s589f" +... +``` + +## Multiple backendRefs + +If multiple backendRefs are configured, then traffic will be split between the backendRefs equally unless a weight is +configured. + +First, create a second instance of the example app from the quickstart: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Then create an HTTPRoute that uses both the app from the quickstart and the second instance that was just created + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Querying `backends.example/get` should result in `200` responses from the example Gateway and the output from the +example app that indicates which pod handled the request should switch between the first pod and the second one from the +new deployment on subsequent requests. + +```console +$ curl -vvv --header "Host: backends.example" "http://${GATEWAY_HOST}/get" +... +> GET /get HTTP/1.1 +> Host: backends.example +> User-Agent: curl/7.81.0 +> Accept: */* +> add-header: something +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< content-length: 474 +< x-envoy-upstream-service-time: 0 +< server: envoy +< +... + "namespace": "default", + "ingress": "", + "service": "", + "pod": "backend-75bcd4c969-lsxpz" +... +``` + +## Weighted backendRefs + +If multiple backendRefs are configured and an un-even traffic split between the backends is desired, then the `weight` +field can be used to control the weight of requests to each backend. If weight is not configured for a backendRef it is +assumed to be `1`. + +The [weight field in a backendRef][backendRefs] controls the distribution of the traffic split. The proportion of +requests to a single backendRef is calculated by dividing its `weight` by the sum of all backendRef weights in the +HTTPRoute. The weight is not a percentage and the sum of all weights does not need to add up to 100. + +The HTTPRoute below will configure the gateway to send 80% of the traffic to the backend service, and 20% to the +backend-2 service. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +## Invalid backendRefs + +backendRefs can be considered invalid for the following reasons: + +- The `group` field is configured to something other than `""`. Currently, only the core API group (specified by + omitting the group field or setting it to an empty string) is supported +- The `kind` field is configured to anything other than `Service`. Envoy Gateway currently only supports Kubernetes + Service backendRefs +- The backendRef configures a service with a `namespace` not permitted by any existing ReferenceGrants +- The `port` field is not configured or is configured to a port that does not exist on the Service +- The named Service configured by the backendRef cannot be found + +Modifying the above example to make the backend-2 backendRef invalid by using a port that does not exist on the Service +will result in 80% of the traffic being sent to the backend service, and 20% of the traffic receiving an HTTP response +with status code `500`. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Querying `backends.example/get` should result in `200` responses 80% of the time, and `500` responses 20% of the time. + +```console +$ curl -vvv --header "Host: backends.example" "http://${GATEWAY_HOST}/get" +> GET /get HTTP/1.1 +> Host: backends.example +> User-Agent: curl/7.81.0 +> Accept: */* +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 500 Internal Server Error +< server: envoy +< content-length: 0 +< +``` + +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute/ +[backendRefs]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.BackendRef diff --git a/site/content/en/docs/tasks/traffic/http-urlrewrite.md b/site/content/en/docs/tasks/traffic/http-urlrewrite.md new file mode 100644 index 00000000000..0ebb7595c22 --- /dev/null +++ b/site/content/en/docs/tasks/traffic/http-urlrewrite.md @@ -0,0 +1,405 @@ +--- +title: "HTTP URL Rewrite" +--- + +[HTTPURLRewriteFilter][] defines a filter that modifies a request during forwarding. At most one of these filters may be +used on a Route rule. This MUST NOT be used on the same Route rule as a HTTPRequestRedirect filter. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## Rewrite URL Prefix Path + +You can configure to rewrite the prefix in the url like below. In this example, any curls to +`http://${GATEWAY_HOST}/get/xxx` will be rewritten to `http://${GATEWAY_HOST}/replace/xxx`. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The HTTPRoute status should indicate that it has been accepted and is bound to the example Gateway. + +```shell +kubectl get httproute/http-filter-url-rewrite -o yaml +``` + +Get the Gateway's address: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +Querying `http://${GATEWAY_HOST}/get/origin/path` should rewrite to +`http://${GATEWAY_HOST}/replace/origin/path`. + +```console +$ curl -L -vvv --header "Host: path.rewrite.example" "http://${GATEWAY_HOST}/get/origin/path" +... +> GET /get/origin/path HTTP/1.1 +> Host: path.rewrite.example +> User-Agent: curl/7.85.0 +> Accept: */* +> + +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< date: Wed, 21 Dec 2022 11:03:28 GMT +< content-length: 503 +< x-envoy-upstream-service-time: 0 +< server: envoy +< +{ + "path": "/replace/origin/path", + "host": "path.rewrite.example", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/7.85.0" + ], + "X-Envoy-Expected-Rq-Timeout-Ms": [ + "15000" + ], + "X-Envoy-Original-Path": [ + "/get/origin/path" + ], + "X-Forwarded-Proto": [ + "http" + ], + "X-Request-Id": [ + "fd84b842-9937-4fb5-83c7-61470d854b90" + ] + }, + "namespace": "default", + "ingress": "", + "service": "", + "pod": "backend-6fdd4b9bd8-8vlc5" +... +``` + +You can see that the `X-Envoy-Original-Path` is `/get/origin/path`, but the actual path is `/replace/origin/path`. + +## Rewrite URL Full Path + +You can configure to rewrite the fullpath in the url like below. In this example, any request sent to +`http://${GATEWAY_HOST}/get/origin/path/xxxx` will be rewritten to +`http://${GATEWAY_HOST}/force/replace/fullpath`. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The HTTPRoute status should indicate that it has been accepted and is bound to the example Gateway. + +```shell +kubectl get httproute/http-filter-url-rewrite -o yaml +``` + +Querying `http://${GATEWAY_HOST}/get/origin/path/extra` should rewrite the request to +`http://${GATEWAY_HOST}/force/replace/fullpath`. + +```console +$ curl -L -vvv --header "Host: path.rewrite.example" "http://${GATEWAY_HOST}/get/origin/path/extra" +... +> GET /get/origin/path/extra HTTP/1.1 +> Host: path.rewrite.example +> User-Agent: curl/7.85.0 +> Accept: */* +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< date: Wed, 21 Dec 2022 11:09:31 GMT +< content-length: 512 +< x-envoy-upstream-service-time: 0 +< server: envoy +< +{ + "path": "/force/replace/fullpath", + "host": "path.rewrite.example", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/7.85.0" + ], + "X-Envoy-Expected-Rq-Timeout-Ms": [ + "15000" + ], + "X-Envoy-Original-Path": [ + "/get/origin/path/extra" + ], + "X-Forwarded-Proto": [ + "http" + ], + "X-Request-Id": [ + "8ab774d6-9ffa-4faa-abbb-f45b0db00895" + ] + }, + "namespace": "default", + "ingress": "", + "service": "", + "pod": "backend-6fdd4b9bd8-8vlc5" +... +``` + +You can see that the `X-Envoy-Original-Path` is `/get/origin/path/extra`, but the actual path is +`/force/replace/fullpath`. + +## Rewrite Host Name + +You can configure to rewrite the hostname like below. In this example, any requests sent to +`http://${GATEWAY_HOST}/get` with `--header "Host: path.rewrite.example"` will rewrite host into `envoygateway.io`. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The HTTPRoute status should indicate that it has been accepted and is bound to the example Gateway. + +```shell +kubectl get httproute/http-filter-url-rewrite -o yaml +``` + +Querying `http://${GATEWAY_HOST}/get` with `--header "Host: path.rewrite.example"` will rewrite host into +`envoygateway.io`. + +```console +$ curl -L -vvv --header "Host: path.rewrite.example" "http://${GATEWAY_HOST}/get" +... +> GET /get HTTP/1.1 +> Host: path.rewrite.example +> User-Agent: curl/7.85.0 +> Accept: */* +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< date: Wed, 21 Dec 2022 11:15:15 GMT +< content-length: 481 +< x-envoy-upstream-service-time: 0 +< server: envoy +< +{ + "path": "/get", + "host": "envoygateway.io", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/7.85.0" + ], + "X-Envoy-Expected-Rq-Timeout-Ms": [ + "15000" + ], + "X-Forwarded-Host": [ + "path.rewrite.example" + ], + "X-Forwarded-Proto": [ + "http" + ], + "X-Request-Id": [ + "39aa447c-97b9-45a3-a675-9fb266ab1af0" + ] + }, + "namespace": "default", + "ingress": "", + "service": "", + "pod": "backend-6fdd4b9bd8-8vlc5" +... +``` + +You can see that the `X-Forwarded-Host` is `path.rewrite.example`, but the actual host is `envoygateway.io`. + +[HTTPURLRewriteFilter]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPURLRewriteFilter diff --git a/site/content/en/docs/tasks/traffic/http3.md b/site/content/en/docs/tasks/traffic/http3.md new file mode 100644 index 00000000000..af95ab8ba57 --- /dev/null +++ b/site/content/en/docs/tasks/traffic/http3.md @@ -0,0 +1,136 @@ +--- +title: "HTTP3" +--- + +This task will help you get started using HTTP3 using EG. +This task uses a self-signed CA, so it should be used for testing and demonstration purposes only. + +## Prerequisites + +- OpenSSL to generate TLS assets. + +## Installation + +Follow the steps from the [Quickstart](../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## TLS Certificates + +Generate the certificates and keys used by the Gateway to terminate client TLS connections. + +Create a root certificate and private key to sign certificates: + +```shell +openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -subj '/O=example Inc./CN=example.com' -keyout example.com.key -out example.com.crt +``` + +Create a certificate and a private key for `www.example.com`: + +```shell +openssl req -out www.example.com.csr -newkey rsa:2048 -nodes -keyout www.example.com.key -subj "/CN=www.example.com/O=example organization" +openssl x509 -req -days 365 -CA example.com.crt -CAkey example.com.key -set_serial 0 -in www.example.com.csr -out www.example.com.crt +``` + +Store the cert/key in a Secret: + +```shell +kubectl create secret tls example-cert --key=www.example.com.key --cert=www.example.com.crt +``` + +Update the Gateway from the Quickstart to include an HTTPS listener that listens on port `443` and references the +`example-cert` Secret: + +```shell +kubectl patch gateway eg --type=json --patch ' + - op: add + path: /spec/listeners/- + value: + name: https + protocol: HTTPS + port: 443 + tls: + mode: Terminate + certificateRefs: + - kind: Secret + group: "" + name: example-cert + ' +``` + +Apply the following ClientTrafficPolicy to enable HTTP3 + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the Gateway status: + +```shell +kubectl get gateway/eg -o yaml +``` + +## Testing + +{{< tabpane text=true >}} +{{% tab header="With External LoadBalancer Support" %}} + +Get the External IP of the Gateway: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +Query the example app through the Gateway: + +The below example uses a custom docker image with custom `curl` binary with built-in http3. + +```shell +docker run --net=host --rm ghcr.io/macbre/curl-http3 curl -kv --http3 -HHost:www.example.com --resolve "www.example.com:443:${GATEWAY_HOST}" https://www.example.com/get +``` + +{{% /tab %}} +{{% tab header="Without LoadBalancer Support" %}} + +It is not possible at the moment to port-forward UDP protocol in kubernetes service +check out https://github.com/kubernetes/kubernetes/issues/47862. +Hence we need external loadbalancer to test this feature out. + +{{% /tab %}} +{{< /tabpane >}} diff --git a/site/content/en/docs/tasks/traffic/load-balancing.md b/site/content/en/docs/tasks/traffic/load-balancing.md new file mode 100644 index 00000000000..90a816e7bc3 --- /dev/null +++ b/site/content/en/docs/tasks/traffic/load-balancing.md @@ -0,0 +1,922 @@ +--- +title: "Load Balancing" +--- + +[Envoy load balancing][] is a way of distributing traffic between multiple hosts within a single upstream cluster +in order to effectively make use of available resources. + +Envoy Gateway supports the following load balancing policies: + +- **Round Robin**: a simple policy in which each available upstream host is selected in round robin order. +- **Random**: load balancer selects a random available host. +- **Least Request**: load balancer uses different algorithms depending on whether hosts have the same or different weights. +- **Consistent Hash**: load balancer implements consistent hashing to upstream hosts. + +Envoy Gateway introduces a new CRD called [BackendTrafficPolicy][] that allows the user to describe their desired load balancing polices. +This instantiated resource can be linked to a [Gateway][], [HTTPRoute][] or [GRPCRoute][] resource. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +For better testing the load balancer, you can add more hosts in upstream cluster by increasing the replicas of one deployment: + +```shell +kubectl patch deployment backend -n default -p '{"spec": {"replicas": 4}}' +``` + +### Install the hey load testing tool + +Install the `Hey` CLI tool, this tool will be used to generate load and measure response times. + +Follow the installation instruction from the [Hey project] docs. + +## Round Robin + +This example will create a Load Balancer with Round Robin policy via [BackendTrafficPolicy][]. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The `hey` tool will be used to generate 100 concurrent requests. + +```shell +hey -n 100 -c 100 -host "www.example.com" http://${GATEWAY_HOST}/round +``` + +```console +Summary: + Total: 0.0487 secs + Slowest: 0.0440 secs + Fastest: 0.0181 secs + Average: 0.0307 secs + Requests/sec: 2053.1676 + + Total data: 50500 bytes + Size/request: 505 bytes + +Response time histogram: + 0.018 [1] |■■ + 0.021 [2] |■■■■ + 0.023 [10] |■■■■■■■■■■■■■■■■■■■■■■ + 0.026 [16] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.028 [7] |■■■■■■■■■■■■■■■■ + 0.031 [10] |■■■■■■■■■■■■■■■■■■■■■■ + 0.034 [17] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.036 [18] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.039 [11] |■■■■■■■■■■■■■■■■■■■■■■■■ + 0.041 [6] |■■■■■■■■■■■■■ + 0.044 [2] |■■■■ +``` + +As a result, you can see all available upstream hosts receive traffics evenly. + +```shell +kubectl get pods -l app=backend --no-headers -o custom-columns=":metadata.name" | while read -r pod; do echo "$pod: received $(($(kubectl logs $pod | wc -l) - 2)) requests"; done +``` + +```console +backend-69fcff487f-2gfp7: received 26 requests +backend-69fcff487f-69g8c: received 25 requests +backend-69fcff487f-bqwpr: received 24 requests +backend-69fcff487f-kbn8l: received 25 requests +``` + +You should note that this results may vary, the output here is for reference purpose only. + +## Random + +This example will create a Load Balancer with Random policy via [BackendTrafficPolicy][]. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The `hey` tool will be used to generate 1000 concurrent requests. + +```shell +hey -n 1000 -c 100 -host "www.example.com" http://${GATEWAY_HOST}/random +``` + +```console +Summary: + Total: 0.2624 secs + Slowest: 0.0851 secs + Fastest: 0.0007 secs + Average: 0.0179 secs + Requests/sec: 3811.3020 + + Total data: 506000 bytes + Size/request: 506 bytes + +Response time histogram: + 0.001 [1] | + 0.009 [421] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.018 [219] |■■■■■■■■■■■■■■■■■■■■■ + 0.026 [118] |■■■■■■■■■■■ + 0.034 [64] |■■■■■■ + 0.043 [73] |■■■■■■■ + 0.051 [41] |■■■■ + 0.060 [22] |■■ + 0.068 [19] |■■ + 0.077 [13] |■ + 0.085 [9] |■ +``` + +As a result, you can see all available upstream hosts receive traffics randomly. + +```shell +kubectl get pods -l app=backend --no-headers -o custom-columns=":metadata.name" | while read -r pod; do echo "$pod: received $(($(kubectl logs $pod | wc -l) - 2)) requests"; done +``` + +```console +backend-69fcff487f-bf6lm: received 246 requests +backend-69fcff487f-gwmqk: received 256 requests +backend-69fcff487f-mzngr: received 230 requests +backend-69fcff487f-xghqq: received 268 requests +``` + +You should note that this results may vary, the output here is for reference purpose only. + +## Least Request + +This example will create a Load Balancer with Least Request policy via [BackendTrafficPolicy][]. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The `hey` tool will be used to generate 100 concurrent requests. + +```shell +hey -n 100 -c 100 -host "www.example.com" http://${GATEWAY_HOST}/least +``` + +```console +Summary: + Total: 0.0489 secs + Slowest: 0.0479 secs + Fastest: 0.0054 secs + Average: 0.0297 secs + Requests/sec: 2045.9317 + + Total data: 50500 bytes + Size/request: 505 bytes + +Response time histogram: + 0.005 [1] |■■ + 0.010 [1] |■■ + 0.014 [8] |■■■■■■■■■■■■■■■ + 0.018 [6] |■■■■■■■■■■■ + 0.022 [11] |■■■■■■■■■■■■■■■■■■■■ + 0.027 [7] |■■■■■■■■■■■■■ + 0.031 [15] |■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.035 [13] |■■■■■■■■■■■■■■■■■■■■■■■■ + 0.039 [22] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.044 [12] |■■■■■■■■■■■■■■■■■■■■■■ + 0.048 [4] |■■■■■■■ +``` + +As a result, you can see all available upstream hosts receive traffics randomly, +and host `backend-69fcff487f-6l2pw` receives fewer requests than others. + +```shell +kubectl get pods -l app=backend --no-headers -o custom-columns=":metadata.name" | while read -r pod; do echo "$pod: received $(($(kubectl logs $pod | wc -l) - 2)) requests"; done +``` + +```console +backend-69fcff487f-59hvs: received 24 requests +backend-69fcff487f-6l2pw: received 19 requests +backend-69fcff487f-ktsx4: received 30 requests +backend-69fcff487f-nqxc7: received 27 requests +``` + +If you send one more requests to the `${GATEWAY_HOST}/least`, you can tell that host `backend-69fcff487f-6l2pw` is very likely +to get the attention of load balancer and receive this request. + +```console +backend-69fcff487f-59hvs: received 24 requests +backend-69fcff487f-6l2pw: received 20 requests +backend-69fcff487f-ktsx4: received 30 requests +backend-69fcff487f-nqxc7: received 27 requests +``` + +You should note that this results may vary, the output here is for reference purpose only. + +## Consistent Hash + +This example will create a Load Balancer with Consistent Hash policy via [BackendTrafficPolicy][]. + +The underlying consistent hash algorithm that Envoy Gateway utilise is [Maglev][], and it can derive hash from following aspects: + +- **SourceIP** +- **Header** +- **Cookie** + +They are also the supported value as consistent hash type. + +### Source IP + +This example will create a Load Balancer with Source IP based Consistent Hash policy. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The `hey` tool will be used to generate 100 concurrent requests. + +```shell +hey -n 100 -c 100 -host "www.example.com" http://${GATEWAY_HOST}/source +``` + +```console +Summary: + Total: 0.0539 secs + Slowest: 0.0500 secs + Fastest: 0.0198 secs + Average: 0.0340 secs + Requests/sec: 1856.5666 + + Total data: 50600 bytes + Size/request: 506 bytes + +Response time histogram: + 0.020 [1] |■■ + 0.023 [5] |■■■■■■■■■■■ + 0.026 [12] |■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.029 [16] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.032 [11] |■■■■■■■■■■■■■■■■■■■■■■■■ + 0.035 [7] |■■■■■■■■■■■■■■■■ + 0.038 [8] |■■■■■■■■■■■■■■■■■■ + 0.041 [18] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.044 [15] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.047 [4] |■■■■■■■■■ + 0.050 [3] |■■■■■■■ +``` + +As a result, you can see all traffics are routed to only one upstream host, since the client that send requests +has the same source IP. + +```shell +kubectl get pods -l app=backend --no-headers -o custom-columns=":metadata.name" | while read -r pod; do echo "$pod: received $(($(kubectl logs $pod | wc -l) - 2)) requests"; done +``` + +```console +backend-69fcff487f-grzkj: received 0 requests +backend-69fcff487f-n4d8w: received 100 requests +backend-69fcff487f-tb7zx: received 0 requests +backend-69fcff487f-wbzpg: received 0 requests +``` + +You can try different client to send out these requests, the upstream host that receives traffics may vary. + +### Header + +This example will create a Load Balancer with Header based Consistent Hash policy. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The `hey` tool will be used to generate 100 concurrent requests. + +```shell +hey -n 100 -c 100 -host "www.example.com" -H "FooBar: 1.2.3.4" http://${GATEWAY_HOST}/header +``` + +```console +Summary: + Total: 0.0579 secs + Slowest: 0.0510 secs + Fastest: 0.0323 secs + Average: 0.0431 secs + Requests/sec: 1728.6064 + + Total data: 53800 bytes + Size/request: 538 bytes + +Response time histogram: + 0.032 [1] |■■ + 0.034 [3] |■■■■■■ + 0.036 [1] |■■ + 0.038 [1] |■■ + 0.040 [7] |■■■■■■■■■■■■■■ + 0.042 [20] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.044 [20] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.045 [20] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.047 [16] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.049 [9] |■■■■■■■■■■■■■■■■■■ + 0.051 [2] |■■■■ +``` + +As a result, you can see all traffics are routed to only one upstream host, since the header of all requests are the same. + +```shell +kubectl get pods -l app=backend --no-headers -o custom-columns=":metadata.name" | while read -r pod; do echo "$pod: received $(($(kubectl logs $pod | wc -l) - 2)) requests"; done +``` + +```console +backend-69fcff487f-dvt9r: received 0 requests +backend-69fcff487f-f8qdl: received 100 requests +backend-69fcff487f-gnpm4: received 0 requests +backend-69fcff487f-t2pgm: received 0 requests +``` + +You can try to add different header to these requests, and the upstream host that receives traffics may vary. +The following output happens when you use `hey` to send another 100 requests with header `FooBar: 5.6.7.8`. + +```console +backend-69fcff487f-dvt9r: received 0 requests +backend-69fcff487f-f8qdl: received 100 requests +backend-69fcff487f-gnpm4: received 100 requests +backend-69fcff487f-t2pgm: received 0 requests +``` + +### Cookie + +This example will create a Load Balancer with Cookie based Consistent Hash policy. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +By sending 10 request with `curl` to the `${GATEWAY_HOST}/cookie`, you can see that all requests got routed to only +one upstream host, since they have same cookie setting. + +```shell +for i in {1..10}; do curl -I --header "Host: www.example.com" --cookie "FooBar=1.2.3.4" http://${GATEWAY_HOST}/cookie ; sleep 1; done +``` + +```shell +kubectl get pods -l app=backend --no-headers -o custom-columns=":metadata.name" | while read -r pod; do echo "$pod: received $(($(kubectl logs $pod | wc -l) - 2)) requests"; done +``` + +```console +backend-69fcff487f-5dxz9: received 0 requests +backend-69fcff487f-gpvl2: received 0 requests +backend-69fcff487f-pglgv: received 10 requests +backend-69fcff487f-qxr74: received 0 requests +``` + +You can try to set different cookie to these requests, the upstream host that receives traffics may vary. +The following output happens when you use `curl` to send another 10 requests with cookie `FooBar: 5.6.7.8`. + +```console +backend-69fcff487f-dvt9r: received 0 requests +backend-69fcff487f-f8qdl: received 0 requests +backend-69fcff487f-gnpm4: received 10 requests +backend-69fcff487f-t2pgm: received 10 requests +``` + +If the cookie has not been set in one request, Envoy Gateway will auto-generate a cookie for this request +according to the `ttl` and `attributes` field. + +In this example, the following cookie will be generated (see `set-cookie` header in response) if sending a request without cookie: + +```shell +curl -v --header "Host: www.example.com" http://${GATEWAY_HOST}/cookie +``` + +```console +> GET /cookie HTTP/1.1 +> Host: www.example.com +> User-Agent: curl/7.74.0 +> Accept: */* +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< date: Fri, 19 Jul 2024 16:49:57 GMT +< content-length: 458 +< set-cookie: FooBar="88358b9442700c56"; Max-Age=60; SameSite=Strict; HttpOnly +< +{ + "path": "/cookie", + "host": "www.example.com", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/7.74.0" + ], + "X-Envoy-Internal": [ + "true" + ], + "X-Forwarded-For": [ + "10.244.0.1" + ], + "X-Forwarded-Proto": [ + "http" + ], + "X-Request-Id": [ + "1adeaaf7-d45c-48c8-9a4d-eadbccb2fd50" + ] + }, + "namespace": "default", + "ingress": "", + "service": "", + "pod": "backend-69fcff487f-5dxz9" +``` + + +[Envoy load balancing]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/load_balancing/overview +[BackendTrafficPolicy]: ../../../api/extension_types#backendtrafficpolicy +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway/ +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute/ +[GRPCRoute]: https://gateway-api.sigs.k8s.io/api-types/grpcroute/ +[Hey project]: https://github.com/rakyll/hey +[Maglev]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/load_balancing/load_balancers#maglev diff --git a/site/content/en/docs/tasks/traffic/local-rate-limit.md b/site/content/en/docs/tasks/traffic/local-rate-limit.md new file mode 100644 index 00000000000..37fb5590a44 --- /dev/null +++ b/site/content/en/docs/tasks/traffic/local-rate-limit.md @@ -0,0 +1,414 @@ +--- +title: "Local Rate Limit" +--- + +Rate limit is a feature that allows the user to limit the number of incoming requests to a predefined value based on attributes within the traffic flow. + +Here are some reasons why you may want to implement Rate limits + +* To prevent malicious activity such as DDoS attacks. +* To prevent applications and its resources (such as a database) from getting overloaded. +* To create API limits based on user entitlements. + +Envoy Gateway supports two types of rate limiting: [Global rate limiting][] and [Local rate limiting][]. + +[Local rate limiting][] applies rate limits to the traffic flowing through a single instance of Envoy proxy. This means +that if the data plane has 2 replicas of Envoy running, and the rate limit is 10 requests/second, each replica will allow +10 requests/second. This is in contrast to [Global Rate Limiting][] which applies rate limits to the traffic flowing through +all instances of Envoy proxy. + +Envoy Gateway introduces a new CRD called [BackendTrafficPolicy][] that allows the user to describe their rate limit intent. +This instantiated resource can be linked to a [Gateway][], [HTTPRoute][] or [GRPCRoute][] resource. + +**Note:** Limit is applied per route. Even if a [BackendTrafficPolicy][] targets a gateway, each route in that gateway +still has a separate rate limit bucket. For example, if a gateway has 2 routes, and the limit is 100r/s, then each route +has its own 100r/s rate limit bucket. + +## Prerequisites + +### Install Envoy Gateway + +* Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the HTTPRoute example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## Rate Limit Specific User + +Here is an example of a rate limit implemented by the application developer to limit a specific user by matching on a custom `x-user-id` header +with a value set to `one`. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +### HTTPRoute + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The HTTPRoute status should indicate that it has been accepted and is bound to the example Gateway. + +```shell +kubectl get httproute/http-ratelimit -o yaml +``` + +Get the Gateway's address: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +Let's query `ratelimit.example/get` 4 times. We should receive a `200` response from the example Gateway for the first 3 requests +and then receive a `429` status code for the 4th request since the limit is set at 3 requests/Hour for the request which contains the header `x-user-id` +and value `one`. + +```shell +for i in {1..4}; do curl -I --header "Host: ratelimit.example" --header "x-user-id: one" http://${GATEWAY_HOST}/get ; sleep 1; done +``` + +```console +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:31 GMT +content-length: 460 +x-envoy-upstream-service-time: 4 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:32 GMT +content-length: 460 +x-envoy-upstream-service-time: 2 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:33 GMT +content-length: 460 +x-envoy-upstream-service-time: 0 +server: envoy + +HTTP/1.1 429 Too Many Requests +x-envoy-ratelimited: true +date: Wed, 08 Feb 2023 02:33:34 GMT +server: envoy +transfer-encoding: chunked + +``` + +You should be able to send requests with the `x-user-id` header and a different value and receive successful responses from the server. + +```shell +for i in {1..4}; do curl -I --header "Host: ratelimit.example" --header "x-user-id: two" http://${GATEWAY_HOST}/get ; sleep 1; done +``` + +```console +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:34:36 GMT +content-length: 460 +x-envoy-upstream-service-time: 0 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:34:37 GMT +content-length: 460 +x-envoy-upstream-service-time: 0 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:34:38 GMT +content-length: 460 +x-envoy-upstream-service-time: 0 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:34:39 GMT +content-length: 460 +x-envoy-upstream-service-time: 0 +server: envoy + +``` + +## Rate Limit All Requests + +This example shows you how to rate limit all requests matching the HTTPRoute rule at 3 requests/Hour by leaving the `clientSelectors` field unset. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +### HTTPRoute + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +```shell +for i in {1..4}; do curl -I --header "Host: ratelimit.example" http://${GATEWAY_HOST}/get ; sleep 1; done +``` + +```console +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:31 GMT +content-length: 460 +x-envoy-upstream-service-time: 4 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:32 GMT +content-length: 460 +x-envoy-upstream-service-time: 2 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:33 GMT +content-length: 460 +x-envoy-upstream-service-time: 0 +server: envoy + +HTTP/1.1 429 Too Many Requests +x-envoy-ratelimited: true +date: Wed, 08 Feb 2023 02:33:34 GMT +server: envoy +transfer-encoding: chunked + +``` + +**Note:** Local rate limiting does not support `distinct` matching. If you want to rate limit based on distinct values, +you should use [Global Rate Limiting][]. + +[Global Rate Limiting]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_features/global_rate_limiting +[Local rate limiting]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_features/local_rate_limiting +[BackendTrafficPolicy]: ../../../api/extension_types#backendtrafficpolicy +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway/ +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute/ +[GRPCRoute]: https://gateway-api.sigs.k8s.io/api-types/grpcroute/ diff --git a/site/content/en/docs/tasks/traffic/multicluster-service.md b/site/content/en/docs/tasks/traffic/multicluster-service.md new file mode 100644 index 00000000000..d0fd7a83fb1 --- /dev/null +++ b/site/content/en/docs/tasks/traffic/multicluster-service.md @@ -0,0 +1,86 @@ +--- +title: "Multicluster Service Routing" +--- + +The Multicluster Service API ServiceImport object can be used as part of the GatewayAPI backendRef for configuring routes. For more information about multicluster service API follow [sig documentation](https://multicluster.sigs.k8s.io/concepts/multicluster-services-api/). + +We will use [Submariner project](https://github.com/submariner-io/submariner) for setting up the multicluster environment for exporting the service to be routed from peer clusters. + +## Setting KIND clusters and installing Submariner. + +- We will be using KIND clusters to demonstrate this example. + +```shell +git clone https://github.com/submariner-io/submariner-operator +cd submariner-operator +make clusters +``` + +Note: remain in submariner-operator directory for the rest of the steps in this section + +- Install subctl: + +```shell +curl -Ls https://get.submariner.io | VERSION=v0.14.6 bash +``` + +- Set up multicluster service API and submariner for cross cluster traffic using ServiceImport + +```shell +subctl deploy-broker --kubeconfig output/kubeconfigs/kind-config-cluster1 --globalnet +subctl join --kubeconfig output/kubeconfigs/kind-config-cluster1 broker-info.subm --clusterid cluster1 --natt=false +subctl join --kubeconfig output/kubeconfigs/kind-config-cluster2 broker-info.subm --clusterid cluster2 --natt=false +``` + +Once the above steps are done and all the pods are up in both the clusters. We are ready for installing envoy gateway. + +## Install EnvoyGateway + +Install the Gateway API CRDs and Envoy Gateway in cluster1: + +```shell +helm install eg oci://docker.io/envoyproxy/gateway-helm --version v0.0.0-latest -n envoy-gateway-system --create-namespace --kubeconfig output/kubeconfigs/kind-config-cluster1 +``` + +Wait for Envoy Gateway to become available: + +```shell +kubectl wait --timeout=5m -n envoy-gateway-system deployment/envoy-gateway --for=condition=Available --kubeconfig output/kubeconfigs/kind-config-cluster1 +``` + +## Install Application + +Install the backend application in cluster2 and export it through subctl command. + +```shell +kubectl apply -f https://raw.githubusercontent.com/envoyproxy/gateway/latest/examples/kubernetes/application.yaml --kubeconfig output/kubeconfigs/kind-config-cluster2 +subctl export service backend --namespace default --kubeconfig output/kubeconfigs/kind-config-cluster2 +``` + +## Create Gateway API Objects + +Create the Gateway API objects GatewayClass, Gateway and HTTPRoute in cluster1 to set up the routing. + +```shell +kubectl apply -f https://raw.githubusercontent.com/envoyproxy/gateway/latest/examples/kubernetes/multicluster-service.yaml --kubeconfig output/kubeconfigs/kind-config-cluster1 +``` + +## Testing the Configuration + +Get the name of the Envoy service created the by the example Gateway: + +```shell +export ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}') +``` + +Port forward to the Envoy service: + +```shell +kubectl -n envoy-gateway-system port-forward service/${ENVOY_SERVICE} 8888:80 & +``` + +Curl the example app through Envoy proxy: + +```shell +curl --verbose --header "Host: www.example.com" http://localhost:8888/get +``` diff --git a/site/content/en/docs/tasks/traffic/retry.md b/site/content/en/docs/tasks/traffic/retry.md new file mode 100644 index 00000000000..4de8f604f96 --- /dev/null +++ b/site/content/en/docs/tasks/traffic/retry.md @@ -0,0 +1,147 @@ +--- +title: Retry +--- + +A retry setting specifies the maximum number of times an Envoy proxy attempts to connect to a service if the initial call fails. Retries can enhance service availability and application performance by making sure that calls don’t fail permanently because of transient problems such as a temporarily overloaded service or network. The interval between retries prevents the called service from being overwhelmed with requests. + +Envoy Gateway supports the following retry settings: +- **NumRetries**: is the number of retries to be attempted. Defaults to 2. +- **RetryOn**: specifies the retry trigger condition. +- **PerRetryPolicy**: is the retry policy to be applied per retry attempt. + +Envoy Gateway introduces a new CRD called [BackendTrafficPolicy](../../../api/extension_types#backendtrafficpolicy) that allows the user to describe their desired retry settings. This instantiated resource can be linked to a [Gateway](https://gateway-api.sigs.k8s.io/api-types/gateway/), [HTTPRoute](https://gateway-api.sigs.k8s.io/api-types/httproute/) or [GRPCRoute](https://gateway-api.sigs.k8s.io/api-types/grpcroute/) resource. + +**Note**: There are distinct circuit breaker counters for each `BackendReference` in an `xRoute` rule. Even if a `BackendTrafficPolicy` targets a `Gateway`, each `BackendReference` in that gateway still has separate circuit breaker counter. + +## Prerequisites + +Follow the installation step from the [Quickstart](../../quickstart) to install Envoy Gateway and sample resources. + +## Test and customize retry settings + +Before applying a `BackendTrafficPolicy` with retry setting to a route, let's test the default retry settings. + +```shell +curl -v -H "Host: www.example.com" "http://${GATEWAY_HOST}/status/500" +``` + +It will return `500` response immediately. + +```console +* Trying 172.18.255.200:80... +* Connected to 172.18.255.200 (172.18.255.200) port 80 +> GET /status/500 HTTP/1.1 +> Host: www.example.com +> User-Agent: curl/8.4.0 +> Accept: */* +> +< HTTP/1.1 500 Internal Server Error +< date: Fri, 01 Mar 2024 15:12:55 GMT +< content-length: 0 +< +* Connection #0 to host 172.18.255.200 left intact +``` + +Let's create a `BackendTrafficPolicy` with a retry setting. + +The request will be retried 5 times with a 100ms base interval and a 10s maximum interval. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Execute the test again. + +```shell +curl -v -H "Host: www.example.com" "http://${GATEWAY_HOST}/status/500" +``` + +It will return `500` response after a few while. + +```console +* Trying 172.18.255.200:80... +* Connected to 172.18.255.200 (172.18.255.200) port 80 +> GET /status/500 HTTP/1.1 +> Host: www.example.com +> User-Agent: curl/8.4.0 +> Accept: */* +> +< HTTP/1.1 500 Internal Server Error +< date: Fri, 01 Mar 2024 15:15:53 GMT +< content-length: 0 +< +* Connection #0 to host 172.18.255.200 left intact +``` + +Let's check the stats to see the retry behavior. + +```shell +egctl x stats envoy-proxy -n envoy-gateway-system -l gateway.envoyproxy.io/owning-gateway-name=eg,gateway.envoyproxy.io/owning-gateway-namespace=default | grep "envoy_cluster_upstream_rq_retry{envoy_cluster_name=\"httproute/default/backend/rule/0\"}" +``` + +You will expect to see the stats. + +```console +envoy_cluster_upstream_rq_retry{envoy_cluster_name="httproute/default/backend/rule/0"} 5 +``` diff --git a/site/content/en/docs/tasks/traffic/routing-outside-kubernetes.md b/site/content/en/docs/tasks/traffic/routing-outside-kubernetes.md new file mode 100644 index 00000000000..7382b9cb78d --- /dev/null +++ b/site/content/en/docs/tasks/traffic/routing-outside-kubernetes.md @@ -0,0 +1,168 @@ +--- +title: "Routing outside Kubernetes" +--- + +Routing to endpoints outside the Kubernetes cluster where Envoy Gateway and its corresponding Envoy Proxy fleet is +running is a common use case. This can be achieved by: +- defining FQDN addresses in a [EndpointSlice][] (covered in this document) +- defining a [Backend][] resource, as described in the [Backend Task][]. + +## Installation + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## Configuration + +Define a Service and EndpointSlice that represents https://httpbin.org + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Update the [Gateway][] to include a TLS Listener on port 443 + +```shell +kubectl patch gateway eg --type=json --patch ' + - op: add + path: /spec/listeners/- + value: + name: tls + protocol: TLS + port: 443 + tls: + mode: Passthrough + ' +``` + +Add a [TLSRoute][] that can route incoming traffic to the above backend that we created + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Get the Gateway address: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +Send a request and view the response: + +```shell +curl -I -HHost:httpbin.org --resolve "httpbin.org:443:${GATEWAY_HOST}" https://httpbin.org/ +``` + +[EndpointSlice]: https://kubernetes.io/docs/concepts/services-networking/endpoint-slices/ +[Backend]: ../../api/extension_types#backend +[Backend Task]: ./backend.md +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway/ +[TLSRoute]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.TLSRoute diff --git a/site/content/en/docs/tasks/traffic/tcp-routing.md b/site/content/en/docs/tasks/traffic/tcp-routing.md new file mode 100644 index 00000000000..d36f145e266 --- /dev/null +++ b/site/content/en/docs/tasks/traffic/tcp-routing.md @@ -0,0 +1,483 @@ +--- +title: "TCP Routing" +--- + +[TCPRoute][] provides a way to route TCP requests. When combined with a Gateway listener, it can be used to forward +connections on the port specified by the listener to a set of backends specified by the TCPRoute. To learn more about +HTTP routing, refer to the [Gateway API documentation][]. + +## Installation + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## Configuration + +In this example, we have one Gateway resource and two TCPRoute resources that distribute the traffic with the following +rules: + +All TCP streams on port `8088` of the Gateway are forwarded to port 3001 of `foo` Kubernetes Service. +All TCP streams on port `8089` of the Gateway are forwarded to port 3002 of `bar` Kubernetes Service. +In this example two TCP listeners will be applied to the Gateway in order to route them to two separate backend +TCPRoutes, note that the protocol set for the listeners on the Gateway is TCP: + +Install the GatewayClass and a `tcp-gateway` Gateway first. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Install two services `foo` and `bar`, which are bound to `backend-1` and `backend-2`. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Install two TCPRoutes `tcp-app-1` and `tcp-app-2` with different `sectionName`: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +In the above example we separate the traffic for the two separate backend TCP Services by using the sectionName field in +the parentRefs: + +``` yaml +spec: + parentRefs: + - name: tcp-gateway + sectionName: foo +``` + +This corresponds directly with the name in the listeners in the Gateway: + +``` yaml + listeners: + - name: foo + protocol: TCP + port: 8088 + - name: bar + protocol: TCP + port: 8089 +``` + +In this way each TCPRoute "attaches" itself to a different port on the Gateway so that the `foo` service +is taking traffic for port `8088` from outside the cluster and `bar` service takes the port `8089` traffic. + +Before testing, please get the tcp-gateway Gateway's address first: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/tcp-gateway -o jsonpath='{.status.addresses[0].value}') +``` + +You can try to use nc to test the TCP connections of envoy gateway with different ports, and you can see them succeeded: + +```shell +nc -zv ${GATEWAY_HOST} 8088 + +nc -zv ${GATEWAY_HOST} 8089 +``` + +You can also try to send requests to envoy gateway and get responses as shown below: + +```shell +curl -i "http://${GATEWAY_HOST}:8088" + +HTTP/1.1 200 OK +Content-Type: application/json +X-Content-Type-Options: nosniff +Date: Tue, 03 Jan 2023 10:18:36 GMT +Content-Length: 267 + +{ + "path": "/", + "host": "xxx.xxx.xxx.xxx:8088", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/7.85.0" + ] + }, + "namespace": "default", + "ingress": "", + "service": "foo", + "pod": "backend-1-c6c5fb958-dl8vl" +} +``` + +You can see that the traffic routing to `foo` service when sending request to `8088` port. + +```shell +curl -i "http://${GATEWAY_HOST}:8089" + +HTTP/1.1 200 OK +Content-Type: application/json +X-Content-Type-Options: nosniff +Date: Tue, 03 Jan 2023 10:19:28 GMT +Content-Length: 267 + +{ + "path": "/", + "host": "xxx.xxx.xxx.xxx:8089", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/7.85.0" + ] + }, + "namespace": "default", + "ingress": "", + "service": "bar", + "pod": "backend-2-98fcff498-hcmgb" +} +``` + +You can see that the traffic routing to `bar` service when sending request to `8089` port. + +[TCPRoute]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.TCPRoute +[Gateway API documentation]: https://gateway-api.sigs.k8s.io/ diff --git a/site/content/en/docs/tasks/traffic/udp-routing.md b/site/content/en/docs/tasks/traffic/udp-routing.md new file mode 100644 index 00000000000..b9d8e379282 --- /dev/null +++ b/site/content/en/docs/tasks/traffic/udp-routing.md @@ -0,0 +1,170 @@ +--- +title: "UDP Routing" +--- + +The [UDPRoute][] resource allows users to configure UDP routing by matching UDP traffic and forwarding it to Kubernetes +backends. This task will use CoreDNS example to walk you through the steps required to configure UDPRoute on Envoy +Gateway. + +__Note:__ UDPRoute allows Envoy Gateway to operate as a non-transparent proxy between a UDP client and server. The lack +of transparency means that the upstream server will see the source IP and port of the Gateway instead of the client. +For additional information, refer to Envoy's [UDP proxy documentation][]. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## Installation + +Install CoreDNS in the Kubernetes cluster as the example backend. The installed CoreDNS is listening on +UDP port 53 for DNS lookups. + +```shell +kubectl apply -f https://raw.githubusercontent.com/envoyproxy/gateway/latest/examples/kubernetes/udp-routing-example-backend.yaml +``` + +Wait for the CoreDNS deployment to become available: + +```shell +kubectl wait --timeout=5m deployment/coredns --for=condition=Available +``` + +Update the Gateway from the Quickstart to include a UDP listener that listens on UDP port `5300`: + +```shell +kubectl patch gateway eg --type=json --patch ' + - op: add + path: /spec/listeners/- + value: + name: coredns + protocol: UDP + port: 5300 + allowedRoutes: + kinds: + - kind: UDPRoute + ' +``` + +Verify the Gateway status: + +```shell +kubectl get gateway/eg -o yaml +``` + +## Configuration + +Create a UDPRoute resource to route UDP traffic received on Gateway port 5300 to the CoredDNS backend. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the UDPRoute status: + +```shell +kubectl get udproute/coredns -o yaml +``` + +## Testing + +Get the External IP of the Gateway: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +Use `dig` command to query the dns entry foo.bar.com through the Gateway. + +```shell +dig @${GATEWAY_HOST} -p 5300 foo.bar.com +``` + +You should see the result of the dns query as the below output, which means that the dns query has been successfully +routed to the backend CoreDNS. + +Note: 49.51.177.138 is the resolved address of GATEWAY_HOST. + +```bash +; <<>> DiG 9.18.1-1ubuntu1.1-Ubuntu <<>> @49.51.177.138 -p 5300 foo.bar.com +; (1 server found) +;; global options: +cmd +;; Got answer: +;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 58125 +;; flags: qr aa rd; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 3 +;; WARNING: recursion requested but not available + +;; OPT PSEUDOSECTION: +; EDNS: version: 0, flags:; udp: 1232 +; COOKIE: 24fb86eba96ebf62 (echoed) +;; QUESTION SECTION: +;foo.bar.com. IN A + +;; ADDITIONAL SECTION: +foo.bar.com. 0 IN A 10.244.0.19 +_udp.foo.bar.com. 0 IN SRV 0 0 42376 . + +;; Query time: 1 msec +;; SERVER: 49.51.177.138#5300(49.51.177.138) (UDP) +;; WHEN: Fri Jan 13 10:20:34 UTC 2023 +;; MSG SIZE rcvd: 114 +``` + +## Clean-Up + +Follow the steps from the [Quickstart](../../quickstart) to uninstall Envoy Gateway. + +Delete the CoreDNS example manifest and the UDPRoute: + +```shell +kubectl delete deploy/coredns +kubectl delete service/coredns +kubectl delete cm/coredns +kubectl delete udproute/coredns +``` + +## Next Steps + +Checkout the [Developer Guide](../../../contributions/develop) to get involved in the project. + +[UDPRoute]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1alpha2.UDPRoute +[UDP proxy documentation]: https://www.envoyproxy.io/docs/envoy/latest/configuration/listeners/udp_filters/udp_proxy diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 6548b6b7bbb..4dc4ccd890c 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -755,7 +755,7 @@ _Appears in:_ | Field | Type | Required | Description | | --- | --- | --- | --- | -| `type` | _[ConsistentHashType](#consistenthashtype)_ | true | ConsistentHashType defines the type of input to hash on. Valid Type values are "SourceIP" or "Header". | +| `type` | _[ConsistentHashType](#consistenthashtype)_ | true | ConsistentHashType defines the type of input to hash on. Valid Type values are
"SourceIP",
"Header",
"Cookie". | | `header` | _[Header](#header)_ | false | Header configures the header hash policy when the consistent hash type is set to Header. | | `cookie` | _[Cookie](#cookie)_ | false | Cookie configures the cookie hash policy when the consistent hash type is set to Cookie. | | `tableSize` | _integer_ | false | The table size for consistent hashing, must be prime number limited to 5000011. | diff --git a/site/content/en/latest/concepts/_index.md b/site/content/en/latest/concepts/_index.md new file mode 100644 index 00000000000..4d568bd4491 --- /dev/null +++ b/site/content/en/latest/concepts/_index.md @@ -0,0 +1,5 @@ +--- +title: "Concepts" +weight: 1 +description: Learn about key concepts when working with Envoy Gateway +--- diff --git a/site/content/en/latest/concepts/concepts_overview.md b/site/content/en/latest/concepts/concepts_overview.md new file mode 100644 index 00000000000..31838b520f2 --- /dev/null +++ b/site/content/en/latest/concepts/concepts_overview.md @@ -0,0 +1,54 @@ ++++ +title = "Envoy Gateway Resources" ++++ + +There are several resources that play a part in enabling you to meet your Kubernetes ingress traffic handling needs. This page provides a brief overview of the resources you’ll be working with. + +## Overview + +![](/img/envoy-gateway-resources-overview.png) + +There are several resources that play a part in enabling you to meet your Kubernetes ingress traffic handling needs. This page provides a brief overview of the resources you’ll be working with. + +# Overview + +## Kubernetes Gateway API Resources +- **GatewayClass:** Defines a class of Gateways with common configuration. +- **Gateway:** Specifies how traffic can enter the cluster. +- **Routes:** **HTTPRoute, GRPCRoute, TLSRoute, TCPRoute, UDPRoute:** Define routing rules for different types of traffic. +## Envoy Gateway (EG) API Resources +- **EnvoyProxy:** Represents the deployment and configuration of the Envoy proxy within a Kubernetes cluster, managing its lifecycle and settings. +- **EnvoyPatchPolicy, ClientTrafficPolicy, SecurityPolicy, BackendTrafficPolicy, EnvoyExtensionPolicy, BackendTLSPolicy:** Additional policies and configurations specific to Envoy Gateway. +- **Backend:** A resource that makes routing to cluster-external backends easier and makes access to external processes via Unix Domain Sockets possible. + +| Resource | API | Required | Purpose | References | Description | +| ----------------------------------------------------------------------- | ----------- | -------- | ------------------ | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [GatewayClass][1] | Gateway API | Yes | Gateway Config | Core | Defines a class of Gateways with common configuration. | +| [Gateway][2] | Gateway API | Yes | Gateway Config | GatewayClass | Specifies how traffic can enter the cluster. | +| [HTTPRoute][3] [GRPCRoute][4] [TLSRoute][5] [TCPRoute][6] [UDPRoute][7] | Gateway API | Yes | Routing | Gateway | Define routing rules for different types of traffic. **Note:**_For simplicity these resources are referenced collectively as Route in the References column_ | +| [Backend][8] | EG API | No | Routing | N/A | Used for routing to cluster-external backends using FQDN or IP. Can also be used when you want to extend Envoy with external processes accessed via Unix Domain Sockets. | +| [ClientTrafficPolicy][9] | EG API | No | Traffic Handling | Gateway | Specifies policies for handling client traffic, including rate limiting, retries, and other client-specific configurations. | +| [BackendTrafficPolicy][10] | EG API | No | Traffic Handling | Gateway Route | Specifies policies for traffic directed towards backend services, including load balancing, health checks, and failover strategies. **Note:**_Most specific configuration wins_ | +| [SecurityPolicy][11] | EG API | No | Security | Gateway Route | Defines security-related policies such as authentication, authorization, and encryption settings for traffic handled by Envoy Gateway. **Note:**_Most specific configuration wins_ | +| [BackendTLSPolicy][12] | Gateway API | No | Security | Service | Defines TLS settings for backend connections, including certificate management, TLS version settings, and other security configurations. This policy is applied to Kubernetes Services. | +| [EnvoyProxy][13] | EG API | No | Customize & Extend | GatewayClass Gateway | The EnvoyProxy resource represents the deployment and configuration of the Envoy proxy itself within a Kubernetes cluster, managing its lifecycle and settings. **Note:**_Most specific configuration wins_ | +| [EnvoyPatchPolicy][14] | EG API | No | Customize & Extend | GatewayClass Gateway | This policy defines custom patches to be applied to Envoy Gateway resources, allowing users to tailor the configuration to their specific needs. **Note:**_Most specific configuration wins_ | +| [EnvoyExtensionPolicy][15] | EG API | No | Customize & Extend | Gateway Route, Backend | Allows for the configuration of Envoy proxy extensions, enabling custom behavior and functionality. **Note:**_Most specific configuration wins_ | + + + +[1]: https://gateway-api.sigs.k8s.io/api-types/gatewayclass/ +[2]: https://gateway-api.sigs.k8s.io/api-types/gateway/ +[3]: https://gateway-api.sigs.k8s.io/api-types/httproute/ +[4]: https://gateway-api.sigs.k8s.io/api-types/grpcroute/ +[5]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.TLSRoute +[6]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.TCPRoute +[7]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.UDPRoute +[8]: ../tasks/traffic/backend +[9]: ../api/extension_types#clienttrafficpolicy +[10]: ../api/extension_types#backendtrafficpolicy +[11]: ../api/extension_types#securitypolicy +[12]: https://gateway-api.sigs.k8s.io/api-types/backendtlspolicy/ +[13]: ../api/extension_types#envoyproxy +[14]: ../api/extension_types#envoypatchpolicy +[15]: ../api/extension_types#envoyextensionpolicy \ No newline at end of file diff --git a/site/content/en/latest/install/gateway-addons-helm-api.md b/site/content/en/latest/install/gateway-addons-helm-api.md index 591af36c35e..448aa91e504 100644 --- a/site/content/en/latest/install/gateway-addons-helm-api.md +++ b/site/content/en/latest/install/gateway-addons-helm-api.md @@ -40,6 +40,7 @@ An Add-ons Helm chart for Envoy Gateway | fluent-bit.config.service | string | `"[SERVICE]\n Daemon Off\n Flush {{ .Values.flush }}\n Log_Level {{ .Values.logLevel }}\n Parsers_File parsers.conf\n Parsers_File custom_parsers.conf\n HTTP_Server On\n HTTP_Listen 0.0.0.0\n HTTP_Port {{ .Values.metricsPort }}\n Health_Check On\n"` | | | fluent-bit.enabled | bool | `true` | | | fluent-bit.fullnameOverride | string | `"fluent-bit"` | | +| fluent-bit.image.repository | string | `"fluent/fluent-bit"` | | | fluent-bit.podAnnotations."fluentbit.io/exclude" | string | `"true"` | | | fluent-bit.podAnnotations."prometheus.io/path" | string | `"/api/v1/metrics/prometheus"` | | | fluent-bit.podAnnotations."prometheus.io/port" | string | `"2020"` | | diff --git a/site/content/en/latest/releases/_index.md b/site/content/en/latest/releases/_index.md deleted file mode 100644 index 382eb1dd20d..00000000000 --- a/site/content/en/latest/releases/_index.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: "Releases" -weight: 90 -description: This section includes Releases of Envoy Gateway. ---- diff --git a/site/content/en/latest/releases/v0.2.0-rc1.md b/site/content/en/latest/releases/v0.2.0-rc1.md deleted file mode 100644 index 59da2015f9d..00000000000 --- a/site/content/en/latest/releases/v0.2.0-rc1.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: "v0.2.0-rc.1" -publishdate: 2022-08-31 ---- - -Date: August 31, 2022 - -## Documentation -- Added a quickstart guide for users to run and use Envoy Gateway. - -## API -- Added the EnvoyGateway API type for configuring Envoy Gateway. -- Added the EnvoyProxy API type for configuring managed Envoys. - -## CI -- Added tooling to build, run, etc. Envoy Gateway. - -## Providers -- Added the Kubernetes provider. - -## xDS -- Added xDS server to configure managed Envoys. - -## IR -- Added xds and infra IRs to decouple user-facing APIs from Envoy Gateway. -- Added IR validation. - -## Translator -- Added the gatewayapi translator to translate Gateway API and associated resources to the IR and manage -- Gateway API status. - -## Message Service -- Added infra and xds IR watchable map messages for inter-component communication. -- Added a Runner to each component to support pub/sub between components. - -## Infra Manager -- Added Kubernetes Infra Manager to manage Envoy infrastructure running in a Kubernetes cluster. diff --git a/site/content/en/latest/releases/v0.2.0-rc2.md b/site/content/en/latest/releases/v0.2.0-rc2.md deleted file mode 100644 index 756ccfb18da..00000000000 --- a/site/content/en/latest/releases/v0.2.0-rc2.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: "v0.2.0-rc.2" -publishdate: 2022-09-29 ---- - -Date: September 29, 2022 - -## Documentation -- Updated and expanded developer documentation. -- Added `kube-demo` target to demonstrate Envoy Gateway functionality. -- Added developer debugging documentation. - -## CI -- Added Gateway API conformance tests. - -## Providers -- Added watchers for dependent resources of managed Envoy infrastructure. -- Added Gateway namespace/name labels to managed resources. -- Added support for finalizing the managed GatewayClass. - -## xDS -- Updated xds server and Envoy bootstrap config to use Delta xDS. -- Added initial support for mTLS between the xDS server and Envoy. - -## Translator -- Expanded support for Gateway API status. -- Added support for request modifier and redirect filters. -- Added support to return 500 responses for invalid backends. - -## Message Service -- Updated IRs to support managing multiple Envoy fleets. - -## Infra Manager -- Separate Envoy infrastructure is created per Gateway. diff --git a/site/content/en/latest/releases/v0.3.0-rc.1.md b/site/content/en/latest/releases/v0.3.0-rc.1.md deleted file mode 100644 index 4c50801e6f3..00000000000 --- a/site/content/en/latest/releases/v0.3.0-rc.1.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -title: "v0.3.0-rc.1" -publishdate: 2023-02-02 ---- - -Date: February 02, 2023 - -## Documentation -- Added Support for Multiple Release Versions -- Added Support for Versioned Docs -- Added Release Details Docs -- Refactored Layout for User Docs - -## API -- Upgraded to v0.6.0 Gateway API -- Added Support for the TCPRoute API -- Added Support for the UDPRoute API -- Added Support for the GRPCRoute API (Add to the ListenerStatus.SupportedKinds Field until https://github.com/envoyproxy/gateway/issues/950 is fixed.) -- Added Support for HTTPRoute URLRewrite Filter -- Added Support for HTTPRoute RequestMirror Filter -- Added Support for HTTPRoute ResponseHeaderModifier Filter -- Added APIs to Manage Envoy Deployment -- Added Support for Request Authentication -- Added Support for Global Rate Limiting -- Added Support for Routes ReferenceGrant -- Added Support for Namespace Server Config Type - -## CI Tooling Testing -- Fixes Make Image Failed in Darwin -- Fixes Wait for Job Succeeded before conformance test -- Upgraded Echoserver Image Tag -- Added Support for User-Facing Version -- Added Support for Testing EG against Multiple Kubernetes Versions - -## Conformance -- Enabled HTTPRouteInvalidParentRefNotMatchingListenerPort conformance test -- Enabled GatewayInvalidTLSConfiguration conformance test -- Enabled GatewayInvalidRouteKind conformance test -- Enabled HTTPRoutePartiallyInvalidViaInvalidReferenceGrant conformance test -- Enabled HTTPRouteReferenceGrant conformance test -- Enabled HTTPRouteMethodMatching conformance test - -## IR -- Added TCP Listener per TLSRoute - -## Translator -- Fixes Remove Stale Listener Condition -- Added Support for Suffix Matches for Headers -- Added Support for HTTP Method Matching to HTTPRoute -- Added Support for Regex Match Type -- Added Support for HTTPQueryParamMatch - -## Providers -- Refactored Kubernetes Provider to Single Reconciler -- Upgraded Kube Provider Test Data Manifests to v0.6.0 -- Removed Duplicate Settings from Bootstrap Config -- Updated Certgen to Use EG Namespace Env -- Added EnvoyProxy to Translator and Kube Infra Manager -- Upgraded Envoyproxy Image to envoy-dev latest in Main -- Removed EG Logs Private Key - -## xDS -- Fixed Start xDS Server Watchable Map Panics -- Enabled Access Logging for xDS Components diff --git a/site/content/en/latest/releases/v0.4.0.md b/site/content/en/latest/releases/v0.4.0.md deleted file mode 100644 index 12c40904088..00000000000 --- a/site/content/en/latest/releases/v0.4.0.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -title: "v0.4.0" -publishdate: 2023-04-24 ---- - -Date: April 24, 2023 - -## Documentation -- Added Docs for Installing and Using egctl - -## Installation -- Added Helm Installation Support -- Added Support for Ratelimiting Based On IP Subnet -- Added Gateway API Support Doc -- Added Namespace Resource to Helm Templates -- Updated Installation Yaml to Use the envoy-gateway-system Namespace - -## API -- Upgraded to Gateway API v0.6.2 -- Added Support for Custom Envoy Proxy Bootstrap Config -- Added Support for Configuring the Envoy Proxy Image and Service -- Added Support for Configuring Annotations, Resources, and Securitycontext Settings on Ratelimit Infra and Envoy Proxy -- Added Support for Using Multiple Certificates on a Single Fully Qualified Domain Name -- Gateway Status Address is now Populated for ClusterIP type Envoy Services -- Envoy Proxy Pod and Container SecurityContext is now Configurable -- Added Custom Envoy Gateway Extensions Framework -- Added Support for Service Method Match in GRPCRoute -- Fixed a Bug in the Extension Hooks for xDS Virtual Hosts and Routes - -## CI Tooling Testing -- Fixed CI Flakes During Helm Install -- Added Test To Ensure Static xDS Cluster Has Same Field Values as Dynamic Cluster -- Added egctl to Build and Test CI Workflow -- Code Coverage Thresholds are now Enforced by CI -- Fixed latest-release-check CI Job Failures -- Added Auto Release Tooling for Charts - -## Conformance -- Enabled GatewayWithAttachedRoutes Test -- Enabled Enable HTTPRouteInvalidParentRefNotMatchingSectionName Test -- Enabled Enable HTTPRouteDisallowedKind Test -- Re-Enabled Gateway/HTTPRouteObservedGenerationBump Test - -## Translator -- Added Support for Dynamic GatewayControllerName in Route Status - -## Providers -- Update GatewayClass Status Based on EnvoyProxy Config Validation - -## xDS -- Added EDS Support -- Fixed PathSeparatedPrefix and Optimized Logic for Prefixes Ending With Trailing Slash -- Updated Deprecated RegexMatcher -- Refactored Authn and Ratelimit Features to Reuse buildXdsCluster - -## Cli -- Added egctl CLI Tool -- Added egctl Support for Dry Runs of Gateway API Config -- Added egctl Support for Dumping Envoy Proxy xDS Resources diff --git a/site/content/en/latest/releases/v0.5.0.md b/site/content/en/latest/releases/v0.5.0.md deleted file mode 100644 index ce1bd6b9188..00000000000 --- a/site/content/en/latest/releases/v0.5.0.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -title: "v0.5.0" -publishdate: 2023-07-26 ---- - -Date: July 26, 2023 - -## Documentation -- Added Docs for Installation page using Helm -- Added Docs for Cert Manager Integration -- Added Docs for Presentation Links -- Added Docs for configuring multiple TLS Certificates per Listener - -## Installation -- Added Support for configuring Envoy Gateway Label and Annotations using Helm -- Increased default Resource defaults for Envoy Gateway to 100m CPU and 256Mi Memory -- Fixes Helm values for EnvoyGateway startup configuration -- Added opt-in field to skip creating control plane TLS Certificates allowing users to bring their own certificates. - -## API -- Upgraded to Gateway API v0.7.1 -- Added Support for EnvoyPatchPolicy -- Added Support for EnvoyProxy Telemetry - Access Logging, Traces and Metrics -- Added Support for configuring EnvoyProxy Pod Labels -- Added Support for configuring EnvoyProxy Deployment Strategy Settings, Volumes and Volume Mounts -- Added Support for configuring EnvoyProxy as a NodePort Type Service -- Added Support for Distinct RateLimiting for IP Addresses -- Added Support for converting JWT Claims to Headers, to be used for RateLimiting -- Added Admin Server for Envoy Gateway -- Added Pprof Debug Support for Envoy Gateway -- Added Support to Watch for Resources in Select Namespaces -### Breaking Changes -- Renamed field in EnvoyGateway API from Extension to ExtensionManager - -## CI Tooling Testing -- Added Retest Github Action -- Added CherryPick Github Action -- Added E2E Step in Github CI Workflow -- Added RateLimit E2E Tests -- Added JWT Claim based RateLimit E2E Tests -- Added Access Logging E2E tests -- Added Metrics E2E tests -- Added Tracing E2E tests - -## Conformance -- Enabled GatewayWithAttachedRoutes Test -- Enabled HttpRouteRequestMirror Test -- Skipped HTTPRouteRedirectPortAndScheme Test - -## Translator -### Breaking Changes -- Renamed IR resources from - to / - which also affects generated Xds Resources - -## Providers -- Reconcile Node resources to be able to compute Status Addresses for Gateway -- Discard Status before publishing Provider resources to reduce memory consumption - -## xDS -- Fix Init Race in Xds Runner when starting Xds Server and receiving Xds Input -- Switched to Xds SOTW Server for RateLimit Service Configuration -- Added Control Plane TLS between EnvoyProxy and RateLimit Service -- Enabled adding RateLimit Headers when RateLimit is set -- Allowed GRPCRoute and HTTPRoute to be linked to the same HTTPS Listener -- Set ALPN in the Xds Listener with TLS enabled. -- Added Best Practices Default Edge Settings to Xds Resources -- Compute and Publish EnvoyPatchPolicy status from xds-translator runner - -## Cli -- Added egctl x translate Support to generate default missing Resources -- Added egctl x translate Support for AuthenticationFilter and EnvoyPatchPolicy diff --git a/site/content/en/latest/releases/v0.6.0.md b/site/content/en/latest/releases/v0.6.0.md deleted file mode 100644 index 2b8714030b0..00000000000 --- a/site/content/en/latest/releases/v0.6.0.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -title: "v0.6.0" -publishdate: 2023-11-01 ---- - -Date: Nov 1, 2023 - -## Documentation -- Introduced a new website based on Hugo -- Added Grafana dashboards and integration docs for EnvoyProxy metrics -- Added Grafana integration docs for Gateway API metrics - -## Installation -- Updated EnvoyProxy image to be a distroless variant. -- Removed resources around kube-rbac-proxy - -## API -- Upgraded to Gateway API v1.0.0 -- Added the ClientTrafficPolicy CRD with Keep Alive Support -- Added the BackendTrafficPolicy CRD with RateLimit and LoadBalancer Support -- Added the SecurityPolicy CRD with CORS and JWT Support -- Added EnvoyGateway Metrics with Prometheus and OpenTelemetry support -- Added Support for InitContainers in EnvoyProxy CRD -- Added Support for LoadBalancerIP in EnvoyProxy CRD -- Added Support for AllocateLoadBalancerNodePorts in EnvoyProxy CRD -- Added Support for LoadBalancerClass in EnvoyProxy CRD -- Added Support for selecting EnvoyProxy stats to be generated -- Added Support for enabling EnvoyProxy Virtual Host metrics -- Added Support for Merging Gateway resources onto the same infrastructure - -### Breaking changes -- Removed the AuthenticationFilter CRD -- Removed the RateLimitFilter CRD -- Moved EnvoyProxy CRD from `config.gateway.envoyproxy.io` to `gateway.envoyproxy.io` -- Enabled EnvoyProxy Prometheus Endpoint by default with an option to disable it -- Updated the Bootstrap field within the EnvoyProxy CRD with an additional value -- field to specify bootstrap config - -## Conformance -- Added Support for HTTPRouteBackendProtocolH2C Test -- Added Support for HTTPRouteBackendProtocolWebSocket Test -- Added Support for HTTPRouteRequestMultipleMirrors Test -- Added Support for HTTPRouteTimeoutRequest Test -- Added Support for HTTPRouteTimeoutBackendRequest Test -- Added Support for HTTPRouteRedirectPortAndScheme Test - -## Watchable -- Improved caching of resource by implementing a compare function agnostic of resource order - -## Translator -- Added support for routing to EndpointSlice endpoints -- Added support for HTTPRoute Timeouts -- Added support for multiple RequestMirror filters per HTTPRoute rule -- Use / instead of - in IR Route Names -- Added Support to ignore ports in Host header - -## Providers -- Added the generationChangedPredicate to most resources to limit resource reconiliation -- Improved reconiliation by using the same enqueue request for all resources -- Added support for reconciling ServiceImport CRD -- Added support for selectively watching resources based on Namespace Selector - - -## XDS -- Fixed Layered Runtime warnings -- Upgraded to the latest version of go-control-plane that fixed xDS Resource ordering issues for ADS. -- Added HTTP2 Keep Alives to the xds connection - -## Cli -- Added Support for egctl stats command diff --git a/site/content/en/latest/tasks/extensibility/build-wasm-image.md b/site/content/en/latest/tasks/extensibility/build-wasm-image.md new file mode 100644 index 00000000000..dfe983dd193 --- /dev/null +++ b/site/content/en/latest/tasks/extensibility/build-wasm-image.md @@ -0,0 +1,71 @@ +--- +title: "Build a Wasm image" +--- + +Envoy Gateway supports two types of Wasm extensions within the [EnvoyExtensionPolicy][] API: HTTP Wasm Extensions and Image Wasm Extensions. +Packaging a Wasm extension as an OCI image is beneficial because it simplifies versioning and distribution for users. +Additionally, users can leverage existing image toolchain to build and manage Wasm images. + +This document describes how to build OCI images which are consumable by Envoy Gateway. + +## Wasm Image Formats + +There are two types of images that are supported by Envoy Gateway. One is in the Docker format, and another is the standard +OCI specification compliant format. Please note that both of them are supported by any OCI registries. You can choose +either format depending on your preference, and both types of images are consumable by Envoy Gateway [EnvoyExtensionPolicy][] API. + +## Build Wasm Docker image + +We assume that you have a valid Wasm binary named `plugin.wasm`. Then you can build a Wasm Docker image with the Docker CLI. + +1. First, we prepare the following Dockerfile: + +``` +$ cat Dockerfile +FROM scratch + +COPY plugin.wasm ./ +``` + +**Note: you must have exactly one `COPY` instruction in the Dockerfile in order to end up having only one layer in produced images.** + +2. Then, build your image via `docker build` command + +``` +$ docker build . -t my-registry/mywasm:0.1.0 +``` + +3. Finally, push the image to your registry via `docker push` command + +``` +$ docker push my-registry/mywasm:0.1.0 +``` + +## Build Wasm OCI image + +We assume that you have a valid Wasm binary named `plugin.wasm`, and you have [buildah](https://buildah.io/) installed on your machine. +Then you can build a Wasm OCI image with the `buildah` CLI. + +1. First, we create a working container from `scratch` base image with `buildah from` command. + +``` +$ buildah --name mywasm from scratch +mywasm +``` + +2. Then copy the Wasm binary into that base image by `buildah copy` command to create the layer. + +``` +$ buildah copy mywasm plugin.wasm ./ +af82a227630327c24026d7c6d3057c3d5478b14426b74c547df011ca5f23d271 +``` + +**Note: you must execute `buildah copy` exactly once in order to end up having only one layer in produced images** + +4. Now, you can build an OCI image and push it to your registry via `buildah commit` command + +``` +$ buildah commit mywasm docker://my-remote-registry/mywasm:0.1.0 +``` + +[EnvoyExtensionPolicy]: ../../../api/extension_types#envoyextensionpolicy diff --git a/site/content/en/latest/tasks/extensibility/extension-server.md b/site/content/en/latest/tasks/extensibility/extension-server.md new file mode 100644 index 00000000000..7d67c23f6da --- /dev/null +++ b/site/content/en/latest/tasks/extensibility/extension-server.md @@ -0,0 +1,209 @@ +--- +title: "Envoy Gateway Extension Server" +--- + +This task explains how to extend Envoy Gateway using an Extension Server. Envoy Gateway +can be configured to call an external server over gRPC with the xDS configuration _before_ +it is sent to Envoy Proxy. The external server can modify the provided configuration +programmatically using any semantics supported by the [xDS][] API. + +Using an extension server allows vendors to add xDS configuration that Envoy Gateway itself +doesn't support with a very high level of control over the generated xDS configuration. + +**Note:** Modifying the xDS configuration generated by Envoy Gateway may break functionality +configured by native Envoy Gateway means. Like other cases where the xDS configuration +is modified outside of Envoy Gateway's control, this is risky and should be tested thoroughly, +especially when using the same extension server across different Envoy Gateway versions. + +## Introduction + +One of the Envoy Gateway project goals is to "provide a common foundation for vendors to +build value-added products without having to re-engineer fundamental interactions". The +Envoy Gateway Extension Server provides a mechanism where Envoy Gateway tracks all provider +resources and then calls a set of hooks that allow the generated xDS configuration to be +modified before it is sent to Envoy Proxy. See the [design documentation][] for full details. + +This task sets up an example extension server that adds the Envoy Proxy Basic Authentication +HTTP filter to all the listeners generated by Envoy Gateway. The example extension server +includes its own CRD which allows defining username/password pairs that will be accepted by +the Envoy Proxy. + +**Note:** Envoy Gateway supports adding Basic Authentication to routes using a [SecurityPolicy][]. +See [this task](../security/basic-auth) for the preferred way to configure Basic +Authentication. + + +## Quickstart + +### Prerequisites + +* Follow the steps from the [Quickstart](../quickstart) task to install Envoy Gateway and the example manifest. + Before proceeding, you should be able to query the example backend using HTTP. + +### Build and run the example Extension Server + +Build and deploy the example extension server in the `examples/extension-server` folder into the cluster +running Envoy Gateway. + +* Build the extension server image + + **Note:** The provided `Makefile` builds an image with the name `extension-server:latest`. You may need to create +a different tag for it in order to allow Kubernetes to pull it correctly. + + ```shell + make image + ``` + +* Publish the extension server image in your docker repository + + {{< tabpane text=true >}} + {{% tab header="local kind server" %}} + + ```shell + kind load docker-image --name envoy-gateway extension-server:latest + ``` + + {{% /tab %}} + {{% tab header="other Kubernetes server" %}} + + ```shell + docker tag extension-server:latest $YOUR_DOCKER_REPO + docker push $YOUR_DOCKER_REPO + ``` + + {{% /tab %}} + {{< /tabpane >}} + +* Deploy the extension server in your cluster + + If you are using your own docker image repository, make sure to update the `values.yaml` with the correct +image name and tag. + + ```shell + helm install -n envoy-gateway-system extension-server ./examples/extension-server/charts/extension-server + ``` + +### Configure Envoy Gateway + +* Grant Envoy Gateway's `ServiceAccount` permission to access the extension server's CRD + + ```shell + kubectl create clusterrole listener-context-example-viewer \ + --verb=get,list,watch \ + --resource=ListenerContextExample + + kubectl create clusterrolebinding envoy-gateway-listener-context \ + --clusterrole=listener-context-example-viewer \ + --serviceaccount=envoy-gateway-system:envoy-gateway + ``` + +* Configure Envoy Gateway to use the Extension Server + + Add the following fragment to Envoy Gateway's [configuration][] file: + + ```yaml + extensionManager: + # Envoy Gateway will watch these resource kinds and use them as extension policies + # which can be attached to Gateway resources. + policyResources: + - group: example.extensions.io + version: v1alpha1 + kind: ListenerContextExample + hooks: + # The type of hooks that should be invoked + xdsTranslator: + post: + - HTTPListener + service: + # The service that is hosting the extension server + fqdn: + hostname: extension-server.envoy-gateway-system.svc.cluster.local + port: 5005 + ``` + + After updating Envoy Gateway's configuration file, restart Envoy Gateway. + +## Testing + +Get the Gateway's address: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +The extension server adds the Basic Authentication HTTP filter to all listeners configured by +Envoy Gateway. Initially there are no valid user/password combinations available. Accessing the +example backend should fail with a 401 status: + +```console +$ curl -v --header "Host: www.example.com" "http://${GATEWAY_HOST}/example" +... +> GET /example HTTP/1.1 +> Host: www.example.com +> User-Agent: curl/7.81.0 +> Accept: */* +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 401 Unauthorized +< www-authenticate: Basic realm="http://www.example.com/example" +< content-length: 58 +< content-type: text/plain +< date: Mon, 08 Jul 2024 10:53:11 GMT +< +... +User authentication failed. Missing username and password. +... +``` + +Add a new Username/Password combination using the example extension server's CRD: + +```shell +kubectl apply -f - << EOF +apiVersion: example.extensions.io/v1alpha1 +kind: ListenerContextExample +metadata: + name: listeneruser +spec: + targetRefs: + - kind: Gateway + name: eg + group: gateway.networking.k8s.io + username: user + password: p@ssw0rd +EOF +``` + +Authenticating with this user/password combination will now work. + +```console +$ curl -v http://${GATEWAY_HOST}/example -H "Host: www.example.com" --user 'user:p@ssw0rd' +... +> GET /example HTTP/1.1 +> Host: www.example.com +> Authorization: Basic dXNlcm5hbWU6cEBzc3cwcmQ= +> User-Agent: curl/7.81.0 +> Accept: */* +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< date: Mon, 08 Jul 2024 10:56:17 GMT +< content-length: 559 +< +... + "headers": { + "Authorization": [ + "Basic dXNlcm5hbWU6cEBzc3cwcmQ=" + ], + "X-Example-Ext": [ + "user" + ], +... +``` + + +[xDS]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/operations/dynamic_configuration +[design documentation]: /contributions/design/extending-envoy-gateway +[SecurityPolicy]: /latest/api/extension_types/#securitypolicy +[configuration]: /latest/api/extension_types/#extensionmanager diff --git a/site/content/en/latest/tasks/extensibility/wasm.md b/site/content/en/latest/tasks/extensibility/wasm.md new file mode 100644 index 00000000000..d973de77950 --- /dev/null +++ b/site/content/en/latest/tasks/extensibility/wasm.md @@ -0,0 +1,194 @@ +--- +title: "Wasm Extensions" +--- + +This task provides instructions for extending Envoy Gateway with WebAssembly (Wasm) extensions. + +Wasm extensions allow you to extend the functionality of Envoy Gateway by running custom code against HTTP requests and responses, +without modifying the Envoy Gateway binary. These extensions can be written in any language that compiles to Wasm, such as C++, Rust, AssemblyScript, or TinyGo. + +Envoy Gateway introduces a new CRD called [EnvoyExtensionPolicy][] that allows the user to configure Wasm extensions. +This instantiated resource can be linked to a [Gateway][Gateway] and [HTTPRoute][HTTPRoute] resource. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +Verify the Gateway status: + +```shell +kubectl get gateway/eg -o yaml +``` + +## Configuration + +Envoy Gateway supports two types of Wasm extensions: +* HTTP Wasm Extension: The Wasm extension is fetched from a remote URL. +* Image Wasm Extension: The Wasm extension is packaged as an OCI image and fetched from an image registry. + +The following example demonstrates how to configure an [EnvoyExtensionPolicy][] to attach a Wasm extension to an [EnvoyExtensionPolicy][] . +This Wasm extension adds a custom header `x-wasm-custom: FOO` to the response. + +### HTTP Wasm Extension + +This [EnvoyExtensionPolicy][] configuration fetches the Wasm extension from an HTTP URL. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the EnvoyExtensionPolicy status: + +```shell +kubectl get envoyextensionpolicy/http-wasm-source-test -o yaml +``` + +### Image Wasm Extension + +This [EnvoyExtensionPolicy][] configuration fetches the Wasm extension from an OCI image. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the EnvoyExtensionPolicy status: + +```shell +kubectl get envoyextensionpolicy/http-wasm-source-test -o yaml +``` + +### Testing + +Ensure the `GATEWAY_HOST` environment variable from the [Quickstart](../../quickstart) is set. If not, follow the +Quickstart instructions to set the variable. + +```shell +echo $GATEWAY_HOST +``` + +Send a request to the backend service: + +```shell +curl -i -H "Host: www.example.com" "http://${GATEWAY_HOST}" +``` + +You should see that the wasm extension has added this header to the response: + +``` +x-wasm-custom: FOO +``` + +## Clean-Up + +Follow the steps from the [Quickstart](../../quickstart) to uninstall Envoy Gateway and the example manifest. + +Delete the EnvoyExtensionPolicy: + +```shell +kubectl delete envoyextensionpolicy/wasm-test +``` + +## Next Steps + +Checkout the [Developer Guide](../../../contributions/develop) to get involved in the project. + +[EnvoyExtensionPolicy]: ../../../api/extension_types#envoyextensionpolicy +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute diff --git a/site/content/en/latest/tasks/observability/grafana-integration.md b/site/content/en/latest/tasks/observability/grafana-integration.md index 983a044fdf0..259f6958bf0 100644 --- a/site/content/en/latest/tasks/observability/grafana-integration.md +++ b/site/content/en/latest/tasks/observability/grafana-integration.md @@ -35,25 +35,19 @@ Envoy Gateway has examples of dashboard for you to get started, you can check th If you'd like import Grafana dashboards on your own, please refer to Grafana docs for [importing dashboards](https://grafana.com/docs/grafana/latest/dashboards/manage-dashboards/#import-a-dashboard). -### [Envoy Global](https://raw.githubusercontent.com/envoyproxy/gateway/main/charts/gateway-addons-helm/dashboards/envoy-global.json) +### Envoy Proxy Global -This dashboard example shows the overall downstram and upstream stats for each Envoy Proxy instance. +This dashboard example shows the overall downstream and upstream stats for each Envoy Proxy instance. -![Envoy Global](/img/envoy-global-dashboard.png) +![Envoy Proxy Global](/img/envoy-proxy-global-dashboard.png) -### [Envoy Clusters](https://raw.githubusercontent.com/envoyproxy/gateway/main/charts/gateway-addons-helm/dashboards/envoy-clusters.json) +### Envoy Clusters This dashboard example shows the overall stats for each cluster from Envoy Proxy fleet. ![Envoy Clusters](/img/envoy-clusters-dashboard.png) -### [Envoy Pod Resources](https://raw.githubusercontent.com/envoyproxy/gateway/main/charts/gateway-addons-helm/dashboards/envoy-pod-resource.json) - -This dashboard example shows the overall pod resources stats for each Envoy Proxy instance. - -![Envoy Pod Resources](/img/envoy-pod-resources-dashboard.png) - -### [Envoy Gateway Global](https://raw.githubusercontent.com/envoyproxy/gateway/main/charts/gateway-addons-helm/dashboards/envoy-gateway-global.json) +### Envoy Gateway Global This dashboard example shows the overall stats exported by Envoy Gateway fleet. @@ -65,16 +59,29 @@ This dashboard example shows the overall stats exported by Envoy Gateway fleet. ![Envoy Gateway Global: Infrastructure Manager](/img/envoy-gateway-global-infra-manager.png) -### [Envoy Gateway Resources](https://raw.githubusercontent.com/envoyproxy/gateway/main/charts/gateway-addons-helm/dashboards/envoy-gateway-resource.json) +### Resources Monitor -This dashboard example shows the overall resources stats for each Envoy Gateway fleet. +This dashboard example shows the overall resources stats for both Envoy Gateway and Envoy Proxy fleet. -![Envoy Gateway Resources](/img/envoy-gateway-resources-dashboard.png) +![Envoy Gateway Resources](/img/resources-monitor-dashboard.png) ## Update Dashboards -The example dashboards cannot be updated in-place by default, if you are trying to -make some changes to current dashboards, you can save them directly as a JSON file. - -All dashboards of Envoy Gateway are maintained under `charts/gateway-addons-helm/dashboards`, +All dashboards of Envoy Gateway are maintained under `charts/gateway-addons-helm/dashboards`, feel free to make [contributions](../../../contributions/CONTRIBUTING). + +### Grafonnet + +Newer dashboards are generated with [Jsonnet](https://jsonnet.org/) with the [Grafonnet](https://grafana.github.io/grafonnet/index.html). +This is the preferred method for any new dashboards. + +You can run `make helm-generate.gateway-addons-helm` to generate new version of dashboards. +All the generated dashboards have a `.gen.json` suffix. + +### Legacy Dashboards + +Many of our older dashboards are manually created in the UI and exported as JSON and checked in. + +These example dashboards cannot be updated in-place by default, if you are trying to +make some changes to the older dashboards, you can save them directly as a JSON file +and then re-import. diff --git a/site/content/en/latest/tasks/observability/proxy-accesslog.md b/site/content/en/latest/tasks/observability/proxy-accesslog.md index 02596c94627..fb0200f1739 100644 --- a/site/content/en/latest/tasks/observability/proxy-accesslog.md +++ b/site/content/en/latest/tasks/observability/proxy-accesslog.md @@ -140,6 +140,57 @@ Verify logs from loki: curl -s "http://$LOKI_IP:3100/loki/api/v1/query_range" --data-urlencode "query={exporter=\"OTLP\"}" | jq '.data.result[0].values' ``` +## gGRPC Access Log Service(ALS) Sink + +Envoy Gateway can send logs to a backend implemented [gRPC access log service proto](https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/accesslog/v3/als.proto). +There's an example service [here](https://raw.githubusercontent.com/envoyproxy/gateway/main/examples/kubernetes/envoy-als.yaml), which simply count the log and export to prometheus endpoint. + +```shell +kubectl apply -f https://raw.githubusercontent.com/envoyproxy/gateway/main/examples/kubernetes/envoy-als.yaml -n monitoring +``` + +The following configuration sends logs to the gRPC access log service: + +```shell +kubectl apply -f - <}} {{% tab header="Apply from stdin" %}} @@ -745,6 +743,8 @@ spec: You can customize the EnvoyProxy using patches. +### Patching Deployment for EnvoyProxy + For example, the following configuration will add resource limits to the `envoy` and the `shutdown-manager` containers in the `envoyproxy` deployment: {{< tabpane text=true >}} @@ -822,6 +822,63 @@ spec: After applying the configuration, you will see the change in both containers in the `envoyproxy` deployment. +### Patching Service for EnvoyProxy + +For example, the following configuration will add an annotation for the `envoyproxy` service: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +After applying the configuration, you will see the `custom-annotation: foobar` has been added to the `envoyproxy` service. + ## Customize Filter Order Under the hood, Envoy Gateway uses a series of [Envoy HTTP filters](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/http_filters) diff --git a/site/content/en/latest/tasks/quickstart.md b/site/content/en/latest/tasks/quickstart.md index 93458a4eb92..4075eb5d478 100644 --- a/site/content/en/latest/tasks/quickstart.md +++ b/site/content/en/latest/tasks/quickstart.md @@ -110,6 +110,8 @@ kubectl apply -f ./gateway-helm/crds/generated Update your `BackendTLSPolicy` and `GRPCRoute` resources according to Gateway-API [v1.1 Upgrade Notes](https://gateway-api.sigs.k8s.io/guides/#v11-upgrade-notes) +Update your Envoy Gateway xPolicy resources: remove the namespace section from targetRef. + Install Envoy Gateway v1.1.0: ```shell diff --git a/site/content/en/latest/tasks/security/backend-mtls.md b/site/content/en/latest/tasks/security/backend-mtls.md new file mode 100644 index 00000000000..1d91c7a95f8 --- /dev/null +++ b/site/content/en/latest/tasks/security/backend-mtls.md @@ -0,0 +1,200 @@ +--- +title: "Backend Mutual TLS: Gateway to Backend" +--- + +This task demonstrates how mTLS can be achieved between the Gateway and a backend. +This task uses a self-signed CA, so it should be used for testing and demonstration purposes only. + +Envoy Gateway supports the Gateway-API defined [BackendTLSPolicy][] to establish TLS. For mTLS, the Gateway must authenticate by presenting a client certificate to the backend. + +## Prerequisites + +- OpenSSL to generate TLS assets. + +## Installation + +Follow the steps from the [Backend TLS][] to install Envoy Gateway and configure TLS to the backend server. + +## TLS Certificates + +Generate the certificates and keys used by the Gateway for authentication against the backend. + +Create a root certificate and private key to sign certificates: + +```shell +openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -subj '/O=example Inc./CN=example.com' -keyout clientca.key -out clientca.crt +``` + +Create a certificate and a private key for `www.example.com`: + +```shell +openssl req -new -newkey rsa:2048 -nodes -keyout client.key -out client.csr -subj "/CN=example-client/O=example organization" +openssl x509 -req -days 365 -CA clientca.crt -CAkey clientca.key -set_serial 0 -in client.csr -out client.crt +``` + +Store the cert/key in a Secret: + +```shell +kubectl -n envoy-gateway-system create secret tls example-client-cert --key=client.key --cert=client.crt +``` + +Store the CA Cert in another Secret: + +```shell +kubectl create configmap example-client-ca --from-file=clientca.crt +``` + +## Enforce Client Certificate Authentication on the backend + +Patch the existing quickstart backend to enforce Client Certificate Authentication. The patch will mount the server certificate and key required for TLS, and the CA certificate into the backend as volumes. + +```shell +kubectl patch deployment backend --type=json --patch ' + - op: add + path: /spec/template/spec/containers/0/volumeMounts + value: + - name: client-certs-volume + mountPath: /etc/client-certs + - name: secret-volume + mountPath: /etc/secret-volume + - op: add + path: /spec/template/spec/volumes + value: + - name: client-certs-volume + configMap: + name: example-client-ca + items: + - key: clientca.crt + path: crt + - name: secret-volume + secret: + secretName: example-cert + items: + - key: tls.crt + path: crt + - key: tls.key + path: key + - op: add + path: /spec/template/spec/containers/0/env/- + value: + name: TLS_CLIENT_CACERTS + value: /etc/client-certs/crt + ' +``` + +## Configure Envoy Proxy to use a client certificate + +In addition to enablement of backend TLS with the Gateway-API BackendTLSPolicy, Envoy Gateway supports customizing TLS parameters such as TLS Client Certificate. +To achieve this, the [EnvoyProxy][] resource can be used to specify a TLS Client Certificate. + +First, you need to add ParametersRef in GatewayClass, and refer to EnvoyProxy Config: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +## Testing mTLS + +Query the TLS-enabled backend through Envoy proxy: + +```shell +curl -v -HHost:www.example.com --resolve "www.example.com:80:127.0.0.1" \ +http://www.example.com:80/get +``` + +Inspect the output and see that the response contains the details of the TLS handshake between Envoy and the backend. +The response now contains a "peerCertificates" attribute that reflects the client certificate used by the Gateway to establish mTLS with the backend. + +```shell +< HTTP/1.1 200 OK +[...] + "tls": { + "version": "TLSv1.2", + "serverName": "www.example.com", + "negotiatedProtocol": "http/1.1", + "cipherSuite": "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" + "peerCertificates": ["-----BEGIN CERTIFICATE-----\n[...]-----END CERTIFICATE-----\n"] + } +``` + +[Backend TLS]: ./backend-tls +[BackendTLSPolicy]: https://gateway-api.sigs.k8s.io/api-types/backendtlspolicy/ +[EnvoyProxy]: ../../api/extension_types#envoyproxy \ No newline at end of file diff --git a/site/content/en/latest/tasks/security/backend-tls.md b/site/content/en/latest/tasks/security/backend-tls.md index 93d155eb30e..53e9ccbd44a 100644 --- a/site/content/en/latest/tasks/security/backend-tls.md +++ b/site/content/en/latest/tasks/security/backend-tls.md @@ -13,7 +13,7 @@ Envoy Gateway supports the Gateway-API defined [BackendTLSPolicy][]. ## Installation -Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Follow the steps from the [Quickstart][] to install Envoy Gateway and the example manifest. ## TLS Certificates @@ -28,10 +28,12 @@ openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -subj '/O=example In Create a certificate and a private key for `www.example.com`: ```shell -openssl req -out www.example.com.csr -newkey rsa:2048 -nodes -keyout www.example.com.key -subj "/CN=www.example.com/O=example organization" +openssl req -out www.example.com.csr -newkey rsa:2048 -nodes -keyout www.example.com.key -subj "/CN=www.example.com/O=example organization" -addext "subjectAltName = DNS:www.example.com" openssl x509 -req -days 365 -CA ca.crt -CAkey ca.key -set_serial 0 -in www.example.com.csr -out www.example.com.crt ``` +Note that the certificate must contain a DNS SAN for the relevant domain. + Store the cert/key in a Secret: ```shell @@ -206,7 +208,7 @@ Verify the HTTPRoute status: kubectl get HTTPRoute backend -o yaml ``` -## Testing +## Testing backend TLS {{< tabpane text=true >}} {{% tab header="With External LoadBalancer Support" %}} @@ -275,4 +277,114 @@ Inspect the output and see that the response contains the details of the TLS han {{% /tab %}} {{< /tabpane >}} +## Customize backend TLS Parameters + +In addition to enablement of backend TLS with the Gateway-API BackendTLSPolicy, Envoy Gateway supports customizing TLS parameters. +To achieve this, the [EnvoyProxy][] resource can be used to specify TLS parameters. We will customize the TLS version in this example. + +First, you need to add ParametersRef in GatewayClass, and refer to EnvoyProxy Config: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +You can customize the EnvoyProxy Backend TLS Parameters via EnvoyProxy Config like: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +## Testing TLS Parameters + +Query the TLS-enabled backend through Envoy proxy: + +```shell +curl -v -HHost:www.example.com --resolve "www.example.com:80:127.0.0.1" \ +http://www.example.com:80/get +``` + +Inspect the output and see that the response contains the details of the TLS handshake between Envoy and the backend. +The TLS version is now TLS1.3, as configured in the EnvoyProxy resource. The TLS cipher is also changed, since TLS1.3 supports different ciphers from TLS1.2. + +```shell +< HTTP/1.1 200 OK +[...] + "tls": { + "version": "TLSv1.3", + "serverName": "www.example.com", + "negotiatedProtocol": "http/1.1", + "cipherSuite": "TLS_AES_128_GCM_SHA256" + } +``` + +[Quickstart]: ../quickstart [BackendTLSPolicy]: https://gateway-api.sigs.k8s.io/api-types/backendtlspolicy/ +[EnvoyProxy]: ../../api/extension_types#envoyproxy \ No newline at end of file diff --git a/site/content/en/latest/tasks/security/restrict-ip-access.md b/site/content/en/latest/tasks/security/restrict-ip-access.md new file mode 100644 index 00000000000..ba6af118252 --- /dev/null +++ b/site/content/en/latest/tasks/security/restrict-ip-access.md @@ -0,0 +1,197 @@ +--- +title: "IP Allowlist/Denylist" +--- + +This task provides instructions for configuring IP allowlist/denylist on Envoy Gateway. IP allowlist/denylist +checks if an incoming request is from an allowed IP address before routing the request to a backend service. + +Envoy Gateway introduces a new CRD called [SecurityPolicy][SecurityPolicy] that allows the user to configure IP allowlist/denylist. +This instantiated resource can be linked to a [Gateway][Gateway], [HTTPRoute][HTTPRoute] or [GRPCRoute][GRPCRoute] resource. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## Configuration + +### Create a SecurityPolicy + +The below SecurityPolicy restricts access to the backend service by allowing requests only from the IP addresses `10.0.1.0/24`. + +In this example, the default action is set to `Deny`, which means that only requests from the specified IP addresses with `Allow` +action are allowed, and all other requests are denied. You can also change the default action to `Allow` to allow all requests +except those from the specified IP addresses with `Deny` action. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the SecurityPolicy configuration: + +```shell +kubectl get securitypolicy/authorization-client-ip -o yaml +``` + +### Original Source IP + +It's important to note that the IP address used for allowlist/denylist is the original source IP address of the request. +You can use a [ClientTrafficPolicy] to configure how Envoy Gateway should determine the original source IP address. + +For example, the below ClientTrafficPolicy configures Envoy Gateway to use the `X-Forwarded-For` header to determine the original source IP address. +The `numTrustedHops` field specifies the number of trusted hops in the `X-Forwarded-For` header. In this example, the `numTrustedHops` is set to `1`, +which means that the first rightmost IP address in the `X-Forwarded-For` header is used as the original source IP address. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + + +## Testing + +Ensure the `GATEWAY_HOST` environment variable from the [Quickstart](../../quickstart) is set. If not, follow the +Quickstart instructions to set the variable. + +```shell +echo $GATEWAY_HOST +``` + +Send a request to the backend service without the `X-Forwarded-For` header: + +```shell +curl -v -H "Host: www.example.com" "http://${GATEWAY_HOST}/" +``` + +You should see `403 Forbidden` in the response, indicating that the request is not allowed. + +```shell +* Connected to 172.18.255.200 (172.18.255.200) port 80 +> GET /get HTTP/1.1 +> Host: www.example.com +> User-Agent: curl/8.8.0-DEV +> Accept: */* +> +* Request completely sent off +< HTTP/1.1 403 Forbidden +< content-length: 19 +< content-type: text/plain +< date: Mon, 08 Jul 2024 04:23:31 GMT +< +* Connection #0 to host 172.18.255.200 left intact +RBAC: access denied +``` + +Send a request to the backend service with the `X-Forwarded-For` header: + +```shell +curl -v -H "Host: www.example.com" -H "X-Forwarded-For: 10.0.1.1" "http://${GATEWAY_HOST}/" +``` + +The request should be allowed and you should see the response from the backend service. + +## Clean-Up + +Follow the steps from the [Quickstart](../../quickstart) to uninstall Envoy Gateway and the example manifest. + +Delete the SecurityPolicy and the ClientTrafficPolicy + +```shell +kubectl delete securitypolicy/authorization-client-ip +kubectl delete clientTrafficPolicy/enable-client-ip-detection +``` + +## Next Steps + +Checkout the [Developer Guide](../../../contributions/develop) to get involved in the project. + +[SecurityPolicy]: ../../../contributions/design/security-policy +[ClientTrafficPolicy]: ../../../api/extension_types#clienttrafficpolicy +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute +[GRPCRoute]: https://gateway-api.sigs.k8s.io/api-types/grpcroute diff --git a/site/content/en/latest/tasks/security/threat-model.md b/site/content/en/latest/tasks/security/threat-model.md index cd5927e8d98..a16319f9d72 100644 --- a/site/content/en/latest/tasks/security/threat-model.md +++ b/site/content/en/latest/tasks/security/threat-model.md @@ -603,7 +603,7 @@ Set runAsUser and runAsGroup security context options to specific UIDs (e.g., ru |EGTM-008|EGTM-EG-003|Envoy Gateway| There is a risk of a threat actor misconfiguring static config and compromising the integrity of Envoy Gateway, ultimately leading to the compromised confidentiality, integrity, or availability of tenant data and cluster resources.

| Accidental or deliberate misconfiguration of static configuration leads to a misconfigured deployment of Envoy Gateway, for example logging parameters could be modified or global rate limiting configuration misconfigured.

|Medium| Implement a GitOps model, utilising Kubernetes\' Role-Based Access Control (RBAC) and adhering to the principle of least privilege to minimise human intervention on the cluster. For instance, tools like [ArgoCD](https://argo-cd.readthedocs.io/en/stable/) can be used for declarative GitOps deployments, ensuring all changes are tracked and reviewed. Additionally, configure your source control management (SCM) system to include mandatory pull request (PR) reviews, commit signing, and protected branches to ensure only authorised changes can be committed to the start-up configuration. | |EGTM-010|EGTM-CS-005|Container Security| There is a risk that a threat actor exploits a weak pod security context, compromising the CIA of a node and the resources / services which run on it.

| Threat Actor who has compromised a pod exploits weak security context to escape to a node, potentially leading to the compromise of Envoy Proxy or Gateway running on the same node.

|Medium| To mitigate this risk, apply [Pod Security Standards](https://kubernetes.io/docs/concepts/security/pod-security-standards/) at a minimum of [Baseline](https://kubernetes.io/docs/concepts/security/pod-security-standards/#baseline) level to all namespaces, especially those containing Envoy Gateway and Proxy Pods. Pod security standards are implemented through K8s [Pod Security Admission](https://kubernetes.io/docs/concepts/security/pod-security-admission/) to provide [admission control modes](https://kubernetes.io/docs/concepts/security/pod-security-admission/#pod-security-admission-labels-for-namespaces) (enforce, audit, and warn) for namespaces. Pod security standards can be enforced by namespace labels as shown [here](https://kubernetes.io/docs/tasks/configure-pod-container/enforce-standards-namespace-labels/), to enforce a baseline level of pod security to specific namespaces.

Further enhance the security by implementing a sandboxing solution such as [gVisor](https://gvisor.dev/) for Envoy Gateway and Proxy Pods to isolate the application from the host kernel. This can be set within the runtimeClassName of the Pod specification. | |EGTM-012|EGTM-GW-004|Gateway API| There is a risk that a threat actor could abuse excessive RBAC privileges to create ReferenceGrant resources. These resources could then be used to create cross-namespace communication, leading to unauthorised access to the application. This could compromise the confidentiality and integrity of resources and configuration in the affected namespaces and potentially disrupt the availability of services that rely on these object references.

| A ReferenceGrant is created, which validates traffic to cross namespace trust boundaries without a valid business reason, such as a route in one tenant\'s namespace referencing a backend in another.

|Medium| Ensure that the ability to create ReferenceGrant resources is restricted to the minimum number of people. Pay special attention to ClusterRoles that allow that action. | -|EGTM-018|EGTM-GW-006|Gateway API| There is a risk that malicious requests could lead to a Denial of Service (DoS) attack, thereby reducing API gateway availability due to misconfigurations in rate-limiting or load balancing controls, or a lack of route timeout enforcement.

| Reduced API gateway availability due to an attacker\'s maliciously crafted request (e.g., QoD) potentially inducing a Denial of Service (DoS) attack.

|Medium| To ensure high availability and to mitigate potential security threats, adhere to the Envoy Gateway documentation for the configuration of a [rate-limiting](https://gateway.envoyproxy.io/v0.6.0/user/rate-limit/) filter and load balancing.

Further, adhere to best practices for configuring Envoy Proxy as an edge proxy documented [here](https://www.envoyproxy.io/docs/envoy/latest/configuration/best_practices/edge#configuring-envoy-as-an-edge-proxy) within the EnvoyProxy docs. This involves configuring TCP and HTTP proxies with specific settings, including restricting access to the admin endpoint, setting the [overload manager](https://www.envoyproxy.io/docs/envoy/latest/configuration/operations/overload_manager/overload_manager#config-overload-manager) and [listener](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) / [cluster](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) buffer limits, enabling [use_remote_address](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-use-remote-address), setting [connection and stream timeouts](https://www.envoyproxy.io/docs/envoy/latest/faq/configuration/timeouts#faq-configuration-timeouts), limiting [maximum concurrent streams](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-field-config-core-v3-http2protocoloptions-max-concurrent-streams), setting [initial stream window size limit](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-field-config-core-v3-http2protocoloptions-initial-stream-window-size), and configuring action on [headers_with_underscores](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-field-config-core-v3-httpprotocoloptions-headers-with-underscores-action).

[Path normalisation](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-normalize-path) should be enabled to minimise path confusion vulnerabilities. These measures help protect against volumetric threats such as Denial of Service (DoS)nattacks. Utilise custom resources to implement policy attachment, thereby exposing request limit configuration for route types. | +|EGTM-018|EGTM-GW-006|Gateway API| There is a risk that malicious requests could lead to a Denial of Service (DoS) attack, thereby reducing API gateway availability due to misconfigurations in rate-limiting or load balancing controls, or a lack of route timeout enforcement.

| Reduced API gateway availability due to an attacker\'s maliciously crafted request (e.g., QoD) potentially inducing a Denial of Service (DoS) attack.

|Medium| To ensure high availability and to mitigate potential security threats, adhere to the Envoy Gateway documentation for the configuration of a [rate-limiting](../traffic/global-rate-limit) filter and load balancing.

Further, adhere to best practices for configuring Envoy Proxy as an edge proxy documented [here](https://www.envoyproxy.io/docs/envoy/latest/configuration/best_practices/edge#configuring-envoy-as-an-edge-proxy) within the EnvoyProxy docs. This involves configuring TCP and HTTP proxies with specific settings, including restricting access to the admin endpoint, setting the [overload manager](https://www.envoyproxy.io/docs/envoy/latest/configuration/operations/overload_manager/overload_manager#config-overload-manager) and [listener](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) / [cluster](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) buffer limits, enabling [use_remote_address](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-use-remote-address), setting [connection and stream timeouts](https://www.envoyproxy.io/docs/envoy/latest/faq/configuration/timeouts#faq-configuration-timeouts), limiting [maximum concurrent streams](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-field-config-core-v3-http2protocoloptions-max-concurrent-streams), setting [initial stream window size limit](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-field-config-core-v3-http2protocoloptions-initial-stream-window-size), and configuring action on [headers_with_underscores](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-field-config-core-v3-httpprotocoloptions-headers-with-underscores-action).

[Path normalisation](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-normalize-path) should be enabled to minimise path confusion vulnerabilities. These measures help protect against volumetric threats such as Denial of Service (DoS)nattacks. Utilise custom resources to implement policy attachment, thereby exposing request limit configuration for route types. | |EGTM-019|EGTM-DP-004|Container Security| There is a risk that replay attacks using stolen or reused JSON Web Tokens (JWTs) can compromise transmission integrity, thereby undermining the confidentiality and integrity of the data plane.

| Transmission integrity is compromised due to replay attacks using stolen or reused JSON Web Tokens (JWTs).

|Medium| Comply with JWT best practices for enhanced security, paying special attention to the use of short-lived tokens, which reduce the window of opportunity for a replay attack. The [exp](https://datatracker.ietf.org/doc/html/rfc7519#page-9) claim can be used to set token expiration times. | |EGTM-024|EGTM-EG-008|Envoy Gateway| There is a risk of developers getting more privileges than required due to the use of SecurityPolicy, ClientTrafficPolicy, EnvoyPatchPolicy and BackendTrafficPolicy. These resources can be attached to a Gateway resource. Therefore, a developer with permission to deploy them would be able to modify a Gateway configuration by targeting the gateway in the policy manifest. This conflicts with the [Advanced 4 Tier Model](https://gateway-api.sigs.k8s.io/concepts/security-model/#write-permissions-for-advanced-4-tier-model), where developers do not have write permissions on Gateways.

| Excessive developer permissions lead to a misconfiguration and/or unauthorised access.

|Medium| Considering the Tenant C scenario (represented in the Architecture Diagram), if a developer can create SecurityPolicy, ClientTrafficPolicy, EnvoyPatchPolicy or BackendTrafficPolicy objects in namespace C, they would be able to modify a Gateway configuration by attaching the policy to the gateway. In such scenarios, it is recommended to either:

a. Create a separate namespace, where developers have no permissions, > to host tenant C\'s gateway. Note that, due to design decisions, > the > SecurityPolicy/EnvoyPatchPolicy/ClientTrafficPolicy/BackendTrafficPolicy > object can only target resources deployed in the same namespace. > Therefore, having a separate namespace for the gateway would > prevent developers from attaching the policy to the gateway.

b. Forbid the creation of these policies for developers in namespace C.

On the other hand, in scenarios similar to tenants A and B, where a shared gateway namespace is in place, this issue is more limited. Note that in this scenario, developers don\'t have access to the shared gateway namespace.

In addition, it is important to mention that EnvoyPatchPolicy resources can also be attached to GatewayClass resources. This means that, in order to comply with the Advanced 4 Tier model, individuals with the Application Administrator role should not have access to this resource either. | |EGTM-003|EGTM-EG-001|Envoy Gateway| There is a risk that a threat actor could downgrade the security of proxied connections by configuring a weak set of cipher suites, compromising the confidentiality and integrity of proxied traffic.

| Exploit weak cipher suite configuration to downgrade security of proxied connections.

|Low| Users operating in highly regulated environments may need to tightly control the TLS protocol and associated cipher suites, blocking non-conforming incoming connections to the gateway.

EnvoyProxy bootstrap config can be customised as per the [customise EnvoyProxy](../operations/customize-envoyproxy) documentation. In addition, from v.1.0.0, it is possible to configure common TLS properties for a Gateway or XRoute through the [ClientTrafficPolicy](https://gateway.envoyproxy.io/latest/api/extension_types/#clienttrafficpolicy) object. | diff --git a/site/content/en/latest/tasks/traffic/load-balancing.md b/site/content/en/latest/tasks/traffic/load-balancing.md new file mode 100644 index 00000000000..90a816e7bc3 --- /dev/null +++ b/site/content/en/latest/tasks/traffic/load-balancing.md @@ -0,0 +1,922 @@ +--- +title: "Load Balancing" +--- + +[Envoy load balancing][] is a way of distributing traffic between multiple hosts within a single upstream cluster +in order to effectively make use of available resources. + +Envoy Gateway supports the following load balancing policies: + +- **Round Robin**: a simple policy in which each available upstream host is selected in round robin order. +- **Random**: load balancer selects a random available host. +- **Least Request**: load balancer uses different algorithms depending on whether hosts have the same or different weights. +- **Consistent Hash**: load balancer implements consistent hashing to upstream hosts. + +Envoy Gateway introduces a new CRD called [BackendTrafficPolicy][] that allows the user to describe their desired load balancing polices. +This instantiated resource can be linked to a [Gateway][], [HTTPRoute][] or [GRPCRoute][] resource. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +For better testing the load balancer, you can add more hosts in upstream cluster by increasing the replicas of one deployment: + +```shell +kubectl patch deployment backend -n default -p '{"spec": {"replicas": 4}}' +``` + +### Install the hey load testing tool + +Install the `Hey` CLI tool, this tool will be used to generate load and measure response times. + +Follow the installation instruction from the [Hey project] docs. + +## Round Robin + +This example will create a Load Balancer with Round Robin policy via [BackendTrafficPolicy][]. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The `hey` tool will be used to generate 100 concurrent requests. + +```shell +hey -n 100 -c 100 -host "www.example.com" http://${GATEWAY_HOST}/round +``` + +```console +Summary: + Total: 0.0487 secs + Slowest: 0.0440 secs + Fastest: 0.0181 secs + Average: 0.0307 secs + Requests/sec: 2053.1676 + + Total data: 50500 bytes + Size/request: 505 bytes + +Response time histogram: + 0.018 [1] |■■ + 0.021 [2] |■■■■ + 0.023 [10] |■■■■■■■■■■■■■■■■■■■■■■ + 0.026 [16] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.028 [7] |■■■■■■■■■■■■■■■■ + 0.031 [10] |■■■■■■■■■■■■■■■■■■■■■■ + 0.034 [17] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.036 [18] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.039 [11] |■■■■■■■■■■■■■■■■■■■■■■■■ + 0.041 [6] |■■■■■■■■■■■■■ + 0.044 [2] |■■■■ +``` + +As a result, you can see all available upstream hosts receive traffics evenly. + +```shell +kubectl get pods -l app=backend --no-headers -o custom-columns=":metadata.name" | while read -r pod; do echo "$pod: received $(($(kubectl logs $pod | wc -l) - 2)) requests"; done +``` + +```console +backend-69fcff487f-2gfp7: received 26 requests +backend-69fcff487f-69g8c: received 25 requests +backend-69fcff487f-bqwpr: received 24 requests +backend-69fcff487f-kbn8l: received 25 requests +``` + +You should note that this results may vary, the output here is for reference purpose only. + +## Random + +This example will create a Load Balancer with Random policy via [BackendTrafficPolicy][]. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The `hey` tool will be used to generate 1000 concurrent requests. + +```shell +hey -n 1000 -c 100 -host "www.example.com" http://${GATEWAY_HOST}/random +``` + +```console +Summary: + Total: 0.2624 secs + Slowest: 0.0851 secs + Fastest: 0.0007 secs + Average: 0.0179 secs + Requests/sec: 3811.3020 + + Total data: 506000 bytes + Size/request: 506 bytes + +Response time histogram: + 0.001 [1] | + 0.009 [421] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.018 [219] |■■■■■■■■■■■■■■■■■■■■■ + 0.026 [118] |■■■■■■■■■■■ + 0.034 [64] |■■■■■■ + 0.043 [73] |■■■■■■■ + 0.051 [41] |■■■■ + 0.060 [22] |■■ + 0.068 [19] |■■ + 0.077 [13] |■ + 0.085 [9] |■ +``` + +As a result, you can see all available upstream hosts receive traffics randomly. + +```shell +kubectl get pods -l app=backend --no-headers -o custom-columns=":metadata.name" | while read -r pod; do echo "$pod: received $(($(kubectl logs $pod | wc -l) - 2)) requests"; done +``` + +```console +backend-69fcff487f-bf6lm: received 246 requests +backend-69fcff487f-gwmqk: received 256 requests +backend-69fcff487f-mzngr: received 230 requests +backend-69fcff487f-xghqq: received 268 requests +``` + +You should note that this results may vary, the output here is for reference purpose only. + +## Least Request + +This example will create a Load Balancer with Least Request policy via [BackendTrafficPolicy][]. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The `hey` tool will be used to generate 100 concurrent requests. + +```shell +hey -n 100 -c 100 -host "www.example.com" http://${GATEWAY_HOST}/least +``` + +```console +Summary: + Total: 0.0489 secs + Slowest: 0.0479 secs + Fastest: 0.0054 secs + Average: 0.0297 secs + Requests/sec: 2045.9317 + + Total data: 50500 bytes + Size/request: 505 bytes + +Response time histogram: + 0.005 [1] |■■ + 0.010 [1] |■■ + 0.014 [8] |■■■■■■■■■■■■■■■ + 0.018 [6] |■■■■■■■■■■■ + 0.022 [11] |■■■■■■■■■■■■■■■■■■■■ + 0.027 [7] |■■■■■■■■■■■■■ + 0.031 [15] |■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.035 [13] |■■■■■■■■■■■■■■■■■■■■■■■■ + 0.039 [22] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.044 [12] |■■■■■■■■■■■■■■■■■■■■■■ + 0.048 [4] |■■■■■■■ +``` + +As a result, you can see all available upstream hosts receive traffics randomly, +and host `backend-69fcff487f-6l2pw` receives fewer requests than others. + +```shell +kubectl get pods -l app=backend --no-headers -o custom-columns=":metadata.name" | while read -r pod; do echo "$pod: received $(($(kubectl logs $pod | wc -l) - 2)) requests"; done +``` + +```console +backend-69fcff487f-59hvs: received 24 requests +backend-69fcff487f-6l2pw: received 19 requests +backend-69fcff487f-ktsx4: received 30 requests +backend-69fcff487f-nqxc7: received 27 requests +``` + +If you send one more requests to the `${GATEWAY_HOST}/least`, you can tell that host `backend-69fcff487f-6l2pw` is very likely +to get the attention of load balancer and receive this request. + +```console +backend-69fcff487f-59hvs: received 24 requests +backend-69fcff487f-6l2pw: received 20 requests +backend-69fcff487f-ktsx4: received 30 requests +backend-69fcff487f-nqxc7: received 27 requests +``` + +You should note that this results may vary, the output here is for reference purpose only. + +## Consistent Hash + +This example will create a Load Balancer with Consistent Hash policy via [BackendTrafficPolicy][]. + +The underlying consistent hash algorithm that Envoy Gateway utilise is [Maglev][], and it can derive hash from following aspects: + +- **SourceIP** +- **Header** +- **Cookie** + +They are also the supported value as consistent hash type. + +### Source IP + +This example will create a Load Balancer with Source IP based Consistent Hash policy. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The `hey` tool will be used to generate 100 concurrent requests. + +```shell +hey -n 100 -c 100 -host "www.example.com" http://${GATEWAY_HOST}/source +``` + +```console +Summary: + Total: 0.0539 secs + Slowest: 0.0500 secs + Fastest: 0.0198 secs + Average: 0.0340 secs + Requests/sec: 1856.5666 + + Total data: 50600 bytes + Size/request: 506 bytes + +Response time histogram: + 0.020 [1] |■■ + 0.023 [5] |■■■■■■■■■■■ + 0.026 [12] |■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.029 [16] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.032 [11] |■■■■■■■■■■■■■■■■■■■■■■■■ + 0.035 [7] |■■■■■■■■■■■■■■■■ + 0.038 [8] |■■■■■■■■■■■■■■■■■■ + 0.041 [18] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.044 [15] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.047 [4] |■■■■■■■■■ + 0.050 [3] |■■■■■■■ +``` + +As a result, you can see all traffics are routed to only one upstream host, since the client that send requests +has the same source IP. + +```shell +kubectl get pods -l app=backend --no-headers -o custom-columns=":metadata.name" | while read -r pod; do echo "$pod: received $(($(kubectl logs $pod | wc -l) - 2)) requests"; done +``` + +```console +backend-69fcff487f-grzkj: received 0 requests +backend-69fcff487f-n4d8w: received 100 requests +backend-69fcff487f-tb7zx: received 0 requests +backend-69fcff487f-wbzpg: received 0 requests +``` + +You can try different client to send out these requests, the upstream host that receives traffics may vary. + +### Header + +This example will create a Load Balancer with Header based Consistent Hash policy. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The `hey` tool will be used to generate 100 concurrent requests. + +```shell +hey -n 100 -c 100 -host "www.example.com" -H "FooBar: 1.2.3.4" http://${GATEWAY_HOST}/header +``` + +```console +Summary: + Total: 0.0579 secs + Slowest: 0.0510 secs + Fastest: 0.0323 secs + Average: 0.0431 secs + Requests/sec: 1728.6064 + + Total data: 53800 bytes + Size/request: 538 bytes + +Response time histogram: + 0.032 [1] |■■ + 0.034 [3] |■■■■■■ + 0.036 [1] |■■ + 0.038 [1] |■■ + 0.040 [7] |■■■■■■■■■■■■■■ + 0.042 [20] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.044 [20] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.045 [20] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.047 [16] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.049 [9] |■■■■■■■■■■■■■■■■■■ + 0.051 [2] |■■■■ +``` + +As a result, you can see all traffics are routed to only one upstream host, since the header of all requests are the same. + +```shell +kubectl get pods -l app=backend --no-headers -o custom-columns=":metadata.name" | while read -r pod; do echo "$pod: received $(($(kubectl logs $pod | wc -l) - 2)) requests"; done +``` + +```console +backend-69fcff487f-dvt9r: received 0 requests +backend-69fcff487f-f8qdl: received 100 requests +backend-69fcff487f-gnpm4: received 0 requests +backend-69fcff487f-t2pgm: received 0 requests +``` + +You can try to add different header to these requests, and the upstream host that receives traffics may vary. +The following output happens when you use `hey` to send another 100 requests with header `FooBar: 5.6.7.8`. + +```console +backend-69fcff487f-dvt9r: received 0 requests +backend-69fcff487f-f8qdl: received 100 requests +backend-69fcff487f-gnpm4: received 100 requests +backend-69fcff487f-t2pgm: received 0 requests +``` + +### Cookie + +This example will create a Load Balancer with Cookie based Consistent Hash policy. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +By sending 10 request with `curl` to the `${GATEWAY_HOST}/cookie`, you can see that all requests got routed to only +one upstream host, since they have same cookie setting. + +```shell +for i in {1..10}; do curl -I --header "Host: www.example.com" --cookie "FooBar=1.2.3.4" http://${GATEWAY_HOST}/cookie ; sleep 1; done +``` + +```shell +kubectl get pods -l app=backend --no-headers -o custom-columns=":metadata.name" | while read -r pod; do echo "$pod: received $(($(kubectl logs $pod | wc -l) - 2)) requests"; done +``` + +```console +backend-69fcff487f-5dxz9: received 0 requests +backend-69fcff487f-gpvl2: received 0 requests +backend-69fcff487f-pglgv: received 10 requests +backend-69fcff487f-qxr74: received 0 requests +``` + +You can try to set different cookie to these requests, the upstream host that receives traffics may vary. +The following output happens when you use `curl` to send another 10 requests with cookie `FooBar: 5.6.7.8`. + +```console +backend-69fcff487f-dvt9r: received 0 requests +backend-69fcff487f-f8qdl: received 0 requests +backend-69fcff487f-gnpm4: received 10 requests +backend-69fcff487f-t2pgm: received 10 requests +``` + +If the cookie has not been set in one request, Envoy Gateway will auto-generate a cookie for this request +according to the `ttl` and `attributes` field. + +In this example, the following cookie will be generated (see `set-cookie` header in response) if sending a request without cookie: + +```shell +curl -v --header "Host: www.example.com" http://${GATEWAY_HOST}/cookie +``` + +```console +> GET /cookie HTTP/1.1 +> Host: www.example.com +> User-Agent: curl/7.74.0 +> Accept: */* +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< date: Fri, 19 Jul 2024 16:49:57 GMT +< content-length: 458 +< set-cookie: FooBar="88358b9442700c56"; Max-Age=60; SameSite=Strict; HttpOnly +< +{ + "path": "/cookie", + "host": "www.example.com", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/7.74.0" + ], + "X-Envoy-Internal": [ + "true" + ], + "X-Forwarded-For": [ + "10.244.0.1" + ], + "X-Forwarded-Proto": [ + "http" + ], + "X-Request-Id": [ + "1adeaaf7-d45c-48c8-9a4d-eadbccb2fd50" + ] + }, + "namespace": "default", + "ingress": "", + "service": "", + "pod": "backend-69fcff487f-5dxz9" +``` + + +[Envoy load balancing]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/load_balancing/overview +[BackendTrafficPolicy]: ../../../api/extension_types#backendtrafficpolicy +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway/ +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute/ +[GRPCRoute]: https://gateway-api.sigs.k8s.io/api-types/grpcroute/ +[Hey project]: https://github.com/rakyll/hey +[Maglev]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/load_balancing/load_balancers#maglev diff --git a/site/content/en/news/blogs/1.0-release/1.0-release.md b/site/content/en/news/blogs/1.0-release/1.0-release.md index 5306d4bfbc3..3e2d4f58bd5 100644 --- a/site/content/en/news/blogs/1.0-release/1.0-release.md +++ b/site/content/en/news/blogs/1.0-release/1.0-release.md @@ -12,19 +12,19 @@ Today we’re ecstatic to announce the 1.0 release of Envoy Gateway (EG) for Kub After nearly two years with contributions from over 90 engineers we are proud to say EG meets the goals that Matt outlined in the [original post](https://blog.envoyproxy.io/introducing-envoy-gateway-ad385cc59532) introducing the project, summarized here: * Built around the (then emerging) [Kubernetes Gateway API](https://gateway-api.sigs.k8s.io) * Addresses common needs with a solution that is simple to configure and understand -* Provides [great docs](/v1.0.2/tasks/) for common use cases to enable ease of adoption +* Provides [great docs](/v1.0/tasks/) for common use cases to enable ease of adoption * Empowers the community and vendors to drive the project forward through an extensible API -Can’t wait to try it? Visit the EG [tasks](/v1.0.2/tasks/) to get started with Envoy Gateway 1.0 +Can’t wait to try it? Visit the EG [tasks](/v1.0/tasks/) to get started with Envoy Gateway 1.0 ## Envoy Gateway 1.0 The 1.0 release brings a lot of functionality. In addition to implementing the full Kubernetes Gateway API – including the awesome Envoy L7 features you love like per-request policy, load balancing, and best-in-class observability – it also goes further, Envoy Gateway 1.0: * Provides support for common features such as Rate Limiting and OAuth2.0 * Deploys and upgrades Envoy on your behalf, easing operations and lifecycle management -* Introduces extensions to the Kubernetes Gateway API to address [Client](/v1.0.2/api/extension_types#clienttrafficpolicy), [Backend](/v1.0.2/api/extension_types#backendtrafficpolicy), and [Security](/v1.0.2/api/extension_types#securitypolicy) settings and features -* Is easily extensible through the [EnvoyPatchPolicy](/v1.0.2/tasks/extensibility/envoy-patch-policy/) API to allow you to configure any Envoy behavior (including stuff you build yourself!) -* Has a CLI, [egctl](/v1.0.2/tasks/operations/egctl/), for interacting with and debugging the system +* Introduces extensions to the Kubernetes Gateway API to address [Client](/v1.0/api/extension_types#clienttrafficpolicy), [Backend](/v1.0/api/extension_types#backendtrafficpolicy), and [Security](/v1.0/api/extension_types#securitypolicy) settings and features +* Is easily extensible through the [EnvoyPatchPolicy](/v1.0/tasks/extensibility/envoy-patch-policy/) API to allow you to configure any Envoy behavior (including stuff you build yourself!) +* Has a CLI, [egctl](/v1.0/tasks/operations/egctl/), for interacting with and debugging the system * Comes with a large (and growing!) set of scenarios to make common use cases straightforward to implement ## What Does 1.0 Mean for the Project? @@ -57,9 +57,9 @@ Following the 1.0 release, we’ll be focusing on: * Exposing more knobs to fine tune more traffic shaping parameters * **Features**: More API Gateway features such as authorization (IP Addresses, JWT Claims, API Key, etc.) and compression * **Scale**: Building out a performance benchmarking tool into our CI -* **Extensibility**: We plan on providing a first-class API for data plane extensions such as Lua, WASM, and Ext Proc to enable users to implement their custom use cases +* **Extensibility**: We plan on providing a first-class API for data plane extensions such as Lua, Wasm, and Ext Proc to enable users to implement their custom use cases * **Outside of Kubernetes**: Running Envoy Gateway in non-k8s environments - this has been an [explicit goal](/contributions/design/goals#all-environments) and we’d like to focus on this in the coming months. Envoy Proxy already supports running on bare metal environments, with Envoy Gateway users getting the added advantage of a simpler API -* **Debug**: And a lot of capabilities with the [egctl CLI](/v1.0.2/tasks/operations/egctl/) +* **Debug**: And a lot of capabilities with the [egctl CLI](/v1.0/tasks/operations/egctl/) ## Get Started -If you’ve been looking to use Envoy as a Gateway, check out our [quickstart guide](/v1.0.2/tasks/quickstart) and give it a try! If you’re interested in contributing, check out our [guide for getting involved](/contributions/)! +If you’ve been looking to use Envoy as a Gateway, check out our [quickstart guide](/v1.0/tasks/quickstart) and give it a try! If you’re interested in contributing, check out our [guide for getting involved](/contributions/)! diff --git a/site/content/en/news/releases/matrix.md b/site/content/en/news/releases/matrix.md index 9345aaa184c..337b26d1262 100644 --- a/site/content/en/news/releases/matrix.md +++ b/site/content/en/news/releases/matrix.md @@ -7,6 +7,7 @@ Envoy Gateway relies on the Envoy Proxy and the Gateway API, and runs within a K | Envoy Gateway version | Envoy Proxy version | Rate Limit version | Gateway API version | Kubernetes version | |-----------------------|-----------------------------|--------------------|---------------------|----------------------------| +| v1.1 | **distroless-v1.31.0** | **91484c59** | **v1.1.0** | v1.27, v1.28, v1.29, v1.30 | | v1.0 | **distroless-v1.29.2** | **19f2079f** | **v1.0.0** | v1.26, v1.27, v1.28, v1.29 | | v0.6 | **distroless-v1.28-latest** | **b9796237** | **v1.0.0** | v1.26, v1.27, v1.28 | | v0.5 | **v1.27-latest** | **e059638d** | **v0.7.1** | v1.25, v1.26, v1.27 | diff --git a/site/content/en/news/releases/notes/_index.md b/site/content/en/news/releases/notes/_index.md new file mode 100644 index 00000000000..e37b8f2e06c --- /dev/null +++ b/site/content/en/news/releases/notes/_index.md @@ -0,0 +1,5 @@ +--- +title: "Notes" +weight: 90 +description: This section includes Releases Notes of Envoy Gateway. +--- diff --git a/site/content/en/latest/releases/v0.1.0.md b/site/content/en/news/releases/notes/v0.1.0.md similarity index 99% rename from site/content/en/latest/releases/v0.1.0.md rename to site/content/en/news/releases/notes/v0.1.0.md index 3d55118a846..40a9b920527 100644 --- a/site/content/en/latest/releases/v0.1.0.md +++ b/site/content/en/news/releases/notes/v0.1.0.md @@ -7,3 +7,4 @@ Date: May 16, 2022 ## Documentation - The initial open source release describing project goals and high-level design. + diff --git a/site/content/en/v1.0.2/releases/v0.2.0-rc1.md b/site/content/en/news/releases/notes/v0.2.0-rc1.md similarity index 93% rename from site/content/en/v1.0.2/releases/v0.2.0-rc1.md rename to site/content/en/news/releases/notes/v0.2.0-rc1.md index 59da2015f9d..a2adb24fda5 100644 --- a/site/content/en/v1.0.2/releases/v0.2.0-rc1.md +++ b/site/content/en/news/releases/notes/v0.2.0-rc1.md @@ -1,5 +1,5 @@ --- -title: "v0.2.0-rc.1" +title: "v0.2.0-rc1" publishdate: 2022-08-31 --- @@ -12,7 +12,7 @@ Date: August 31, 2022 - Added the EnvoyGateway API type for configuring Envoy Gateway. - Added the EnvoyProxy API type for configuring managed Envoys. -## CI +## Ci - Added tooling to build, run, etc. Envoy Gateway. ## Providers @@ -29,9 +29,10 @@ Date: August 31, 2022 - Added the gatewayapi translator to translate Gateway API and associated resources to the IR and manage - Gateway API status. -## Message Service +## Message service - Added infra and xds IR watchable map messages for inter-component communication. - Added a Runner to each component to support pub/sub between components. -## Infra Manager +## Infra manager - Added Kubernetes Infra Manager to manage Envoy infrastructure running in a Kubernetes cluster. + diff --git a/site/content/en/v1.0.2/releases/v0.2.0-rc2.md b/site/content/en/news/releases/notes/v0.2.0-rc2.md similarity index 93% rename from site/content/en/v1.0.2/releases/v0.2.0-rc2.md rename to site/content/en/news/releases/notes/v0.2.0-rc2.md index 756ccfb18da..f1fd55075ce 100644 --- a/site/content/en/v1.0.2/releases/v0.2.0-rc2.md +++ b/site/content/en/news/releases/notes/v0.2.0-rc2.md @@ -1,5 +1,5 @@ --- -title: "v0.2.0-rc.2" +title: "v0.2.0-rc2" publishdate: 2022-09-29 --- @@ -10,7 +10,7 @@ Date: September 29, 2022 - Added `kube-demo` target to demonstrate Envoy Gateway functionality. - Added developer debugging documentation. -## CI +## Ci - Added Gateway API conformance tests. ## Providers @@ -27,8 +27,9 @@ Date: September 29, 2022 - Added support for request modifier and redirect filters. - Added support to return 500 responses for invalid backends. -## Message Service +## Message service - Updated IRs to support managing multiple Envoy fleets. -## Infra Manager +## Infra manager - Separate Envoy infrastructure is created per Gateway. + diff --git a/site/content/en/latest/releases/v0.2.0.md b/site/content/en/news/releases/notes/v0.2.0.md similarity index 91% rename from site/content/en/latest/releases/v0.2.0.md rename to site/content/en/news/releases/notes/v0.2.0.md index 6ebad0cf8f2..2351c0b0b90 100644 --- a/site/content/en/latest/releases/v0.2.0.md +++ b/site/content/en/news/releases/notes/v0.2.0.md @@ -15,7 +15,7 @@ Date: October 19, 2022 - Added the EnvoyGateway API type for configuring Envoy Gateway. - Added the EnvoyProxy API type for configuring managed Envoys. -## CI Tooling Testing +## Ci tooling testing - Added tooling to build, run, etc. Envoy Gateway. - Added Gateway API conformance tests. - Added Make-based tooling to fetch all tools so checks (code lint, spellchecks) and tests can be run locally. @@ -27,20 +27,22 @@ Date: October 19, 2022 - Added IR validation. ## Translator -- Added the gatewayapi translator to translate Gateway API and associated resources to the IR and manage the status of Gateway API resources. +- Added the gatewayapi translator to translate Gateway API and associated resources to the IR and manage the +- status of Gateway API resources. - Added the xDS translator to translate the xds IR to xDS resources. -## Message Service +## Message-service - Added infra and xds IR watchable map messages for inter-component communication. - Added a Runner to each Envoy Gateway component to support pub/sub between components. - Added support for managing multiple separate Envoy proxy fleets. -## Infra Manager +## Infra-manager - Added Kubernetes Infra Manager to manage Envoy infrastructure running in a Kubernetes cluster. - Added support for managing a separate Envoy infrastructure per Gateway. ## Providers -- Added the Kubernetes provider with support for managing GatewayClass, Gateway, HTTPRoute, ReferenceGrant, and TLSRoute resources. +- Added the Kubernetes provider with support for managing GatewayClass, Gateway, HTTPRoute, ReferenceGrant, and +- TLSRoute resources. - Due to Issue #539, a ReferenceGrant is not removed from the system when unreferenced. - Due to Issue #577, TLSRoute is not being tested for Gateway API conformance. - Added watchers for dependent resources of managed Envoy infrastructure to trigger reconciliation. @@ -51,3 +53,4 @@ Date: October 19, 2022 - Added xDS server support to configure managed Envoys using Delta xDS. - Added initial support for mTLS between the xDS server and managed Envoys. - Due to envoyproxy/go-control-plane Issue #599, Envoy Gateway logs the private key of HTTPS listeners. + diff --git a/site/content/en/v1.0.2/releases/v0.3.0-rc.1.md b/site/content/en/news/releases/notes/v0.3.0-rc.1.md similarity index 98% rename from site/content/en/v1.0.2/releases/v0.3.0-rc.1.md rename to site/content/en/news/releases/notes/v0.3.0-rc.1.md index 4c50801e6f3..dc69be5b050 100644 --- a/site/content/en/v1.0.2/releases/v0.3.0-rc.1.md +++ b/site/content/en/news/releases/notes/v0.3.0-rc.1.md @@ -25,7 +25,7 @@ Date: February 02, 2023 - Added Support for Routes ReferenceGrant - Added Support for Namespace Server Config Type -## CI Tooling Testing +## Ci tooling testing - Fixes Make Image Failed in Darwin - Fixes Wait for Job Succeeded before conformance test - Upgraded Echoserver Image Tag @@ -62,3 +62,4 @@ Date: February 02, 2023 ## xDS - Fixed Start xDS Server Watchable Map Panics - Enabled Access Logging for xDS Components + diff --git a/site/content/en/latest/releases/v0.3.0.md b/site/content/en/news/releases/notes/v0.3.0.md similarity index 99% rename from site/content/en/latest/releases/v0.3.0.md rename to site/content/en/news/releases/notes/v0.3.0.md index 4eacf8c45c8..d3ff1e6a5d1 100644 --- a/site/content/en/latest/releases/v0.3.0.md +++ b/site/content/en/news/releases/notes/v0.3.0.md @@ -36,7 +36,7 @@ Date: February 09, 2023 - Added Support for Namespace Server Config Type - Added initial management of Envoy Proxy deployment via EnvoyProxy API -## CI Tooling Testing +## Ci tooling testing - Fixed Make Image Failed in Darwin - Fixed Wait for Job Succeeded before conformance test - Upgraded Echoserver Image Tag @@ -75,3 +75,4 @@ Date: February 09, 2023 ## xDS - Fixed Start xDS Server Watchable Map Panics - Enabled Access Logging for xDS Components + diff --git a/site/content/en/latest/releases/v0.4.0-rc.1.md b/site/content/en/news/releases/notes/v0.4.0-rc.1.md similarity index 98% rename from site/content/en/latest/releases/v0.4.0-rc.1.md rename to site/content/en/news/releases/notes/v0.4.0-rc.1.md index 927069f641f..3541435e8df 100644 --- a/site/content/en/latest/releases/v0.4.0-rc.1.md +++ b/site/content/en/news/releases/notes/v0.4.0-rc.1.md @@ -24,7 +24,7 @@ Date: April 13, 2023 - Added Custom Envoy Gateway Extensions Framework - Added Support for Service Method Match in GRPCRoute -## CI Tooling Testing +## Ci tooling testing - Fixed CI Flakes During Helm Install - Added Test To Ensure Static xDS Cluster Has Same Field Values as Dynamic Cluster - Added egctl to Build and Test CI Workflow @@ -54,3 +54,4 @@ Date: April 13, 2023 - Added egctl CLI Tool - Added egctl Support for Dry Runs of Gateway API Config - Added egctl Support for Dumping Envoy Proxy xDS Resources + diff --git a/site/content/en/v1.0.2/releases/v0.4.0.md b/site/content/en/news/releases/notes/v0.4.0.md similarity index 98% rename from site/content/en/v1.0.2/releases/v0.4.0.md rename to site/content/en/news/releases/notes/v0.4.0.md index 12c40904088..47035979de8 100644 --- a/site/content/en/v1.0.2/releases/v0.4.0.md +++ b/site/content/en/news/releases/notes/v0.4.0.md @@ -27,7 +27,7 @@ Date: April 24, 2023 - Added Support for Service Method Match in GRPCRoute - Fixed a Bug in the Extension Hooks for xDS Virtual Hosts and Routes -## CI Tooling Testing +## Ci tooling testing - Fixed CI Flakes During Helm Install - Added Test To Ensure Static xDS Cluster Has Same Field Values as Dynamic Cluster - Added egctl to Build and Test CI Workflow @@ -57,3 +57,4 @@ Date: April 24, 2023 - Added egctl CLI Tool - Added egctl Support for Dry Runs of Gateway API Config - Added egctl Support for Dumping Envoy Proxy xDS Resources + diff --git a/site/content/en/latest/releases/v0.5.0-rc.1.md b/site/content/en/news/releases/notes/v0.5.0-rc.1.md similarity index 98% rename from site/content/en/latest/releases/v0.5.0-rc.1.md rename to site/content/en/news/releases/notes/v0.5.0-rc.1.md index bda070995c8..718ce1635de 100644 --- a/site/content/en/latest/releases/v0.5.0-rc.1.md +++ b/site/content/en/news/releases/notes/v0.5.0-rc.1.md @@ -29,10 +29,11 @@ Date: July 26, 2023 - Added Admin Server for Envoy Gateway - Added Pprof Debug Support for Envoy Gateway - Added Support to Watch for Resources in Select Namespaces + ### Breaking Changes - Renamed field in EnvoyGateway API from Extension to ExtensionManager -## CI Tooling Testing +## Ci tooling testing - Added Retest Github Action - Added CherryPick Github Action - Added E2E Step in Github CI Workflow @@ -48,7 +49,7 @@ Date: July 26, 2023 - Skipped HTTPRouteRedirectPortAndScheme Test ## Translator -### Breaking changes +### Breaking Changes - Renamed IR resources from - to / - which also affects generated Xds Resources @@ -69,3 +70,4 @@ Date: July 26, 2023 ## Cli - Added egctl x translate Support to generate default missing Resources - Added egctl x translate Support for AuthenticationFilter and EnvoyPatchPolicy + diff --git a/site/content/en/v1.0.2/releases/v0.5.0-rc.1.md b/site/content/en/news/releases/notes/v0.5.0.md similarity index 97% rename from site/content/en/v1.0.2/releases/v0.5.0-rc.1.md rename to site/content/en/news/releases/notes/v0.5.0.md index bda070995c8..01674c039e0 100644 --- a/site/content/en/v1.0.2/releases/v0.5.0-rc.1.md +++ b/site/content/en/news/releases/notes/v0.5.0.md @@ -1,5 +1,5 @@ --- -title: "v0.5.0-rc.1" +title: "v0.5.0" publishdate: 2023-07-26 --- @@ -29,10 +29,11 @@ Date: July 26, 2023 - Added Admin Server for Envoy Gateway - Added Pprof Debug Support for Envoy Gateway - Added Support to Watch for Resources in Select Namespaces + ### Breaking Changes - Renamed field in EnvoyGateway API from Extension to ExtensionManager -## CI Tooling Testing +## Ci tooling testing - Added Retest Github Action - Added CherryPick Github Action - Added E2E Step in Github CI Workflow @@ -48,7 +49,7 @@ Date: July 26, 2023 - Skipped HTTPRouteRedirectPortAndScheme Test ## Translator -### Breaking changes +### Breaking Changes - Renamed IR resources from - to / - which also affects generated Xds Resources @@ -69,3 +70,4 @@ Date: July 26, 2023 ## Cli - Added egctl x translate Support to generate default missing Resources - Added egctl x translate Support for AuthenticationFilter and EnvoyPatchPolicy + diff --git a/site/content/en/latest/releases/v0.6.0-rc.1.md b/site/content/en/news/releases/notes/v0.6.0-rc.1.md similarity index 95% rename from site/content/en/latest/releases/v0.6.0-rc.1.md rename to site/content/en/news/releases/notes/v0.6.0-rc.1.md index 5141bc27966..2757f5da3f5 100644 --- a/site/content/en/latest/releases/v0.6.0-rc.1.md +++ b/site/content/en/news/releases/notes/v0.6.0-rc.1.md @@ -30,18 +30,24 @@ Date: Oct 27, 2023 - Added Support for enabling EnvoyProxy Virtual Host metrics - Added Support for Merging Gateway resources onto the same infrastructure -### Breaking changes +### Breaking Changes - Removed the AuthenticationFilter CRD - Removed the RateLimitFilter CRD - Enabled EnvoyProxy Prometheus Endpoint by default with an option to disable it - Updated the Bootstrap field within the EnvoyProxy CRD with an additional value - field to specify bootstrap config -## watchable +## Ci tooling testing +- + +## Conformance +- + +## Watchable - Improved caching of resource by implementing a compare function agnostic of resource order ## Translator -### Breaking changes +### Breaking Changes - Added support for routing to EndpointSlice endpoints - Added support for HTTPRoute Timeouts - Added support for multiple RequestMirror filters per HTTPRoute rule @@ -54,7 +60,7 @@ Date: Oct 27, 2023 - Added support for reconciling ServiceImport CRD - Added support for selectively watching resources based on Namespace Selector -## XDS +## xDS - Fixed Layered Runtime warnings - Upgraded to the latest version of go-control-plane that fixed xDS Resource ordering issues for ADS. - Added HTTP2 Keep Alives to the xds connection diff --git a/site/content/en/v1.0.2/releases/v0.6.0.md b/site/content/en/news/releases/notes/v0.6.0.md similarity index 89% rename from site/content/en/v1.0.2/releases/v0.6.0.md rename to site/content/en/news/releases/notes/v0.6.0.md index 2b8714030b0..c6453f9987b 100644 --- a/site/content/en/v1.0.2/releases/v0.6.0.md +++ b/site/content/en/news/releases/notes/v0.6.0.md @@ -28,7 +28,7 @@ Date: Nov 1, 2023 - Added Support for enabling EnvoyProxy Virtual Host metrics - Added Support for Merging Gateway resources onto the same infrastructure -### Breaking changes +### Breaking Changes - Removed the AuthenticationFilter CRD - Removed the RateLimitFilter CRD - Moved EnvoyProxy CRD from `config.gateway.envoyproxy.io` to `gateway.envoyproxy.io` @@ -48,11 +48,11 @@ Date: Nov 1, 2023 - Improved caching of resource by implementing a compare function agnostic of resource order ## Translator -- Added support for routing to EndpointSlice endpoints -- Added support for HTTPRoute Timeouts -- Added support for multiple RequestMirror filters per HTTPRoute rule -- Use / instead of - in IR Route Names -- Added Support to ignore ports in Host header +- Added support for routing to EndpointSlice endpoints +- Added support for HTTPRoute Timeouts +- Added support for multiple RequestMirror filters per HTTPRoute rule +- Use / instead of - in IR Route Names +- Added Support to ignore ports in Host header ## Providers - Added the generationChangedPredicate to most resources to limit resource reconiliation @@ -60,11 +60,11 @@ Date: Nov 1, 2023 - Added support for reconciling ServiceImport CRD - Added support for selectively watching resources based on Namespace Selector - -## XDS +## xDS - Fixed Layered Runtime warnings - Upgraded to the latest version of go-control-plane that fixed xDS Resource ordering issues for ADS. - Added HTTP2 Keep Alives to the xds connection ## Cli - Added Support for egctl stats command + diff --git a/site/content/en/v1.0.2/releases/v1.0.0.md b/site/content/en/news/releases/notes/v1.0.0-rc.1.md similarity index 75% rename from site/content/en/v1.0.2/releases/v1.0.0.md rename to site/content/en/news/releases/notes/v1.0.0-rc.1.md index 22630e9e98c..2a5fbebd6fd 100644 --- a/site/content/en/v1.0.2/releases/v1.0.0.md +++ b/site/content/en/news/releases/notes/v1.0.0-rc.1.md @@ -1,33 +1,24 @@ --- -title: "v1.0.0" -publishdate: 2024-03-13 +title: "v1.0.0-rc.1" +publishdate: 2023-11-01 --- -Date: March 13, 2024 +Date: Nov 1, 2023 ## Documentation -- Added User Guide for Local Ratelimit -- Added User Guide for Circuit Breaker +- Added User Guide for local rate limit +- Added User Guide for circuit breaker - Added User Guide for fault injection - Added User Guide for EnvoyProxy extraArgs - Added User Guide for Timeouts in ClientTrafficPolicy - Added User Guide for JWT claim base routing - Added User Guide for HTTP Timeout - Added User Guide for Retry in BackendTrafficPolicy -- Added User Guide for Basic Auth +- Added User Guide for basic auth - Added User Guide for OIDC - Added User Guide for ClientTrafficPolicy - Added User Guide for BackendTrafficPolicy -- Added User Guide for Basic Auth using HTTPS -- Added User Guide for External Authorization -- Added User Guide for Routing Outside Kubernetes -- Added User Guide for BackendTLSPolicy -- Added User Guide for Mutual TLS from External Clients to the Gateway -- Added User Guide for Control Plane Authentication using custom certs -- Added User Guide for Multiple Gatewayclass and Merge Gateways Deployment Mode - Added `Type` and `required` for CRD API doc -- Refactored Structure of User Guide docs -- Refactored Move Design docs under "Get Involved" - Updated crd-ref-docs to 0.0.10 - Updated Envoy proxy image to envoy:distroless-dev in main @@ -39,11 +30,11 @@ Date: March 13, 2024 ## API - Added Support for Downstream QUIC/HTTP3 in ClientTrafficPolicy CRD - Added Support for Downstream MTLS in ClientTrafficPolicy CRD -- Added Support for Enabling EnvoyHeaders in ClientTrafficPolicy CRD +- Added Support for enabling EnvoyHeaders in ClientTrafficPolicy CRD - Added Support for DisableMergeSlash and escapedSlashesAction in ClientTrafficPolicy CRD - Added Support for EnableTrailers in HTTP/1.1 in ClientTrafficPolicy CRD - Added Support for Preserving header letter-case on HTTP/1 in ClientTrafficPolicy CRD -- Added Support for Enabling HTTP/1.0 and HTTP/0.9 in ClientTrafficPolicy CRD +- Added Support for enabling HTTP/1.0 and HTTP/0.9 in ClientTrafficPolicy CRD - Added Support for Client IP Detection using XFF in ClientTrafficPolicy CRD - Added Support for Client IP Detection using Custom Header in ClientTrafficPolicy CRD - Added Support for Connection Timeouts in ClientTrafficPolicy CRD @@ -61,14 +52,11 @@ Date: March 13, 2024 - Added Support for Slow start mode in BackendTrafficPolicy CRD - Added Support for Proxy protocol in BackendTrafficPolicy CRD - Added Support for TCPKeepAlive in BackendTrafficPolicy CRD -- Added Support for PolicyStatus in BackendTrafficPolicy CRD -- Added Support for PolicyStatus in ClientTrafficPolicy CRD -- Added Support for PolicyStatus in SecurityPolicy CRD - Added Support for OIDC in SecurityPolicy CRD - Added Support for Basic Auth in SecurityPolicy CRD - Added Support for RedirectURL and signoutPath to OIDC in SecurityPolicy CRD - Added Support for ExtractFrom headers and params to JWT in SecurityPolicy CRD -- Added Support for External Authorization in SecurityPolicy CRD +- Added Support for External authorization in SecurityPolicy CRD - Added Support for RecomputeRoute field to JWT in SecurityPolicy CRD - Added Support for AllowCredentials knob to CORS setting in SecurityPolicy CRD - Added Support for Extract from different identifier to JWT in SecurityPolicy CRD @@ -78,7 +66,7 @@ Date: March 13, 2024 - Added Support for MergeGateways in EnvoyPatchPolicy CRD - Added Support for Upstream TLS by implementing BackendTLSPolicy CRD - Added Support for LabelSelector type for NamespaceSelectors in EnvoyGateway Configuration -- Added Support for Ratelimit prometheus in EnvoyGateway Configuration +- Added Support for ratelimit prometheus in EnvoyGateway Configuration - Added Support for Gracefully drain listeners before envoy shutdown on pod termination in EnvoyProxy CRD - Added Support for Configuring externalTrafficPolicy to the envoy service in EnvoyProxy CRD - Added Support for Envoy extra args in EnvoyProxy CRD @@ -91,23 +79,17 @@ Date: March 13, 2024 ### Breaking Changes - Use wildcard to match AllowOrigins to CORS in SecurityPolicy CRD -- Remove Hostnetwork support in EnvoyProxy CRD ## Conformance - Replaced backend image from gcr.io/k8s-staging-ingressconformance/echoserver to gcr.io/k8s-staging-gateway-api/echo-basic ## Testing -- Added e2e test for Header Case-Preserving +- Added e2e test for header case-preserving +- Added LoadBalancerIP validation to prevent trailing period - Added e2e test for Timeout in ClientTrafficPolicy -- Added e2e test for JWT claim base routing +- Added e2e test for jwt claim base routing - Added e2e test for OIDC - Added e2e test for BackendTrafficPolicy Retry -- Added e2e test for Backend Upgrade -- Added e2e test for External Authorization -- Added e2e test for Backend TLS policy -- Added e2e test for Envoy Gateway Release Upgrade -- Added e2e test for Weighted backend -- Added validation for LoadBalancerIP to prevent trailing period ## Translator - Fixed Prefix match to prevent mismatching routes with the same prefix @@ -127,15 +109,6 @@ Date: March 13, 2024 - Fixed Relaxing HTTPS restriction for OIDC token endpoint - Fixed Panic when translating routes with empty backends - Fixed Xds translation should be done in a best-effort manner -- Fixed Delete unused status keys from watchable -- Fixed Ignoring finalizers when comparing envoy proxy service -- Fixed Don't override the ALPN array if HTTP/3 is enabled -- Fixed Add h3 ALPN by default if HTTP/3 is enabled -- Fixed Change the Merge behavior to Replace for SecurityPolicy/BackendTrafficPolicy -- Fixed Use service port in alt-svc header if HTTP/3 is enabled -- Fixed Prevent policies targeting non-TLS listeners on the same port from conflicting -- Fixed Skip the ReasonTargetNotFound for all policies -- Fixed Skip publishing empty status for all policies - Added Support for validating regex before sending to Envoy - Added Support for setting spec.addresses.value into ClusterIP when Service Type is ClusterIP - Added Unsupported status condition for filters within BackendRef @@ -144,7 +117,6 @@ Date: March 13, 2024 - Added Support for overriding condition to BackendTrafficPolicy and SecurityPolicy - Added Support for default retry budget and retry host predicate - Added Support for implementing gateway.spec.infrastructure -- Added Support for Upstream TLS to multiple Backends - Added Validation for CA Cert in ClientTrafficPolicy ## Providers @@ -166,5 +138,4 @@ Date: March 13, 2024 - Added Support for egctl x status - Added Support for egctl experimental dashboard envoy-proxy - Added Support for egctl config ratelimit -- Added Support for egctl translate from gateway-api resources to IR diff --git a/site/content/en/latest/releases/v1.0.0.md b/site/content/en/news/releases/notes/v1.0.0.md similarity index 100% rename from site/content/en/latest/releases/v1.0.0.md rename to site/content/en/news/releases/notes/v1.0.0.md diff --git a/site/content/en/latest/releases/v1.0.1.md b/site/content/en/news/releases/notes/v1.0.1.md similarity index 100% rename from site/content/en/latest/releases/v1.0.1.md rename to site/content/en/news/releases/notes/v1.0.1.md diff --git a/site/content/en/latest/releases/v1.0.2.md b/site/content/en/news/releases/notes/v1.0.2.md similarity index 100% rename from site/content/en/latest/releases/v1.0.2.md rename to site/content/en/news/releases/notes/v1.0.2.md diff --git a/site/content/en/news/releases/notes/v1.1.0-rc.1.md b/site/content/en/news/releases/notes/v1.1.0-rc.1.md new file mode 100644 index 00000000000..e0ad9998d67 --- /dev/null +++ b/site/content/en/news/releases/notes/v1.1.0-rc.1.md @@ -0,0 +1,225 @@ +--- +title: "v1.1.0-rc.1" +publishdate: 2024-07-08 +--- + +Date: July 8, 2024 + +## Documentation +- Added Performance Benchmarking Document +- Added User Guide for Zipkin Tracing +- Added User Guide for Customizing Ordering of Filters +- Added User Guide for External Processing Filter in EnvoyExtensionPolicy +- Added User Guide for installation of egctl with brew +- Added User Guide for Client Buffer Size Limit +- Added User Guide for Client Idle Timeout +- Added Chinese translation for release notes, roadmap, installation, development, contribution and several User Guides +- Added User Guide for Backend resource +- Added GA Blog Post +- Added Threat Model +- Added Adopters section to docs +- Added User Guide and Dashboards for Control Plane and Resource Observability +- Added User Guide for Connection Limits in ClientTrafficPolicy +- Added User Guide on using Private Key Provider +- Added Design Doc for Authorization +- Added Design Doc for XDS Metadata +- Added Design Doc for Backend resource +- Added Design Doc for Control Plane Observability +- Added Design Doc for EnvoyExtensionPolicy +- Added Design Doc for External Processing in EnvoyExtensionPolicy +- Updated Access Logging User Guide to include filtering with CEL Expression +- Updated Access Logging User Guide to include Metadata +- Updated Development Guide to require Golang 1.22 +- Updated Quickstart User Guide to fetch GATEWAY_HOST from Gateway resource +- Updated Site to reflect GA status +- Updated HTTP Redirect User Guide to not set a redirect port or require a BackendRef +- Updated Observability User Guides to use gateway-addons-helm +- Updated Gateway-API User Guide to reflect support for BackendRef filters +- Updated HTTP Timeouts User Guide to highlight default Envoy timeouts +- Updated Installation Guide to use server-side apply +- Updated Installation Guide to refer to values.yaml docs +- Updated BackendTLSPolicy User Guide to GW-API v1.1.0 +- Updated User Guides to use tabs when applying yaml from file or stdin +- Updated OIDC User Guide to use HTTPS redirect URLs +- Updated Order of versions in Site +- Updated Extensbility User Gudie to use yaml-format patches +- Updated Quickstart Guide to include next steps +- Updated CRD docs to include enum values +- Updated Extensibility User Guide with Envoy Patch Policy examples +- Updated structure of docs: rename Guides to Tasks, move Contribution +- Updated Support Matrix +- Updated egctl x status docs for xRoute and xPolicy +- Updated egctl User Guide with Install and Uninstall commands +- Updated GRPCRoute docs to use v1 instead of v1alpha2 +- Fixed Rate Limiting User Guide to use correct CIDR matcher type names +- Fixed User Guide for JWT-based routing +- Fixed JSON Access Log Example +- Use linkinator to detect dead links in docs +- Use helm-docs to generate chart docs +- Support Not-Implemented-Hide marker in API docs + +## Installation +- Added new gateway-addons-helm chart for Observability +- Added support for global image settings for all images in Envoy Gateway helm chart +- Added Support for PodDistruptionBudget for Envoy Gateway +- Added Support for TopologySpreadConstraints for Envoy Gateway +- Added Support for Tolerations for Envoy Gateway +- Added Support for Ratelimit image pull secrets and pull policy +- Updated ttlSecondsAfterFinished on certgen job to 30 by default +- Updated Envoy Gateway ImagePullPolicy to IfNotPresent released charts +- Remove envoy-gateway-metrics-service and merge its contents into envoy-gateway service + +## API +- Added Support for Gateway-API v1.1.0 +- Added new Backend CRD +- Added new EnvoyExtensionPolicy CRD +- Added Support for Plural Target Refs and Target Selectors in xPolicy CRDs +- Added Support for Backend CRD BackendRefs in HTTPRoute, GRPCRoute and EnvoyExtensionPolicy CRDs +- Added Support for Custom Extension Server Policy CRDs in EnvoyGateway Config +- Added Support for Custom ShutDownManager Image in EnvoyGateway Config +- Added Support for Leader Election in EnvoyGateway Config +- Added Support for Connecting to Extension Server over Unix Domain Socket in EnvoyGateway Config +- Added Support for Proxy PodDisruptionBudget in EnvoyProxy CRD +- Added Support for Running Envoy Proxy as a Daemonset in EnvoyProxy CRD +- Added Support for Proxy Loadbalancer Source Ranges in EnvoyProxy CRD +- Added Support for Proxy Prometheus Metrics Compression in EnvoyProxy CRD +- Added Support for BackendRefs in Access Log, Metric and Trace Sinks in EnvoyProxy CRD +- Added Support for Rate Limiting Tracing in EnvoyProxy CRD +- Added Support for Routing to Service IP in EnvoyProxy CRD +- Added Support for Access Log CEL filters in EnvoyProxy CRD +- Added Support for Access Log Formatters for File and OpenTelemetry in EnvoyProxy CRD +- Added Support for Zipkin Tracing in EnvoyProxy CRD +- Added Support for using the Listener port as a the Container port in EnvoyProxy CRD +- Added Support for OpenTelemtry Sink Export Settings in EnvoyProxy CRD +- Added Support for Backend Client Certificate Authentication in EnvoyProxy CRD +- Added Support for Backend TLS Settings in EnvoyProxy CRD +- Added Support for HTTP Filter Ordering in EnvoyProxy CRD +- Added Support for gRPC Access Log Service (ALS) Sink in EnvoyProxy CRD +- Added Support for OpenTelelemetry Sinks as a BackendRef in EnvoyProxy CRD +- Added Support for User-Provided name for generate Kubernetes resources in EnvoyProxy CRD +- Added Support for Per-Endpoint stats in EnvoyProxy CRD +- Added Support for Targeting SectionNames in ClientTrafficPolicy CRD +- Added Support for Preserving X-Request-ID header in ClientTrafficPolicy CRD +- Added Support for Using Downstream Protocol in Upstream connections in ClientTrafficPolicy CRD +- Added Support for HTTP/2 settings in ClientTrafficPolicy CRD +- Added Support for Connection Buffer Size Limit in ClientTrafficPolicy CRD +- Added Support for HTTP Health Check in ClientTrafficPolicy CRD +- Added Support for Optionally requiring a Client Certificate in ClientTrafficPolicy CRD +- Added Support for Headers with Underscores CRD in ClientTrafficPolicy CRD +- Added Support for XFCC header processing in ClientTrafficPolicy CRD +- Added Support for TCP Listener Idle Timeout in ClientTrafficPolicy CRD +- Added Support for IdleTimeout in ClientTrafficPolicy CRD +- Added Support for Connection Limits in ClientTrafficPolicy CRD +- Added Support for additional OIDC settings related to Resource, Token and Cookie in SecurityPolicy CRD +- Added Support for Optionally requiring a JWT in SecurityPolicy CRD +- Added Support for BackendRefs for Ext-Auth in SecurityPolicy CRD +- Added Support for Authorization in SecurityPolicy CRD +- Added Support for Ext-Auth failOpen in SecurityPolicy CRD +- Added Support for Loadbalancer Cookie Consistent Hashing in BackendTrafficPolicy CRD +- Added Support for Disabling X-RateLimit headers in BackendTrafficPolicy CRD +- Added Support for Connection Buffer Size Limit in BackendTrafficPolicy CRD +- Added Support for Loadbalancing Consistent Hash Table Size in BackendTrafficPolicy CRD +- Added Support for Loadbalancing Header Hash Policy in BackendTrafficPolicy CRD +- Added Support for Cluster Connection Buffer Size Limit in BackendTrafficPolicy +- Added Support for more Rate Limit Rules in BackendTrafficPolicy CRD +- Added Support for Wasm extension in EnvoyExtensionPolicy CRD +- Added Support for External Processing extension in EnvoyExtensionPolicy CRD +- Removed Status Print Column from xPolicy CRDs + +### Breaking Changes +- Gateway-API BackendTLSPolicy v1alpha3 is incompatible with previous versions of the CRD +- xPolicy targetRefs can no longer specify a namespace, since Gateway-API v1.1.0 uses LocalPolicyTargetReferenceWithSectionName in Policy resources + +### Deprecations +- xPolicy targetRef is deprecated, use targetRefs instead +- SecurityPolicy ExtAuth BackendRef is deprecated, use BackendRefs instead +- OpenTelemetry Proxy Access Log Host and Port are deprecated, use backendRefs instead +- OpenTelemetry Proxy Metrics Sink Host and Port are deprecated, use backendRefs instead +- Proxy Tracing Provider Host and Port are deprecated, use backendRefs instead +- Envoy Gateway Extension Server Host and Port are deprecated, use BackendEndpoint instead + +## Conformance +- Added Supported Features to Gateway Class + +## Testing +- Added performance benchmarking test +- Added e2e test for Zipking Tracing +- Added e2e test for HTTP Health Checks +- Added e2e test for CEL Access Log Filter +- Added e2e test for GRPC Access Log Service Sink +- Added e2e test for XDS Metadata +- Added e2e test for Wasm from OCI Images and HTTP Source +- Added e2e test for Service IP Routing +- Added e2e test for Multiple GatewayClasses +- Added e2e test for HTTP Full Path rewrite +- Added e2e test for Backend API +- Added e2e test for Backend TLS Settings +- Added e2e test for disabling X-RateLimit Headers +- Added e2e test for Authorization +- Added e2e test for BackendRefs in Ext-Auth +- Added e2e test for Using Client Protocol in Upstream Connection +- Added e2e test for Backend Client Cert Authentication +- Added e2e test for External Processing Filter +- Added e2e test for Merge Gateways Feature +- Added e2e test for Option JWT authentication +- Added e2e test for Infrastructure using Server-Side Apply +- Added e2e test for Connection Limits +- Added e2e test for Envoy Graceful Shutdown +- Updated e2e test for Limit to cover multiple listeners +- Updated e2e test for CORS to not require access-control-expose-headers +- Run CEL tests on all supported K8s versions +- Added OSV Scanner for Golang Vulnerabilities and Licenses +- Added Trivy scanner for Docker images + +## Translator +- Added Support for BackendRef HTTP Filters +- Added Support for attaching EnvoyProxy to Gateways +- Added Support for cross-namespace EnvoyProxy reference from GatewayClass +- Added Support for Backend Traffic Policy for UDPRoute and TCPRoute +- Added Support for ClientTrafficPolicy for UDPRoute and TCPRoute +- Added Support for multiple BackendRefs in TCPRoute and UDPRoute +- Added Metrics related to XDS Server, Infra Manager and Controller +- Added Support for PolicyStatus in EnvoyPatchPolicy +- Added Support for Websocket upgrades in HTTP/1 Routes +- Added Support for custom controller name in egctl +- Added Support for BackendTLSPolicy CA Certificate reference to Secret +- Added names to Filter Chains +- Added Support extension server hooks for TCP and UDP listeners +- Added Support for attaching EnvoyProxy resource to Gateways +- Added Support for Exposing Prometheus Port in Rate Limiter Service +- Added Support for Optional Rate Limit Backend Redis +- Updated OAuth2 filter to preserve Authorization header if OIDC token forwarding is enabled +- Updated Default Filter Order to have Fault filter first in the HTTP Filter Chain +- Updated Ext-Auth Per-Route config to use filter-specific Config Type +- Updated Overload Manager configuration according to Envoy recommendations by default +- Updated Infrastructure resource management to user Server-Side Apply +- Updated Reflection of Errors in Gateway Status when too many addresses are assigned +- Fixed enforcement of same-namespace for BackendTLSPolicy and target +- Fixed processing all listeners before returning with an error +- Fixed creation of infrastructure resources if there are no listeners +- Fixed use GatewayClass Name for Observability if Merge Gateways is enabled +- Fixed CORS to not forward Not-Matching Preflights to Backends +- Fixed BackendTLSPolicy status to fully conform with PolicyStatus +- Fixed duplication of Ext-Auth, OIDC and Basic Auth Filters +- Fixed Proxy Protocol Filter to always be the first Listener Filter +- Fixed Translation Consistency by sorting Gateways +- Fixed QUIC Listener to only Advertise HTTP/3 over ALPN +- Fixed SNI matching for TCP Routes with TLS termination +- Fixed Reconciliation when EnvoyProxy backendRefs changes +- Fixed Reconciliation when a referenced Secret or ConfigMap changes +- Fixed ReplaceFullPath not working for root path +- Fixed Default Application Protocol to TCP for Zipkin Tracing +- Fixed not appending well-known ports (80, 443) in rediret Location header + +## Providers +- Bumped K8s Client to v0.30.0 + +## xDS +- Bumped go-control-plane to v0.12.1 + +## Cli +- Added Support for Install and Uninstall Commands to egctl +- Added Support for xRoute and xPolicy in egctl x status +- Added Golang version to Envoy Gateway version command +- Fixed egctl x status gatewayclass example message + diff --git a/site/content/en/news/releases/notes/v1.1.0.md b/site/content/en/news/releases/notes/v1.1.0.md new file mode 100644 index 00000000000..a57270cca14 --- /dev/null +++ b/site/content/en/news/releases/notes/v1.1.0.md @@ -0,0 +1,238 @@ +--- +title: "v1.1.0" +publishdate: 2024-07-22 +--- + +Date: July 22, 2024 + +## Documentation +- Added Concepts Doc +- Added User Guide for Wasm Extension +- Added User Guide for patching Envoy Service +- Added User Guide for Backend MTLS +- Added User Guide for Backend TLS Parameters +- Added User Guide for IP Allowlist/Denylist +- Added User Guide for Extension Server +- Added User Guide for building Wasm image +- Added Performance Benchmarking Document +- Added User Guide for Zipkin Tracing +- Added User Guide for Customizing Ordering of Filters +- Added User Guide for External Processing Filter in EnvoyExtensionPolicy +- Added User Guide for installation of egctl with brew +- Added User Guide for Client Buffer Size Limit +- Added User Guide for Client Idle Timeout +- Added Chinese translation for release notes, roadmap, installation, development, contribution and several User Guides +- Added User Guide for Backend resource +- Added GA Blog Post +- Added Threat Model +- Added Adopters section to docs +- Added User Guide and Dashboards for Control Plane and Resource Observability +- Added User Guide for Connection Limits in ClientTrafficPolicy +- Added User Guide on using Private Key Provider +- Added Design Doc for Authorization +- Added Design Doc for XDS Metadata +- Added Design Doc for Backend resource +- Added Design Doc for Control Plane Observability +- Added Design Doc for EnvoyExtensionPolicy +- Added Design Doc for External Processing in EnvoyExtensionPolicy +- Updated Access Logging User Guide to include filtering with CEL Expression +- Updated Access Logging User Guide to include Metadata +- Updated Development Guide to require Golang 1.22 +- Updated Quickstart User Guide to fetch GATEWAY_HOST from Gateway resource +- Updated Site to reflect GA status +- Updated HTTP Redirect User Guide to not set a redirect port or require a BackendRef +- Updated Observability User Guides to use gateway-addons-helm +- Updated Gateway-API User Guide to reflect support for BackendRef filters +- Updated HTTP Timeouts User Guide to highlight default Envoy timeouts +- Updated Installation Guide to use server-side apply +- Updated Installation Guide to refer to values.yaml docs +- Updated BackendTLSPolicy User Guide to GW-API v1.1.0 +- Updated User Guides to use tabs when applying yaml from file or stdin +- Updated OIDC User Guide to use HTTPS redirect URLs +- Updated Order of versions in Site +- Updated Extensbility User Gudie to use yaml-format patches +- Updated Quickstart Guide to include next steps +- Updated CRD docs to include enum values +- Updated Extensibility User Guide with Envoy Patch Policy examples +- Updated structure of docs: rename Guides to Tasks, move Contribution +- Updated Support Matrix +- Updated egctl x status docs for xRoute and xPolicy +- Updated egctl User Guide with Install and Uninstall commands +- Updated GRPCRoute docs to use v1 instead of v1alpha2 +- Fixed Rate Limiting User Guide to use correct CIDR matcher type names +- Fixed User Guide for JWT-based routing +- Fixed JSON Access Log Example +- Use linkinator to detect dead links in docs +- Use helm-docs to generate chart docs +- Support Not-Implemented-Hide marker in API docs + +## Installation +- Added startupProbe to all provisioned containers to reduce risk of restart +- Added new gateway-addons-helm chart for Observability +- Added support for global image settings for all images in Envoy Gateway helm chart +- Added Support for PodDistruptionBudget for Envoy Gateway +- Added Support for TopologySpreadConstraints for Envoy Gateway +- Added Support for Tolerations for Envoy Gateway +- Added Support for Ratelimit image pull secrets and pull policy +- Updated ttlSecondsAfterFinished on certgen job to 30 by default +- Updated Envoy Gateway ImagePullPolicy to IfNotPresent released charts +- Remove envoy-gateway-metrics-service and merge its contents into envoy-gateway service + +## API +- Added Support for Gateway-API v1.1.0 +- Added new Backend CRD +- Added new EnvoyExtensionPolicy CRD +- Added Support for Plural Target Refs and Target Selectors in xPolicy CRDs +- Added Support for Backend CRD BackendRefs in HTTPRoute, GRPCRoute and EnvoyExtensionPolicy CRDs +- Added Support for Custom Extension Server Policy CRDs in EnvoyGateway Config +- Added Support for Custom ShutDownManager Image in EnvoyGateway Config +- Added Support for Leader Election in EnvoyGateway Config +- Added Support for Connecting to Extension Server over Unix Domain Socket in EnvoyGateway Config +- Added Support for Proxy PodDisruptionBudget in EnvoyProxy CRD +- Added Support for Running Envoy Proxy as a Daemonset in EnvoyProxy CRD +- Added Support for Proxy Loadbalancer Source Ranges in EnvoyProxy CRD +- Added Support for Proxy Prometheus Metrics Compression in EnvoyProxy CRD +- Added Support for BackendRefs in Access Log, Metric and Trace Sinks in EnvoyProxy CRD +- Added Support for Rate Limiting Tracing in EnvoyProxy CRD +- Added Support for Routing to Service IP in EnvoyProxy CRD +- Added Support for Access Log CEL filters in EnvoyProxy CRD +- Added Support for Access Log Formatters for File and OpenTelemetry in EnvoyProxy CRD +- Added Support for Zipkin Tracing in EnvoyProxy CRD +- Added Support for using the Listener port as a the Container port in EnvoyProxy CRD +- Added Support for OpenTelemtry Sink Export Settings in EnvoyProxy CRD +- Added Support for Backend Client Certificate Authentication in EnvoyProxy CRD +- Added Support for Backend TLS Settings in EnvoyProxy CRD +- Added Support for HTTP Filter Ordering in EnvoyProxy CRD +- Added Support for gRPC Access Log Service (ALS) Sink in EnvoyProxy CRD +- Added Support for OpenTelelemetry Sinks as a BackendRef in EnvoyProxy CRD +- Added Support for User-Provided name for generate Kubernetes resources in EnvoyProxy CRD +- Added Support for Per-Endpoint stats in EnvoyProxy CRD +- Added Support for Targeting SectionNames in ClientTrafficPolicy CRD +- Added Support for Preserving X-Request-ID header in ClientTrafficPolicy CRD +- Added Support for Using Downstream Protocol in Upstream connections in ClientTrafficPolicy CRD +- Added Support for HTTP/2 settings in ClientTrafficPolicy CRD +- Added Support for Connection Buffer Size Limit in ClientTrafficPolicy CRD +- Added Support for HTTP Health Check in ClientTrafficPolicy CRD +- Added Support for Optionally requiring a Client Certificate in ClientTrafficPolicy CRD +- Added Support for Headers with Underscores CRD in ClientTrafficPolicy CRD +- Added Support for XFCC header processing in ClientTrafficPolicy CRD +- Added Support for TCP Listener Idle Timeout in ClientTrafficPolicy CRD +- Added Support for IdleTimeout in ClientTrafficPolicy CRD +- Added Support for Connection Limits in ClientTrafficPolicy CRD +- Added Support for additional OIDC settings related to Resource, Token and Cookie in SecurityPolicy CRD +- Added Support for Optionally requiring a JWT in SecurityPolicy CRD +- Added Support for BackendRefs for Ext-Auth in SecurityPolicy CRD +- Added Support for Authorization in SecurityPolicy CRD +- Added Support for Ext-Auth failOpen in SecurityPolicy CRD +- Added Support for Loadbalancer Cookie Consistent Hashing in BackendTrafficPolicy CRD +- Added Support for Disabling X-RateLimit headers in BackendTrafficPolicy CRD +- Added Support for Connection Buffer Size Limit in BackendTrafficPolicy CRD +- Added Support for Loadbalancing Consistent Hash Table Size in BackendTrafficPolicy CRD +- Added Support for Loadbalancing Header Hash Policy in BackendTrafficPolicy CRD +- Added Support for Cluster Connection Buffer Size Limit in BackendTrafficPolicy +- Added Support for more Rate Limit Rules in BackendTrafficPolicy CRD +- Added Support for Wasm extension in EnvoyExtensionPolicy CRD +- Added Support for External Processing extension in EnvoyExtensionPolicy CRD +- Removed Status Print Column from xPolicy CRDs + +### Breaking Changes +- SecurityPolicy translation failures will now cause routes referenced by the policy to return an immediate 500 response +- Gateway-API BackendTLSPolicy v1alpha3 is incompatible with previous versions of the CRD +- xPolicy targetRefs can no longer specify a namespace, since Gateway-API v1.1.0 uses LocalPolicyTargetReferenceWithSectionName in Policy resources + +### Deprecations +- xPolicy targetRef is deprecated, use targetRefs instead +- SecurityPolicy ExtAuth BackendRef is deprecated, use BackendRefs instead +- OpenTelemetry Proxy Access Log Host and Port are deprecated, use backendRefs instead +- OpenTelemetry Proxy Metrics Sink Host and Port are deprecated, use backendRefs instead +- Proxy Tracing Provider Host and Port are deprecated, use backendRefs instead +- Envoy Gateway Extension Server Host and Port are deprecated, use BackendEndpoint instead + +## Conformance +- Added Supported Features to Gateway Class + +## Testing +- Added e2e test for Client MTLS +- Added e2e test for Load Balancing +- Added performance benchmarking test +- Added e2e test for Zipking Tracing +- Added e2e test for HTTP Health Checks +- Added e2e test for CEL Access Log Filter +- Added e2e test for GRPC Access Log Service Sink +- Added e2e test for XDS Metadata +- Added e2e test for Wasm from OCI Images and HTTP Source +- Added e2e test for Service IP Routing +- Added e2e test for Multiple GatewayClasses +- Added e2e test for HTTP Full Path rewrite +- Added e2e test for Backend API +- Added e2e test for Backend TLS Settings +- Added e2e test for disabling X-RateLimit Headers +- Added e2e test for Authorization +- Added e2e test for BackendRefs in Ext-Auth +- Added e2e test for Using Client Protocol in Upstream Connection +- Added e2e test for Backend Client Cert Authentication +- Added e2e test for External Processing Filter +- Added e2e test for Merge Gateways Feature +- Added e2e test for Option JWT authentication +- Added e2e test for Infrastructure using Server-Side Apply +- Added e2e test for Connection Limits +- Added e2e test for Envoy Graceful Shutdown +- Updated e2e test for Limit to cover multiple listeners +- Updated e2e test for CORS to not require access-control-expose-headers +- Run CEL tests on all supported K8s versions +- Added OSV Scanner for Golang Vulnerabilities and Licenses +- Added Trivy scanner for Docker images + +## Translator +- Added Support for BackendRef HTTP Filters +- Added Support for attaching EnvoyProxy to Gateways +- Added Support for cross-namespace EnvoyProxy reference from GatewayClass +- Added Support for Backend Traffic Policy for UDPRoute and TCPRoute +- Added Support for ClientTrafficPolicy for UDPRoute and TCPRoute +- Added Support for multiple BackendRefs in TCPRoute and UDPRoute +- Added Metrics related to XDS Server, Infra Manager and Controller +- Added Support for PolicyStatus in EnvoyPatchPolicy +- Added Support for Websocket upgrades in HTTP/1 Routes +- Added Support for custom controller name in egctl +- Added Support for BackendTLSPolicy CA Certificate reference to Secret +- Added names to Filter Chains +- Added Support extension server hooks for TCP and UDP listeners +- Added Support for attaching EnvoyProxy resource to Gateways +- Added Support for Exposing Prometheus Port in Rate Limiter Service +- Added Support for Optional Rate Limit Backend Redis +- Updated OAuth2 filter to preserve Authorization header if OIDC token forwarding is enabled +- Updated Default Filter Order to have Fault filter first in the HTTP Filter Chain +- Updated Ext-Auth Per-Route config to use filter-specific Config Type +- Updated Overload Manager configuration according to Envoy recommendations by default +- Updated Infrastructure resource management to user Server-Side Apply +- Updated Reflection of Errors in Gateway Status when too many addresses are assigned +- Fixed enforcement of same-namespace for BackendTLSPolicy and target +- Fixed processing all listeners before returning with an error +- Fixed creation of infrastructure resources if there are no listeners +- Fixed use GatewayClass Name for Observability if Merge Gateways is enabled +- Fixed CORS to not forward Not-Matching Preflights to Backends +- Fixed BackendTLSPolicy status to fully conform with PolicyStatus +- Fixed duplication of Ext-Auth, OIDC and Basic Auth Filters +- Fixed Proxy Protocol Filter to always be the first Listener Filter +- Fixed Translation Consistency by sorting Gateways +- Fixed QUIC Listener to only Advertise HTTP/3 over ALPN +- Fixed SNI matching for TCP Routes with TLS termination +- Fixed Reconciliation when EnvoyProxy backendRefs changes +- Fixed Reconciliation when a referenced Secret or ConfigMap changes +- Fixed ReplaceFullPath not working for root path +- Fixed Default Application Protocol to TCP for Zipkin Tracing +- Fixed not appending well-known ports (80, 443) in rediret Location header + +## Providers +- Bumped K8s Client to v0.30.0 + +## xDS +- Bumped go-control-plane to v0.12.1 + +## Cli +- Added egctl x collect command +- Added Support for Install and Uninstall commands to egctl +- Added Support for xRoute and xPolicy in egctl x status +- Added Golang version to Envoy Gateway version command +- Fixed egctl x status gatewayclass example message + diff --git a/site/content/en/news/releases/v0.2.md b/site/content/en/news/releases/v0.2.md index ad2b0a2cbd6..10cd1216ed8 100644 --- a/site/content/en/news/releases/v0.2.md +++ b/site/content/en/news/releases/v0.2.md @@ -36,11 +36,11 @@ the [documentation][docs] for additional details on how to use Envoy Gateway for Envoy Gateway will be at [EnvoyCon NA][] this October in Detroit. Don't miss [our talk][] to learn more about the release and future direction of the project. -[Release Notes]: https://github.com/envoyproxy/gateway/blob/main/release-notes/v0.2.0.yaml +[Release Notes]: ./notes/v0.2.0 [matrix]: ./matrix [docs]: https://gateway.envoyproxy.io/index.html [Download]: https://github.com/envoyproxy/gateway/releases/tag/v0.2.0 [conformance tests]: https://gateway-api.sigs.k8s.io/concepts/conformance/?h=conformance -[quickstart guide]: ../v0.2.0/user/quickstart +[quickstart guide]: ../../v0.2/user/quickstart [EnvoyCon NA]: https://events.linuxfoundation.org/envoycon-north-america/program/schedule/ [our talk]: https://sched.co/1AO5S diff --git a/site/content/en/news/releases/v0.3.md b/site/content/en/news/releases/v0.3.md index 15cbc8d7c98..90f164025a8 100644 --- a/site/content/en/news/releases/v0.3.md +++ b/site/content/en/news/releases/v0.3.md @@ -40,7 +40,7 @@ The release adds a ton of features and functionality. Here are some highlights: + Added Support for Request Authentication -[Release Notes]: https://github.com/envoyproxy/gateway/blob/main/release-notes/v0.3.0.yaml +[Release Notes]: ./notes/v0.3.0 [matrix]: ./matrix -[docs]: https://gateway.envoyproxy.io/v0.3.0/index.html +[docs]: /v0.3 [Download]: https://github.com/envoyproxy/gateway/releases/tag/v0.3.0 diff --git a/site/content/en/news/releases/v0.4.md b/site/content/en/news/releases/v0.4.md index 1f980bd0879..1df25b4b405 100644 --- a/site/content/en/news/releases/v0.4.md +++ b/site/content/en/news/releases/v0.4.md @@ -51,7 +51,7 @@ The release adds a ton of features and functionality. Here are some highlights: + Added Support for Service Method Match in GRPCRoute + Added EDS Support -[Release Notes]: https://github.com/envoyproxy/gateway/blob/main/release-notes/v0.4.0.yaml +[Release Notes]: ./notes/v0.4.0 [matrix]: ./matrix -[docs]: https://gateway.envoyproxy.io/v0.4.0/index.html +[docs]: /v0.4 [Download]: https://github.com/envoyproxy/gateway/releases/tag/v0.4.0 diff --git a/site/content/en/news/releases/v0.5.md b/site/content/en/news/releases/v0.5.md index 6baa6eca7e4..860b040985b 100644 --- a/site/content/en/news/releases/v0.5.md +++ b/site/content/en/news/releases/v0.5.md @@ -51,7 +51,7 @@ The release adds a ton of features and functionality. Here are some highlights: + Added Best Practices Default Edge Settings to Xds Resources -[Release Notes]: https://github.com/envoyproxy/gateway/blob/main/release-notes/v0.5.0.yaml +[Release Notes]: ./notes/v0.5.0 [matrix]: ./matrix -[docs]: https://gateway.envoyproxy.io/v0.5.0/index.html +[docs]: /v0.5 [Download]: https://github.com/envoyproxy/gateway/releases/tag/v0.5.0 diff --git a/site/content/en/news/releases/v0.6.md b/site/content/en/news/releases/v0.6.md index 94f7f2e8324..bfb6c57d405 100644 --- a/site/content/en/news/releases/v0.6.md +++ b/site/content/en/news/releases/v0.6.md @@ -76,7 +76,7 @@ The release adds a ton of features and functionality. Here are some highlights: + Moved the EnvoyProxy CRD from `config.gateway.envoyproxy.io` to `gateway.envoyproxy.io` + Converted the `bootstrap` field within `EnvoyProxy` into a struct to support merge operations. -[Release Notes]: https://github.com/envoyproxy/gateway/blob/main/release-notes/v0.6.0.yaml +[Release Notes]: ./notes/v0.6.0 [matrix]: ./matrix -[docs]: https://gateway.envoyproxy.io/v0.6.0/index.html +[docs]: https://gateway.envoyproxy.io/v0.6 [Download]: https://github.com/envoyproxy/gateway/releases/tag/v0.6.0 diff --git a/site/content/en/news/releases/v1.0.md b/site/content/en/news/releases/v1.0.md index 0316593dc92..1f733626f4f 100644 --- a/site/content/en/news/releases/v1.0.md +++ b/site/content/en/news/releases/v1.0.md @@ -194,7 +194,7 @@ The release adds a ton of features and functionality. Here are some highlights: - Added Support for egctl config ratelimit - Added Support for egctl translate from gateway-api resources to IR -[Release Notes]: https://github.com/envoyproxy/gateway/blob/main/release-notes/v1.0.0.yaml +[Release Notes]: ./notes/v1.0.0 [matrix]: ./matrix -[docs]: /v1.0.2/ +[docs]: /v1.0/ [Download]: https://github.com/envoyproxy/gateway/releases/tag/v1.0.0 diff --git a/site/content/en/news/releases/v1.1.md b/site/content/en/news/releases/v1.1.md new file mode 100644 index 00000000000..698c7acab2e --- /dev/null +++ b/site/content/en/news/releases/v1.1.md @@ -0,0 +1,270 @@ +--- +title: Announcing Envoy Gateway v1.1 +subtitle: Major Update +linktitle: Release v1.1 +description: Envoy Gateway v1.1 release announcement. +publishdate: 2024-07-22 +release: v1.1.0 +skip_list: true +--- + +We are thrilled to announce the arrival of Envoy Gateway v1.1.0. + +This release represents a significant achievement, and we extend our heartfelt gratitude to the entire Envoy Gateway community for their contributions, dedication, and support. Your collaborative efforts have been instrumental in reaching this pivotal release. + +Thank you for being an integral part of this journey. We are excited to see how Envoy Gateway v1.1.0 will empower your operations and look forward to continuing our work together to drive the future of Cloud Native API Gateway. + +| [Release Notes][] | [Docs][docs] | [Compatibility Matrix][matrix] | [Download][] | +|-------------------|--------------|--------------------------------|--------------| + +## What's New + +The release adds a ton of features and functionality. Here are some highlights: + +### Documentation + +- Added Concepts Doc +- Added User Guide for Wasm Extension +- Added User Guide for patching Envoy Service +- Added User Guide for Backend MTLS +- Added User Guide for Backend TLS Parameters +- Added User Guide for IP Allowlist/Denylist +- Added User Guide for Extension Server +- Added User Guide for building Wasm image +- Added Performance Benchmarking Document +- Added User Guide for Zipkin Tracing +- Added User Guide for Customizing Ordering of Filters +- Added User Guide for External Processing Filter in EnvoyExtensionPolicy +- Added User Guide for installation of egctl with brew +- Added User Guide for Client Buffer Size Limit +- Added User Guide for Client Idle Timeout +- Added Chinese translation for release notes, roadmap, installation, development, contribution and several User Guides +- Added User Guide for Backend resource +- Added GA Blog Post +- Added Threat Model +- Added Adopters section to docs +- Added User Guide and Dashboards for Control Plane and Resource Observability +- Added User Guide for Connection Limits in ClientTrafficPolicy +- Added User Guide on using Private Key Provider +- Added Design Doc for Authorization +- Added Design Doc for XDS Metadata +- Added Design Doc for Backend resource +- Added Design Doc for Control Plane Observability +- Added Design Doc for EnvoyExtensionPolicy +- Added Design Doc for External Processing in EnvoyExtensionPolicy +- Updated Access Logging User Guide to include filtering with CEL Expression +- Updated Access Logging User Guide to include Metadata +- Updated Development Guide to require Golang 1.22 +- Updated Quickstart User Guide to fetch GATEWAY_HOST from Gateway resource +- Updated Site to reflect GA status +- Updated HTTP Redirect User Guide to not set a redirect port or require a BackendRef +- Updated Observability User Guides to use gateway-addons-helm +- Updated Gateway-API User Guide to reflect support for BackendRef filters +- Updated HTTP Timeouts User Guide to highlight default Envoy timeouts +- Updated Installation Guide to use server-side apply +- Updated Installation Guide to refer to values.yaml docs +- Updated BackendTLSPolicy User Guide to GW-API v1.1.0 +- Updated User Guides to use tabs when applying yaml from file or stdin +- Updated OIDC User Guide to use HTTPS redirect URLs +- Updated Order of versions in Site +- Updated Extensbility User Gudie to use yaml-format patches +- Updated Quickstart Guide to include next steps +- Updated CRD docs to include enum values +- Updated Extensibility User Guide with Envoy Patch Policy examples +- Updated structure of docs: rename Guides to Tasks, move Contribution +- Updated Support Matrix +- Updated egctl x status docs for xRoute and xPolicy +- Updated egctl User Guide with Install and Uninstall commands +- Updated GRPCRoute docs to use v1 instead of v1alpha2 +- Fixed Rate Limiting User Guide to use correct CIDR matcher type names +- Fixed User Guide for JWT-based routing +- Fixed JSON Access Log Example +- Use linkinator to detect dead links in docs +- Use helm-docs to generate chart docs +- Support Not-Implemented-Hide marker in API docs + +### Installation + +- Added startupProbe to all provisioned containers to reduce risk of restart +- Added new gateway-addons-helm chart for Observability +- Added support for global image settings for all images in Envoy Gateway helm chart +- Added Support for PodDistruptionBudget for Envoy Gateway +- Added Support for TopologySpreadConstraints for Envoy Gateway +- Added Support for Tolerations for Envoy Gateway +- Added Support for Ratelimit image pull secrets and pull policy +- Updated ttlSecondsAfterFinished on certgen job to 30 by default +- Updated Envoy Gateway ImagePullPolicy to IfNotPresent released charts +- Remove envoy-gateway-metrics-service and merge its contents into envoy-gateway service + +### API + +- Added Support for Gateway-API v1.1.0 +- Added new Backend CRD +- Added new EnvoyExtensionPolicy CRD +- Added Support for Plural Target Refs and Target Selectors in xPolicy CRDs +- Added Support for Backend CRD BackendRefs in HTTPRoute, GRPCRoute and EnvoyExtensionPolicy CRDs +- Added Support for Custom Extension Server Policy CRDs in EnvoyGateway Config +- Added Support for Custom ShutDownManager Image in EnvoyGateway Config +- Added Support for Leader Election in EnvoyGateway Config +- Added Support for Connecting to Extension Server over Unix Domain Socket in EnvoyGateway Config +- Added Support for Proxy PodDisruptionBudget in EnvoyProxy CRD +- Added Support for Running Envoy Proxy as a Daemonset in EnvoyProxy CRD +- Added Support for Proxy Loadbalancer Source Ranges in EnvoyProxy CRD +- Added Support for Proxy Prometheus Metrics Compression in EnvoyProxy CRD +- Added Support for BackendRefs in Access Log, Metric and Trace Sinks in EnvoyProxy CRD +- Added Support for Rate Limiting Tracing in EnvoyProxy CRD +- Added Support for Routing to Service IP in EnvoyProxy CRD +- Added Support for Access Log CEL filters in EnvoyProxy CRD +- Added Support for Access Log Formatters for File and OpenTelemetry in EnvoyProxy CRD +- Added Support for Zipkin Tracing in EnvoyProxy CRD +- Added Support for using the Listener port as a the Container port in EnvoyProxy CRD +- Added Support for OpenTelemtry Sink Export Settings in EnvoyProxy CRD +- Added Support for Backend Client Certificate Authentication in EnvoyProxy CRD +- Added Support for Backend TLS Settings in EnvoyProxy CRD +- Added Support for HTTP Filter Ordering in EnvoyProxy CRD +- Added Support for gRPC Access Log Service (ALS) Sink in EnvoyProxy CRD +- Added Support for OpenTelelemetry Sinks as a BackendRef in EnvoyProxy CRD +- Added Support for User-Provided name for generate Kubernetes resources in EnvoyProxy CRD +- Added Support for Per-Endpoint stats in EnvoyProxy CRD +- Added Support for Targeting SectionNames in ClientTrafficPolicy CRD +- Added Support for Preserving X-Request-ID header in ClientTrafficPolicy CRD +- Added Support for Using Downstream Protocol in Upstream connections in ClientTrafficPolicy CRD +- Added Support for HTTP/2 settings in ClientTrafficPolicy CRD +- Added Support for Connection Buffer Size Limit in ClientTrafficPolicy CRD +- Added Support for HTTP Health Check in ClientTrafficPolicy CRD +- Added Support for Optionally requiring a Client Certificate in ClientTrafficPolicy CRD +- Added Support for Headers with Underscores CRD in ClientTrafficPolicy CRD +- Added Support for XFCC header processing in ClientTrafficPolicy CRD +- Added Support for TCP Listener Idle Timeout in ClientTrafficPolicy CRD +- Added Support for IdleTimeout in ClientTrafficPolicy CRD +- Added Support for Connection Limits in ClientTrafficPolicy CRD +- Added Support for additional OIDC settings related to Resource, Token and Cookie in SecurityPolicy CRD +- Added Support for Optionally requiring a JWT in SecurityPolicy CRD +- Added Support for BackendRefs for Ext-Auth in SecurityPolicy CRD +- Added Support for Authorization in SecurityPolicy CRD +- Added Support for Ext-Auth failOpen in SecurityPolicy CRD +- Added Support for Loadbalancer Cookie Consistent Hashing in BackendTrafficPolicy CRD +- Added Support for Disabling X-RateLimit headers in BackendTrafficPolicy CRD +- Added Support for Connection Buffer Size Limit in BackendTrafficPolicy CRD +- Added Support for Loadbalancing Consistent Hash Table Size in BackendTrafficPolicy CRD +- Added Support for Loadbalancing Header Hash Policy in BackendTrafficPolicy CRD +- Added Support for Cluster Connection Buffer Size Limit in BackendTrafficPolicy +- Added Support for more Rate Limit Rules in BackendTrafficPolicy CRD +- Added Support for Wasm extension in EnvoyExtensionPolicy CRD +- Added Support for External Processing extension in EnvoyExtensionPolicy CRD +- Removed Status Print Column from xPolicy CRDs + +### Breaking Changes + +- SecurityPolicy translation failures will now cause routes referenced by the policy to return an immediate 500 response +- Gateway-API BackendTLSPolicy v1alpha3 is incompatible with previous versions of the CRD +- xPolicy targetRefs can no longer specify a namespace, since Gateway-API v1.1.0 uses LocalPolicyTargetReferenceWithSectionName in Policy resources + +### Deprecations + +- xPolicy targetRef is deprecated, use targetRefs instead +- SecurityPolicy ExtAuth BackendRef is deprecated, use BackendRefs instead +- OpenTelemetry Proxy Access Log Host and Port are deprecated, use backendRefs instead +- OpenTelemetry Proxy Metrics Sink Host and Port are deprecated, use backendRefs instead +- Proxy Tracing Provider Host and Port are deprecated, use backendRefs instead +- Envoy Gateway Extension Server Host and Port are deprecated, use BackendEndpoint instead + +### Conformance + +- Added Supported Features to Gateway Class + +### Testing + +- Added e2e test for Client MTLS +- Added e2e test for Load Balancing +- Added performance benchmarking test +- Added e2e test for Zipking Tracing +- Added e2e test for HTTP Health Checks +- Added e2e test for CEL Access Log Filter +- Added e2e test for GRPC Access Log Service Sink +- Added e2e test for XDS Metadata +- Added e2e test for Wasm from OCI Images and HTTP Source +- Added e2e test for Service IP Routing +- Added e2e test for Multiple GatewayClasses +- Added e2e test for HTTP Full Path rewrite +- Added e2e test for Backend API +- Added e2e test for Backend TLS Settings +- Added e2e test for disabling X-RateLimit Headers +- Added e2e test for Authorization +- Added e2e test for BackendRefs in Ext-Auth +- Added e2e test for Using Client Protocol in Upstream Connection +- Added e2e test for Backend Client Cert Authentication +- Added e2e test for External Processing Filter +- Added e2e test for Merge Gateways Feature +- Added e2e test for Option JWT authentication +- Added e2e test for Infrastructure using Server-Side Apply +- Added e2e test for Connection Limits +- Added e2e test for Envoy Graceful Shutdown +- Updated e2e test for Limit to cover multiple listeners +- Updated e2e test for CORS to not require access-control-expose-headers +- Run CEL tests on all supported K8s versions +- Added OSV Scanner for Golang Vulnerabilities and Licenses +- Added Trivy scanner for Docker images + +### Translator + +- Added Support for BackendRef HTTP Filters +- Added Support for attaching EnvoyProxy to Gateways +- Added Support for cross-namespace EnvoyProxy reference from GatewayClass +- Added Support for Backend Traffic Policy for UDPRoute and TCPRoute +- Added Support for ClientTrafficPolicy for UDPRoute and TCPRoute +- Added Support for multiple BackendRefs in TCPRoute and UDPRoute +- Added Metrics related to XDS Server, Infra Manager and Controller +- Added Support for PolicyStatus in EnvoyPatchPolicy +- Added Support for Websocket upgrades in HTTP/1 Routes +- Added Support for custom controller name in egctl +- Added Support for BackendTLSPolicy CA Certificate reference to Secret +- Added names to Filter Chains +- Added Support extension server hooks for TCP and UDP listeners +- Added Support for attaching EnvoyProxy resource to Gateways +- Added Support for Exposing Prometheus Port in Rate Limiter Service +- Added Support for Optional Rate Limit Backend Redis +- Updated OAuth2 filter to preserve Authorization header if OIDC token forwarding is enabled +- Updated Default Filter Order to have Fault filter first in the HTTP Filter Chain +- Updated Ext-Auth Per-Route config to use filter-specific Config Type +- Updated Overload Manager configuration according to Envoy recommendations by default +- Updated Infrastructure resource management to user Server-Side Apply +- Updated Reflection of Errors in Gateway Status when too many addresses are assigned +- Fixed enforcement of same-namespace for BackendTLSPolicy and target +- Fixed processing all listeners before returning with an error +- Fixed creation of infrastructure resources if there are no listeners +- Fixed use GatewayClass Name for Observability if Merge Gateways is enabled +- Fixed CORS to not forward Not-Matching Preflights to Backends +- Fixed BackendTLSPolicy status to fully conform with PolicyStatus +- Fixed duplication of Ext-Auth, OIDC and Basic Auth Filters +- Fixed Proxy Protocol Filter to always be the first Listener Filter +- Fixed Translation Consistency by sorting Gateways +- Fixed QUIC Listener to only Advertise HTTP/3 over ALPN +- Fixed SNI matching for TCP Routes with TLS termination +- Fixed Reconciliation when EnvoyProxy backendRefs changes +- Fixed Reconciliation when a referenced Secret or ConfigMap changes +- Fixed ReplaceFullPath not working for root path +- Fixed Default Application Protocol to TCP for Zipkin Tracing +- Fixed not appending well-known ports (80, 443) in rediret Location header + +### Providers + +- Bumped K8s Client to v0.30.0 + +### XDS + +- Bumped go-control-plane to v0.12.1 + +### CLI + +- Added egctl x collect command +- Added Support for Install and Uninstall commands to egctl +- Added Support for xRoute and xPolicy in egctl x status +- Added Golang version to Envoy Gateway version command +- Fixed egctl x status gatewayclass example message + + +[Release Notes]: ./notes/v1.1.0 +[matrix]: ./matrix +[docs]: /v1.1/ +[Download]: https://github.com/envoyproxy/gateway/releases/tag/v1.1.0 diff --git a/site/content/en/v0.2.0/_index.md b/site/content/en/v0.2/_index.md similarity index 100% rename from site/content/en/v0.2.0/_index.md rename to site/content/en/v0.2/_index.md diff --git a/site/content/en/v0.2.0/contributions/CODEOWNERS.md b/site/content/en/v0.2/contributions/CODEOWNERS.md similarity index 100% rename from site/content/en/v0.2.0/contributions/CODEOWNERS.md rename to site/content/en/v0.2/contributions/CODEOWNERS.md diff --git a/site/content/en/v0.2.0/contributions/CODE_OF_CONDUCT.md b/site/content/en/v0.2/contributions/CODE_OF_CONDUCT.md similarity index 100% rename from site/content/en/v0.2.0/contributions/CODE_OF_CONDUCT.md rename to site/content/en/v0.2/contributions/CODE_OF_CONDUCT.md diff --git a/site/content/en/v0.2.0/contributions/CONTRIBUTING.md b/site/content/en/v0.2/contributions/CONTRIBUTING.md similarity index 97% rename from site/content/en/v0.2.0/contributions/CONTRIBUTING.md rename to site/content/en/v0.2/contributions/CONTRIBUTING.md index f94b2c940e9..b37898e948e 100644 --- a/site/content/en/v0.2.0/contributions/CONTRIBUTING.md +++ b/site/content/en/v0.2/contributions/CONTRIBUTING.md @@ -49,7 +49,7 @@ to the following guidelines for all code, APIs, and documentation: build. If your PR cannot have 100% coverage for some reason please clearly explain why when you open it. * Any PR that changes user-facing behavior **must** have associated documentation in the [docs](https://github.com/envoyproxy/gateway/tree/main/site) folder of the repo as - well as the [changelog](/blog/releases). + well as the [changelog](/news/releases). * All code comments and documentation are expected to have proper English grammar and punctuation. If you are not a fluent English speaker (or a bad writer ;-)) please let us know and we will try to find some help but there are no guarantees. @@ -175,7 +175,7 @@ git config --add alias.c "commit -s" ## Fixing DCO If your PR fails the DCO check, it's necessary to fix the entire commit history in the PR. Best -practice is to [squash](https://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html) +practice is to [squash](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges#squash-and-merge-your-commits) the commit history to a single commit, append the DCO sign-off as described above, and [force push](https://git-scm.com/docs/git-push#git-push---force). For example, if you have 2 commits in your history: diff --git a/site/content/en/v0.2/contributions/DEVELOP.md b/site/content/en/v0.2/contributions/DEVELOP.md new file mode 100644 index 00000000000..13c61295f02 --- /dev/null +++ b/site/content/en/v0.2/contributions/DEVELOP.md @@ -0,0 +1,162 @@ +--- +title: "Developer Guide" +description: "This section tells how to develop Envoy Gateway." +weight: 2 +--- + +Envoy Gateway is built using a [make][]-based build system. Our CI is based on [Github Actions][] using [workflows][]. + +## Prerequisites + +### go + +* Version: 1.20 +* Installation Guide: https://go.dev/doc/install + +### make + +* Recommended Version: 4.0 or later +* Installation Guide: https://www.gnu.org/software/make + +### docker + +* Optional when you want to build a Docker image or run `make` inside Docker. +* Recommended Version: 20.10.16 +* Installation Guide: https://docs.docker.com/engine/install + +### python3 + +* Need a `python3` program +* Must have a functioning `venv` module; this is part of the standard + library, but some distributions (such as Debian and Ubuntu) replace + it with a stub and require you to install a `python3-venv` package + separately. + +## Quickstart + +* Run `make help` to see all the available targets to build, test and run Envoy Gateway. + +### Building + +* Run `make build` to build all the binaries. +* Run `make build BINS="envoy-gateway"` to build the Envoy Gateway binary. +* Run `make build BINS="egctl"` to build the egctl binary. + +__Note:__ The binaries get generated in the `bin/$OS/$ARCH` directory, for example, `bin/linux/amd64/`. + +### Testing + +* Run `make test` to run the golang tests. + +* Run `make testdata` to generate the golden YAML testdata files. + +### Running Linters + +* Run `make lint` to make sure your code passes all the linter checks. +__Note:__ The `golangci-lint` configuration resides [here](https://github.com/envoyproxy/gateway/blob/main/tools/linter/golangci-lint/.golangci.yml). + +### Building and Pushing the Image + +* Run `IMAGE=docker.io/you/gateway-dev make image` to build the docker image. +* Run `IMAGE=docker.io/you/gateway-dev make push-multiarch` to build and push the multi-arch docker image. + +__Note:__ Replace `IMAGE` with your registry's image name. + +### Deploying Envoy Gateway for Test/Dev + +* Run `make create-cluster` to create a [Kind][] cluster. + +#### Option 1: Use the Latest [gateway-dev][] Image + +* Run `TAG=latest make kube-deploy` to deploy Envoy Gateway in the Kind cluster using the latest image. Replace `latest` + to use a different image tag. + +#### Option 2: Use a Custom Image + +* Run `make kube-install-image` to build an image from the tip of your current branch and load it in the Kind cluster. +* Run `IMAGE_PULL_POLICY=IfNotPresent make kube-deploy` to install Envoy Gateway into the Kind cluster using your custom image. + +### Deploying Envoy Gateway in Kubernetes + +* Run `TAG=latest make kube-deploy` to deploy Envoy Gateway using the latest image into a Kubernetes cluster (linked to + the current kube context). Preface the command with `IMAGE` or replace `TAG` to use a different Envoy Gateway image or + tag. +* Run `make kube-undeploy` to uninstall Envoy Gateway from the cluster. + +__Note:__ Envoy Gateway is tested against Kubernetes v1.24.0. + +### Demo Setup + +* Run `make kube-demo` to deploy a demo backend service, gatewayclass, gateway and httproute resource +(similar to steps outlined in the [Quickstart][] docs) and test the configuration. +* Run `make kube-demo-undeploy` to delete the resources created by the `make kube-demo` command. + +### Run Gateway API Conformance Tests + +The commands below deploy Envoy Gateway to a Kubernetes cluster and run the Gateway API conformance tests. Refer to the +Gateway API [conformance homepage][] to learn more about the tests. If Envoy Gateway is already installed, run +`TAG=latest make run-conformance` to run the conformance tests. + +#### On a Linux Host + +* Run `TAG=latest make conformance` to create a Kind cluster, install Envoy Gateway using the latest [gateway-dev][] + image, and run Gateway API conformance tests. + +#### On a Mac Host + +Since Mac doesn't support [directly exposing][] the Docker network to the Mac host, use one of the following +workarounds to run conformance tests: + +* Deploy your own Kubernetes cluster or use Docker Desktop with [Kubernetes support][] and then run + `TAG=latest make kube-deploy run-conformance`. This will install Envoy Gateway using the latest [gateway-dev][] image + to the Kubernetes cluster using the current kubectl context and run the conformance tests. Use `make kube-undeploy` to + uninstall Envoy Gateway. +* Install and run [Docker Mac Net Connect][mac_connect] and then run `TAG=latest make conformance`. + +__Note:__ Preface commands with `IMAGE` or replace `TAG` to use a different Envoy Gateway image or tag. If `TAG` +is unspecified, the short SHA of your current branch is used. + +### Debugging the Envoy Config + +An easy way to view the envoy config that Envoy Gateway is using is to port-forward to the admin interface port +(currently `19000`) on the Envoy deployment that corresponds to a Gateway so that it can be accessed locally. + +Get the name of the Envoy deployment. The following example is for Gateway `eg` in the `default` namespace: + +```shell +export ENVOY_DEPLOYMENT=$(kubectl get deploy -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}') +``` + +Port forward the admin interface port: + +```shell +kubectl port-forward deploy/${ENVOY_DEPLOYMENT} -n envoy-gateway-system 19000:19000 +``` + +Now you are able to view the running Envoy configuration by navigating to `127.0.0.1:19000/config_dump`. + +There are many other endpoints on the [Envoy admin interface][] that may be helpful when debugging. + +### JWT Testing + +An example [JSON Web Token (JWT)][jwt] and [JSON Web Key Set (JWKS)][jwks] are used for the request authentication +user guide. The JWT was created by the [JWT Debugger][], using the `RS256` algorithm. The public key from the JWTs +verify signature was copied to [JWK Creator][] for generating the JWK. The JWK Creator was configured with matching +settings, i.e. `Signing` public key use and the `RS256` algorithm. The generated JWK was wrapped in a JWKS structure +and is hosted in the repo. + +[Quickstart]: https://github.com/envoyproxy/gateway/blob/main/docs/latest/user/quickstart.md +[make]: https://www.gnu.org/software/make/ +[Github Actions]: https://docs.github.com/en/actions +[workflows]: https://github.com/envoyproxy/gateway/tree/main/.github/workflows +[Kind]: https://kind.sigs.k8s.io/ +[conformance homepage]: https://gateway-api.sigs.k8s.io/concepts/conformance/ +[directly exposing]: https://kind.sigs.k8s.io/docs/user/loadbalancer/ +[Kubernetes support]: https://docs.docker.com/desktop/kubernetes/ +[gateway-dev]: https://hub.docker.com/r/envoyproxy/gateway-dev/tags +[mac_connect]: https://github.com/chipmk/docker-mac-net-connect +[Envoy admin interface]: https://www.envoyproxy.io/docs/envoy/latest/operations/admin#operations-admin-interface +[jwt]: https://tools.ietf.org/html/rfc7519 +[jwks]: https://tools.ietf.org/html/rfc7517 +[JWT Debugger]: https://jwt.io/ +[JWK Creator]: https://russelldavies.github.io/jwk-creator/ diff --git a/site/content/en/v0.2.0/contributions/DOCS.md b/site/content/en/v0.2/contributions/DOCS.md similarity index 100% rename from site/content/en/v0.2.0/contributions/DOCS.md rename to site/content/en/v0.2/contributions/DOCS.md diff --git a/site/content/en/v0.3.0/contributions/RELEASING.md b/site/content/en/v0.2/contributions/RELEASING.md similarity index 98% rename from site/content/en/v0.3.0/contributions/RELEASING.md rename to site/content/en/v0.2/contributions/RELEASING.md index eb566306141..bad13a6830c 100644 --- a/site/content/en/v0.3.0/contributions/RELEASING.md +++ b/site/content/en/v0.2/contributions/RELEASING.md @@ -73,7 +73,7 @@ export GITHUB_REMOTE=origin ### Setup cherry picker action -After release branch cut, RM (Release Manager) should add job [cherrypick action](../../../.github/workflows/cherrypick.yaml) for target release. +After release branch cut, RM (Release Manager) should add job [cherrypick action](https://github.com/envoyproxy/gateway/blob/main/.github/workflows/cherrypick.yaml) for target release. Configuration looks like following: diff --git a/site/content/en/v0.2.0/contributions/_index.md b/site/content/en/v0.2/contributions/_index.md similarity index 100% rename from site/content/en/v0.2.0/contributions/_index.md rename to site/content/en/v0.2/contributions/_index.md diff --git a/site/content/en/v0.2.0/contributions/roadmap.md b/site/content/en/v0.2/contributions/roadmap.md similarity index 100% rename from site/content/en/v0.2.0/contributions/roadmap.md rename to site/content/en/v0.2/contributions/roadmap.md diff --git a/site/content/en/v0.2.0/design/_index.md b/site/content/en/v0.2/design/_index.md similarity index 100% rename from site/content/en/v0.2.0/design/_index.md rename to site/content/en/v0.2/design/_index.md diff --git a/site/content/en/v0.3.0/design/config-api.md b/site/content/en/v0.2/design/config-api.md similarity index 98% rename from site/content/en/v0.3.0/design/config-api.md rename to site/content/en/v0.2/design/config-api.md index 466b84d8f35..0ed5253007d 100644 --- a/site/content/en/v0.3.0/design/config-api.md +++ b/site/content/en/v0.2/design/config-api.md @@ -88,7 +88,7 @@ type Gateway struct { // defaults to "gateway.envoyproxy.io/gatewayclass-controller". See the following // for additional details: // - // https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.GatewayClass + // https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.GatewayClass // // +optional ControllerName string `json:"controllerName,omitempty"` @@ -347,6 +347,6 @@ __Note:__ The NetworkPublishing API is currently undefined and is provided here [issue_51]: https://github.com/envoyproxy/gateway/issues/51 [design_doc]: https://github.com/envoyproxy/gateway/blob/main/docs/design/SYSTEM_DESIGN.md [gw_api]: https://gateway-api.sigs.k8s.io/ -[gc]: https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.GatewayClass +[gc]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.GatewayClass [cr]: https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/ [union]: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#unions diff --git a/site/content/en/v0.2.0/design/gatewayapi-translator.md b/site/content/en/v0.2/design/gatewayapi-translator.md similarity index 100% rename from site/content/en/v0.2.0/design/gatewayapi-translator.md rename to site/content/en/v0.2/design/gatewayapi-translator.md diff --git a/site/content/en/v0.2.0/design/goals.md b/site/content/en/v0.2/design/goals.md similarity index 100% rename from site/content/en/v0.2.0/design/goals.md rename to site/content/en/v0.2/design/goals.md diff --git a/site/content/en/v0.3.0/design/system-design.md b/site/content/en/v0.2/design/system-design.md similarity index 96% rename from site/content/en/v0.3.0/design/system-design.md rename to site/content/en/v0.2/design/system-design.md index 72c0a98ecda..a414843a955 100644 --- a/site/content/en/v0.3.0/design/system-design.md +++ b/site/content/en/v0.2/design/system-design.md @@ -159,16 +159,16 @@ The draft for this document is [here][draft_design]. [grl]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_features/global_rate_limiting [rls]: https://github.com/envoyproxy/ratelimit [rlf]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/ratelimit/v3/rate_limit.proto#envoy-v3-api-msg-extensions-filters-http-ratelimit-v3-ratelimit -[crf]: https://gateway-api.sigs.k8s.io/v1alpha2/api-types/httproute/#filters-optional +[crf]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRoute#filters-optional [gwapi_conflicts]: https://gateway-api.sigs.k8s.io/concepts/guidelines/#conflicts [listener]: https://www.envoyproxy.io/docs/envoy/latest/configuration/listeners/listeners#config-listeners [route]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#envoy-v3-api-msg-config-route-v3-route -[be_ref]: https://gateway-api.sigs.k8s.io/v1alpha2/api-types/httproute/#backendrefs-optional +[be_ref]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRoute#backendrefs-optional [cluster]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/cluster.proto#config-cluster-v3-cluster [draft_design]: https://docs.google.com/document/d/1riyTPPYuvNzIhBdrAX8dpfxTmcobWZDSYTTB5NeybuY/edit [cr]: https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/ -[be]: https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.BackendObjectReference +[be]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io%2fv1.BackendObjectReference [svc]: https://kubernetes.io/docs/concepts/services-networking/service/ -[ wcd ]: ./watching.md +[ wcd ]: ./watching [Issue #37]: https://github.com/envoyproxy/gateway/issues/37 -[roadmap]: roadmap.md +[roadmap]: ../contributions/roadmap diff --git a/site/content/en/v0.2.0/design/watching.md b/site/content/en/v0.2/design/watching.md similarity index 100% rename from site/content/en/v0.2.0/design/watching.md rename to site/content/en/v0.2/design/watching.md diff --git a/site/content/en/v0.2.0/user/_index.md b/site/content/en/v0.2/user/_index.md similarity index 100% rename from site/content/en/v0.2.0/user/_index.md rename to site/content/en/v0.2/user/_index.md diff --git a/site/content/en/v0.2.0/user/http-redirect.md b/site/content/en/v0.2/user/http-redirect.md similarity index 100% rename from site/content/en/v0.2.0/user/http-redirect.md rename to site/content/en/v0.2/user/http-redirect.md diff --git a/site/content/en/v0.2.0/user/http-request-headers.md b/site/content/en/v0.2/user/http-request-headers.md similarity index 100% rename from site/content/en/v0.2.0/user/http-request-headers.md rename to site/content/en/v0.2/user/http-request-headers.md diff --git a/site/content/en/v0.2.0/user/http-routing.md b/site/content/en/v0.2/user/http-routing.md similarity index 100% rename from site/content/en/v0.2.0/user/http-routing.md rename to site/content/en/v0.2/user/http-routing.md diff --git a/site/content/en/v0.2.0/user/http-traffic-splitting.md b/site/content/en/v0.2/user/http-traffic-splitting.md similarity index 100% rename from site/content/en/v0.2.0/user/http-traffic-splitting.md rename to site/content/en/v0.2/user/http-traffic-splitting.md diff --git a/site/content/en/v0.2.0/user/quickstart.md b/site/content/en/v0.2/user/quickstart.md similarity index 95% rename from site/content/en/v0.2.0/user/quickstart.md rename to site/content/en/v0.2/user/quickstart.md index 291480b1747..08d77a1d8ea 100644 --- a/site/content/en/v0.2.0/user/quickstart.md +++ b/site/content/en/v0.2/user/quickstart.md @@ -9,7 +9,7 @@ This guide will help you get started with Envoy Gateway in a few simple steps. A Kubernetes cluster. -__Note:__ Refer to the [Compatibility Matrix](/blog/2022/10/01/versions/) for supported Kubernetes versions. +__Note:__ Refer to the [Compatibility Matrix](/news/releases/matrix/) for supported Kubernetes versions. ## Installation diff --git a/site/content/en/v0.2.0/user/secure-gateways.md b/site/content/en/v0.2/user/secure-gateways.md similarity index 100% rename from site/content/en/v0.2.0/user/secure-gateways.md rename to site/content/en/v0.2/user/secure-gateways.md diff --git a/site/content/en/v0.2.0/user/tls-passthrough.md b/site/content/en/v0.2/user/tls-passthrough.md similarity index 100% rename from site/content/en/v0.2.0/user/tls-passthrough.md rename to site/content/en/v0.2/user/tls-passthrough.md diff --git a/site/content/en/v0.3.0/_index.md b/site/content/en/v0.3/_index.md similarity index 100% rename from site/content/en/v0.3.0/_index.md rename to site/content/en/v0.3/_index.md diff --git a/site/content/en/v0.4.0/api/_index.md b/site/content/en/v0.3/api/_index.md similarity index 100% rename from site/content/en/v0.4.0/api/_index.md rename to site/content/en/v0.3/api/_index.md diff --git a/site/content/en/v0.3.0/api/config_types.md b/site/content/en/v0.3/api/config_types.md similarity index 98% rename from site/content/en/v0.3.0/api/config_types.md rename to site/content/en/v0.3/api/config_types.md index 4ff5b3f6f18..76999b6d181 100644 --- a/site/content/en/v0.3.0/api/config_types.md +++ b/site/content/en/v0.3/api/config_types.md @@ -104,7 +104,7 @@ _Appears in:_ | Field | Description | | --- | --- | -| `controllerName` _string_ | ControllerName defines the name of the Gateway API controller. If unspecified, defaults to "gateway.envoyproxy.io/gatewayclass-controller". See the following for additional details: https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.GatewayClass | +| `controllerName` _string_ | ControllerName defines the name of the Gateway API controller. If unspecified, defaults to "gateway.envoyproxy.io/gatewayclass-controller". See the following for additional details: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.GatewayClass | ## KubernetesDeploymentSpec diff --git a/site/content/en/v0.3.0/api/extension_types.md b/site/content/en/v0.3/api/extension_types.md similarity index 100% rename from site/content/en/v0.3.0/api/extension_types.md rename to site/content/en/v0.3/api/extension_types.md diff --git a/site/content/en/v0.3.0/contributions/CODEOWNERS.md b/site/content/en/v0.3/contributions/CODEOWNERS.md similarity index 100% rename from site/content/en/v0.3.0/contributions/CODEOWNERS.md rename to site/content/en/v0.3/contributions/CODEOWNERS.md diff --git a/site/content/en/v0.3.0/contributions/CODE_OF_CONDUCT.md b/site/content/en/v0.3/contributions/CODE_OF_CONDUCT.md similarity index 100% rename from site/content/en/v0.3.0/contributions/CODE_OF_CONDUCT.md rename to site/content/en/v0.3/contributions/CODE_OF_CONDUCT.md diff --git a/site/content/en/v0.5.0/contributions/CONTRIBUTING.md b/site/content/en/v0.3/contributions/CONTRIBUTING.md similarity index 97% rename from site/content/en/v0.5.0/contributions/CONTRIBUTING.md rename to site/content/en/v0.3/contributions/CONTRIBUTING.md index f94b2c940e9..b37898e948e 100644 --- a/site/content/en/v0.5.0/contributions/CONTRIBUTING.md +++ b/site/content/en/v0.3/contributions/CONTRIBUTING.md @@ -49,7 +49,7 @@ to the following guidelines for all code, APIs, and documentation: build. If your PR cannot have 100% coverage for some reason please clearly explain why when you open it. * Any PR that changes user-facing behavior **must** have associated documentation in the [docs](https://github.com/envoyproxy/gateway/tree/main/site) folder of the repo as - well as the [changelog](/blog/releases). + well as the [changelog](/news/releases). * All code comments and documentation are expected to have proper English grammar and punctuation. If you are not a fluent English speaker (or a bad writer ;-)) please let us know and we will try to find some help but there are no guarantees. @@ -175,7 +175,7 @@ git config --add alias.c "commit -s" ## Fixing DCO If your PR fails the DCO check, it's necessary to fix the entire commit history in the PR. Best -practice is to [squash](https://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html) +practice is to [squash](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges#squash-and-merge-your-commits) the commit history to a single commit, append the DCO sign-off as described above, and [force push](https://git-scm.com/docs/git-push#git-push---force). For example, if you have 2 commits in your history: diff --git a/site/content/en/v0.3.0/contributions/DEVELOP.md b/site/content/en/v0.3/contributions/DEVELOP.md similarity index 98% rename from site/content/en/v0.3.0/contributions/DEVELOP.md rename to site/content/en/v0.3/contributions/DEVELOP.md index 6f82c4a411f..67500b42915 100644 --- a/site/content/en/v0.3.0/contributions/DEVELOP.md +++ b/site/content/en/v0.3/contributions/DEVELOP.md @@ -158,6 +158,6 @@ and is hosted in the repo. [Envoy admin interface]: https://www.envoyproxy.io/docs/envoy/latest/operations/admin#operations-admin-interface [jwt]: https://tools.ietf.org/html/rfc7519 [jwks]: https://tools.ietf.org/html/rfc7517 -[request authentication]: https://gateway.envoyproxy.io/latest/user/authn.html +[request authentication]: ../user/authn [JWT Debugger]: https://jwt.io/ [JWK Creator]: https://russelldavies.github.io/jwk-creator/ diff --git a/site/content/en/v0.3.0/contributions/DOCS.md b/site/content/en/v0.3/contributions/DOCS.md similarity index 100% rename from site/content/en/v0.3.0/contributions/DOCS.md rename to site/content/en/v0.3/contributions/DOCS.md diff --git a/site/content/en/v0.4.0/contributions/RELEASING.md b/site/content/en/v0.3/contributions/RELEASING.md similarity index 98% rename from site/content/en/v0.4.0/contributions/RELEASING.md rename to site/content/en/v0.3/contributions/RELEASING.md index eb566306141..bad13a6830c 100644 --- a/site/content/en/v0.4.0/contributions/RELEASING.md +++ b/site/content/en/v0.3/contributions/RELEASING.md @@ -73,7 +73,7 @@ export GITHUB_REMOTE=origin ### Setup cherry picker action -After release branch cut, RM (Release Manager) should add job [cherrypick action](../../../.github/workflows/cherrypick.yaml) for target release. +After release branch cut, RM (Release Manager) should add job [cherrypick action](https://github.com/envoyproxy/gateway/blob/main/.github/workflows/cherrypick.yaml) for target release. Configuration looks like following: diff --git a/site/content/en/v0.3.0/contributions/_index.md b/site/content/en/v0.3/contributions/_index.md similarity index 100% rename from site/content/en/v0.3.0/contributions/_index.md rename to site/content/en/v0.3/contributions/_index.md diff --git a/site/content/en/v0.3.0/contributions/roadmap.md b/site/content/en/v0.3/contributions/roadmap.md similarity index 100% rename from site/content/en/v0.3.0/contributions/roadmap.md rename to site/content/en/v0.3/contributions/roadmap.md diff --git a/site/content/en/v0.3.0/design/_index.md b/site/content/en/v0.3/design/_index.md similarity index 100% rename from site/content/en/v0.3.0/design/_index.md rename to site/content/en/v0.3/design/_index.md diff --git a/site/content/en/v0.2.0/design/config-api.md b/site/content/en/v0.3/design/config-api.md similarity index 98% rename from site/content/en/v0.2.0/design/config-api.md rename to site/content/en/v0.3/design/config-api.md index 466b84d8f35..0ed5253007d 100644 --- a/site/content/en/v0.2.0/design/config-api.md +++ b/site/content/en/v0.3/design/config-api.md @@ -88,7 +88,7 @@ type Gateway struct { // defaults to "gateway.envoyproxy.io/gatewayclass-controller". See the following // for additional details: // - // https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.GatewayClass + // https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.GatewayClass // // +optional ControllerName string `json:"controllerName,omitempty"` @@ -347,6 +347,6 @@ __Note:__ The NetworkPublishing API is currently undefined and is provided here [issue_51]: https://github.com/envoyproxy/gateway/issues/51 [design_doc]: https://github.com/envoyproxy/gateway/blob/main/docs/design/SYSTEM_DESIGN.md [gw_api]: https://gateway-api.sigs.k8s.io/ -[gc]: https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.GatewayClass +[gc]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.GatewayClass [cr]: https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/ [union]: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#unions diff --git a/site/content/en/v0.3.0/design/egctl.md b/site/content/en/v0.3/design/egctl.md similarity index 100% rename from site/content/en/v0.3.0/design/egctl.md rename to site/content/en/v0.3/design/egctl.md diff --git a/site/content/en/v0.3.0/design/gatewayapi-support.md b/site/content/en/v0.3/design/gatewayapi-support.md similarity index 96% rename from site/content/en/v0.3.0/design/gatewayapi-support.md rename to site/content/en/v0.3/design/gatewayapi-support.md index 67eaf05bb4a..d9daaf04198 100644 --- a/site/content/en/v0.3.0/design/gatewayapi-support.md +++ b/site/content/en/v0.3/design/gatewayapi-support.md @@ -96,7 +96,7 @@ these types of cross-namespace references. Envoy Gateway supports the following namespace. - Allowing a Gateway's [SecretObjectReference][] to reference a secret in a different namespace. -[system design]: https://gateway.envoyproxy.io/latest/design/system-design.html +[system design]: ../design/system-design [Gateway API]: https://gateway-api.sigs.k8s.io/ [GatewayClass]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.GatewayClass [parameters reference]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.ParametersReference @@ -113,9 +113,9 @@ these types of cross-namespace references. Envoy Gateway supports the following [TLSRoute]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1alpha2.TLSRoute [ReferenceGrant]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io%2fv1beta1.ReferenceGrant [SecretObjectReference]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.SecretObjectReference -[rate limiting]: https://gateway.envoyproxy.io/latest/user/rate-limit.html -[request authentication]: https://gateway.envoyproxy.io/latest/user/authn.html -[EnvoyProxy]: https://gateway.envoyproxy.io/latest/api/config_types.html#envoyproxy +[rate limiting]: ../user/rate-limit +[request authentication]: ../user/authn +[EnvoyProxy]: ../api/config_types#envoyproxy [resolving conflicts]: https://gateway-api.sigs.k8s.io/concepts/guidelines/?h=conflict#conflicts [ExtensionRefs]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteFilterType [grpc-filter]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1alpha2.GRPCRouteFilter diff --git a/site/content/en/v0.3.0/design/gatewayapi-translator.md b/site/content/en/v0.3/design/gatewayapi-translator.md similarity index 100% rename from site/content/en/v0.3.0/design/gatewayapi-translator.md rename to site/content/en/v0.3/design/gatewayapi-translator.md diff --git a/site/content/en/v0.3.0/design/goals.md b/site/content/en/v0.3/design/goals.md similarity index 100% rename from site/content/en/v0.3.0/design/goals.md rename to site/content/en/v0.3/design/goals.md diff --git a/site/content/en/v0.3.0/design/ratelimit.md b/site/content/en/v0.3/design/ratelimit.md similarity index 100% rename from site/content/en/v0.3.0/design/ratelimit.md rename to site/content/en/v0.3/design/ratelimit.md diff --git a/site/content/en/v0.3.0/design/request-authentication.md b/site/content/en/v0.3/design/request-authentication.md similarity index 100% rename from site/content/en/v0.3.0/design/request-authentication.md rename to site/content/en/v0.3/design/request-authentication.md diff --git a/site/content/en/v0.2.0/design/system-design.md b/site/content/en/v0.3/design/system-design.md similarity index 96% rename from site/content/en/v0.2.0/design/system-design.md rename to site/content/en/v0.3/design/system-design.md index 72c0a98ecda..a683e5307bc 100644 --- a/site/content/en/v0.2.0/design/system-design.md +++ b/site/content/en/v0.3/design/system-design.md @@ -159,16 +159,16 @@ The draft for this document is [here][draft_design]. [grl]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_features/global_rate_limiting [rls]: https://github.com/envoyproxy/ratelimit [rlf]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/ratelimit/v3/rate_limit.proto#envoy-v3-api-msg-extensions-filters-http-ratelimit-v3-ratelimit -[crf]: https://gateway-api.sigs.k8s.io/v1alpha2/api-types/httproute/#filters-optional +[crf]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRoute#filters-optional [gwapi_conflicts]: https://gateway-api.sigs.k8s.io/concepts/guidelines/#conflicts [listener]: https://www.envoyproxy.io/docs/envoy/latest/configuration/listeners/listeners#config-listeners [route]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#envoy-v3-api-msg-config-route-v3-route -[be_ref]: https://gateway-api.sigs.k8s.io/v1alpha2/api-types/httproute/#backendrefs-optional +[be_ref]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRoute#backendrefs-optional [cluster]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/cluster.proto#config-cluster-v3-cluster [draft_design]: https://docs.google.com/document/d/1riyTPPYuvNzIhBdrAX8dpfxTmcobWZDSYTTB5NeybuY/edit [cr]: https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/ -[be]: https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.BackendObjectReference +[be]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io%2fv1.BackendObjectReference [svc]: https://kubernetes.io/docs/concepts/services-networking/service/ [ wcd ]: ./watching.md [Issue #37]: https://github.com/envoyproxy/gateway/issues/37 -[roadmap]: roadmap.md +[roadmap]: ../contributions/roadmap diff --git a/site/content/en/v0.3.0/design/tcp-udp-design.md b/site/content/en/v0.3/design/tcp-udp-design.md similarity index 100% rename from site/content/en/v0.3.0/design/tcp-udp-design.md rename to site/content/en/v0.3/design/tcp-udp-design.md diff --git a/site/content/en/v0.3.0/design/watching.md b/site/content/en/v0.3/design/watching.md similarity index 100% rename from site/content/en/v0.3.0/design/watching.md rename to site/content/en/v0.3/design/watching.md diff --git a/site/content/en/v0.3.0/user/_index.md b/site/content/en/v0.3/user/_index.md similarity index 100% rename from site/content/en/v0.3.0/user/_index.md rename to site/content/en/v0.3/user/_index.md diff --git a/site/content/en/v0.3.0/user/authn.md b/site/content/en/v0.3/user/authn.md similarity index 96% rename from site/content/en/v0.3.0/user/authn.md rename to site/content/en/v0.3/user/authn.md index 312e4103b9b..a4887d57438 100644 --- a/site/content/en/v0.3.0/user/authn.md +++ b/site/content/en/v0.3/user/authn.md @@ -92,5 +92,5 @@ kubectl delete authenticationfilter/jwt-example Checkout the [Developer Guide](../../contributions/develop/) to get involved in the project. [jwt]: https://tools.ietf.org/html/rfc7519 -[AuthenticationFilter]: https://gateway.envoyproxy.io/v0.3.0/api/extension_types.html#authenticationfilter +[AuthenticationFilter]: ../api/extension_types#authenticationfilter [jwks]: https://tools.ietf.org/html/rfc7517 diff --git a/site/content/en/v0.3.0/user/grpc-routing.md b/site/content/en/v0.3/user/grpc-routing.md similarity index 100% rename from site/content/en/v0.3.0/user/grpc-routing.md rename to site/content/en/v0.3/user/grpc-routing.md diff --git a/site/content/en/v0.3.0/user/http-redirect.md b/site/content/en/v0.3/user/http-redirect.md similarity index 100% rename from site/content/en/v0.3.0/user/http-redirect.md rename to site/content/en/v0.3/user/http-redirect.md diff --git a/site/content/en/v0.3.0/user/http-request-headers.md b/site/content/en/v0.3/user/http-request-headers.md similarity index 100% rename from site/content/en/v0.3.0/user/http-request-headers.md rename to site/content/en/v0.3/user/http-request-headers.md diff --git a/site/content/en/v0.3.0/user/http-response-headers.md b/site/content/en/v0.3/user/http-response-headers.md similarity index 100% rename from site/content/en/v0.3.0/user/http-response-headers.md rename to site/content/en/v0.3/user/http-response-headers.md diff --git a/site/content/en/v0.3.0/user/http-routing.md b/site/content/en/v0.3/user/http-routing.md similarity index 100% rename from site/content/en/v0.3.0/user/http-routing.md rename to site/content/en/v0.3/user/http-routing.md diff --git a/site/content/en/v0.3.0/user/http-traffic-splitting.md b/site/content/en/v0.3/user/http-traffic-splitting.md similarity index 100% rename from site/content/en/v0.3.0/user/http-traffic-splitting.md rename to site/content/en/v0.3/user/http-traffic-splitting.md diff --git a/site/content/en/v0.3.0/user/http-urlrewrite.md b/site/content/en/v0.3/user/http-urlrewrite.md similarity index 100% rename from site/content/en/v0.3.0/user/http-urlrewrite.md rename to site/content/en/v0.3/user/http-urlrewrite.md diff --git a/site/content/en/v0.3.0/user/quickstart.md b/site/content/en/v0.3/user/quickstart.md similarity index 96% rename from site/content/en/v0.3.0/user/quickstart.md rename to site/content/en/v0.3/user/quickstart.md index 4875a1ff987..c98ef97e20b 100644 --- a/site/content/en/v0.3.0/user/quickstart.md +++ b/site/content/en/v0.3/user/quickstart.md @@ -9,7 +9,7 @@ This guide will help you get started with Envoy Gateway in a few simple steps. A Kubernetes cluster. -__Note:__ Refer to the [Compatibility Matrix](/blog/2022/10/01/versions/) for supported Kubernetes versions. +__Note:__ Refer to the [Compatibility Matrix](/news/releases/matrix/) for supported Kubernetes versions. ## Installation diff --git a/site/content/en/v0.3.0/user/rate-limit.md b/site/content/en/v0.3/user/rate-limit.md similarity index 98% rename from site/content/en/v0.3.0/user/rate-limit.md rename to site/content/en/v0.3/user/rate-limit.md index 08eae102547..bf0675e546e 100644 --- a/site/content/en/v0.3.0/user/rate-limit.md +++ b/site/content/en/v0.3/user/rate-limit.md @@ -484,8 +484,8 @@ transfer-encoding: chunked [Global rate limiting]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_features/global_rate_limiting -[RateLimitFilter]: https://gateway.envoyproxy.io/v0.3.0/api/extension_types.html#ratelimitfilter +[RateLimitFilter]: ../api/config_types#ratelimitfilter [Envoy Ratelimit]: https://github.com/envoyproxy/ratelimit -[EnvoyGateway]: https://gateway.envoyproxy.io/v0.3.0/api/config_types.html#envoygateway +[EnvoyGateway]: ../api/config_types#envoygateway [HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute/ [ExtensionRef]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io%2fv1beta1.HTTPRouteFilter diff --git a/site/content/en/v0.3.0/user/secure-gateways.md b/site/content/en/v0.3/user/secure-gateways.md similarity index 100% rename from site/content/en/v0.3.0/user/secure-gateways.md rename to site/content/en/v0.3/user/secure-gateways.md diff --git a/site/content/en/v0.3.0/user/tcp-routing.md b/site/content/en/v0.3/user/tcp-routing.md similarity index 100% rename from site/content/en/v0.3.0/user/tcp-routing.md rename to site/content/en/v0.3/user/tcp-routing.md diff --git a/site/content/en/v0.3.0/user/tls-passthrough.md b/site/content/en/v0.3/user/tls-passthrough.md similarity index 100% rename from site/content/en/v0.3.0/user/tls-passthrough.md rename to site/content/en/v0.3/user/tls-passthrough.md diff --git a/site/content/en/v0.3.0/user/udp-routing.md b/site/content/en/v0.3/user/udp-routing.md similarity index 98% rename from site/content/en/v0.3.0/user/udp-routing.md rename to site/content/en/v0.3/user/udp-routing.md index 4652db42120..1425c553092 100644 --- a/site/content/en/v0.3.0/user/udp-routing.md +++ b/site/content/en/v0.3/user/udp-routing.md @@ -153,4 +153,4 @@ kubectl delete udproute/coredns Checkout the [Developer Guide](../../contributions/develop/) to get involved in the project. [UDPRoute]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1alpha2.UDPRoute -[UDP proxy documentation]: https://www.envoyproxy.io/docs/envoy/v0.3.0/configuration/listeners/udp_filters/udp_proxy +[UDP proxy documentation]: https://www.envoyproxy.io/docs/envoy/latest/configuration/listeners/udp_filters/udp_proxy diff --git a/site/content/en/v0.4.0/_index.md b/site/content/en/v0.4/_index.md similarity index 100% rename from site/content/en/v0.4.0/_index.md rename to site/content/en/v0.4/_index.md diff --git a/site/content/en/v0.5.0/api/_index.md b/site/content/en/v0.4/api/_index.md similarity index 100% rename from site/content/en/v0.5.0/api/_index.md rename to site/content/en/v0.4/api/_index.md diff --git a/site/content/en/v0.4.0/api/config_types.md b/site/content/en/v0.4/api/config_types.md similarity index 99% rename from site/content/en/v0.4.0/api/config_types.md rename to site/content/en/v0.4/api/config_types.md index 91f6b5fd532..fd702f4ca4b 100644 --- a/site/content/en/v0.4.0/api/config_types.md +++ b/site/content/en/v0.4/api/config_types.md @@ -227,7 +227,7 @@ _Appears in:_ | Field | Description | | --- | --- | -| `controllerName` _string_ | ControllerName defines the name of the Gateway API controller. If unspecified, defaults to "gateway.envoyproxy.io/gatewayclass-controller". See the following for additional details: https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.GatewayClass | +| `controllerName` _string_ | ControllerName defines the name of the Gateway API controller. If unspecified, defaults to "gateway.envoyproxy.io/gatewayclass-controller". See the following for additional details: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.GatewayClass | ## GroupVersionKind diff --git a/site/content/en/v0.4.0/api/extension_types.md b/site/content/en/v0.4/api/extension_types.md similarity index 100% rename from site/content/en/v0.4.0/api/extension_types.md rename to site/content/en/v0.4/api/extension_types.md diff --git a/site/content/en/v0.4.0/contributions/CODEOWNERS.md b/site/content/en/v0.4/contributions/CODEOWNERS.md similarity index 100% rename from site/content/en/v0.4.0/contributions/CODEOWNERS.md rename to site/content/en/v0.4/contributions/CODEOWNERS.md diff --git a/site/content/en/v0.4.0/contributions/CODE_OF_CONDUCT.md b/site/content/en/v0.4/contributions/CODE_OF_CONDUCT.md similarity index 100% rename from site/content/en/v0.4.0/contributions/CODE_OF_CONDUCT.md rename to site/content/en/v0.4/contributions/CODE_OF_CONDUCT.md diff --git a/site/content/en/v0.4.0/contributions/CONTRIBUTING.md b/site/content/en/v0.4/contributions/CONTRIBUTING.md similarity index 97% rename from site/content/en/v0.4.0/contributions/CONTRIBUTING.md rename to site/content/en/v0.4/contributions/CONTRIBUTING.md index f94b2c940e9..b37898e948e 100644 --- a/site/content/en/v0.4.0/contributions/CONTRIBUTING.md +++ b/site/content/en/v0.4/contributions/CONTRIBUTING.md @@ -49,7 +49,7 @@ to the following guidelines for all code, APIs, and documentation: build. If your PR cannot have 100% coverage for some reason please clearly explain why when you open it. * Any PR that changes user-facing behavior **must** have associated documentation in the [docs](https://github.com/envoyproxy/gateway/tree/main/site) folder of the repo as - well as the [changelog](/blog/releases). + well as the [changelog](/news/releases). * All code comments and documentation are expected to have proper English grammar and punctuation. If you are not a fluent English speaker (or a bad writer ;-)) please let us know and we will try to find some help but there are no guarantees. @@ -175,7 +175,7 @@ git config --add alias.c "commit -s" ## Fixing DCO If your PR fails the DCO check, it's necessary to fix the entire commit history in the PR. Best -practice is to [squash](https://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html) +practice is to [squash](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges#squash-and-merge-your-commits) the commit history to a single commit, append the DCO sign-off as described above, and [force push](https://git-scm.com/docs/git-push#git-push---force). For example, if you have 2 commits in your history: diff --git a/site/content/en/v0.2.0/contributions/DEVELOP.md b/site/content/en/v0.4/contributions/DEVELOP.md similarity index 98% rename from site/content/en/v0.2.0/contributions/DEVELOP.md rename to site/content/en/v0.4/contributions/DEVELOP.md index 6f82c4a411f..67500b42915 100644 --- a/site/content/en/v0.2.0/contributions/DEVELOP.md +++ b/site/content/en/v0.4/contributions/DEVELOP.md @@ -158,6 +158,6 @@ and is hosted in the repo. [Envoy admin interface]: https://www.envoyproxy.io/docs/envoy/latest/operations/admin#operations-admin-interface [jwt]: https://tools.ietf.org/html/rfc7519 [jwks]: https://tools.ietf.org/html/rfc7517 -[request authentication]: https://gateway.envoyproxy.io/latest/user/authn.html +[request authentication]: ../user/authn [JWT Debugger]: https://jwt.io/ [JWK Creator]: https://russelldavies.github.io/jwk-creator/ diff --git a/site/content/en/v0.4.0/contributions/DOCS.md b/site/content/en/v0.4/contributions/DOCS.md similarity index 100% rename from site/content/en/v0.4.0/contributions/DOCS.md rename to site/content/en/v0.4/contributions/DOCS.md diff --git a/site/content/en/v0.2.0/contributions/RELEASING.md b/site/content/en/v0.4/contributions/RELEASING.md similarity index 98% rename from site/content/en/v0.2.0/contributions/RELEASING.md rename to site/content/en/v0.4/contributions/RELEASING.md index eb566306141..bad13a6830c 100644 --- a/site/content/en/v0.2.0/contributions/RELEASING.md +++ b/site/content/en/v0.4/contributions/RELEASING.md @@ -73,7 +73,7 @@ export GITHUB_REMOTE=origin ### Setup cherry picker action -After release branch cut, RM (Release Manager) should add job [cherrypick action](../../../.github/workflows/cherrypick.yaml) for target release. +After release branch cut, RM (Release Manager) should add job [cherrypick action](https://github.com/envoyproxy/gateway/blob/main/.github/workflows/cherrypick.yaml) for target release. Configuration looks like following: diff --git a/site/content/en/v0.4.0/contributions/_index.md b/site/content/en/v0.4/contributions/_index.md similarity index 100% rename from site/content/en/v0.4.0/contributions/_index.md rename to site/content/en/v0.4/contributions/_index.md diff --git a/site/content/en/v0.4.0/contributions/roadmap.md b/site/content/en/v0.4/contributions/roadmap.md similarity index 100% rename from site/content/en/v0.4.0/contributions/roadmap.md rename to site/content/en/v0.4/contributions/roadmap.md diff --git a/site/content/en/v0.4.0/design/_index.md b/site/content/en/v0.4/design/_index.md similarity index 100% rename from site/content/en/v0.4.0/design/_index.md rename to site/content/en/v0.4/design/_index.md diff --git a/site/content/en/v0.4.0/design/bootstrap.md b/site/content/en/v0.4/design/bootstrap.md similarity index 99% rename from site/content/en/v0.4.0/design/bootstrap.md rename to site/content/en/v0.4/design/bootstrap.md index 9a8f0c789ef..08c71f978d1 100644 --- a/site/content/en/v0.4.0/design/bootstrap.md +++ b/site/content/en/v0.4/design/bootstrap.md @@ -376,6 +376,6 @@ spec: ``` [Issue 31]: https://github.com/envoyproxy/gateway/issues/31 -[EnvoyProxy]: https://gateway.envoyproxy.io/latest/api/config_types.html#envoyproxy +[EnvoyProxy]: ../api/config_types#envoyproxy [GatewayClass]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.GatewayClass [parametersRef]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.ParametersReference diff --git a/site/content/en/v0.4.0/design/config-api.md b/site/content/en/v0.4/design/config-api.md similarity index 98% rename from site/content/en/v0.4.0/design/config-api.md rename to site/content/en/v0.4/design/config-api.md index ca5380151a8..3762bfb93e9 100644 --- a/site/content/en/v0.4.0/design/config-api.md +++ b/site/content/en/v0.4/design/config-api.md @@ -88,7 +88,7 @@ type Gateway struct { // defaults to "gateway.envoyproxy.io/gatewayclass-controller". See the following // for additional details: // - // https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.GatewayClass + // https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.GatewayClass // // +optional ControllerName string `json:"controllerName,omitempty"` @@ -347,6 +347,6 @@ __Note:__ The NetworkPublishing API is currently undefined and is provided here [issue_51]: https://github.com/envoyproxy/gateway/issues/51 [design_doc]: https://github.com/envoyproxy/gateway/blob/main/docs/design/SYSTEM_DESIGN.md [gw_api]: https://gateway-api.sigs.k8s.io/ -[gc]: https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.GatewayClass +[gc]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.GatewayClass [cr]: https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/ [union]: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#unions diff --git a/site/content/en/v0.4.0/design/egctl.md b/site/content/en/v0.4/design/egctl.md similarity index 100% rename from site/content/en/v0.4.0/design/egctl.md rename to site/content/en/v0.4/design/egctl.md diff --git a/site/content/en/v0.4.0/design/extending-envoy-gateway.md b/site/content/en/v0.4/design/extending-envoy-gateway.md similarity index 98% rename from site/content/en/v0.4.0/design/extending-envoy-gateway.md rename to site/content/en/v0.4/design/extending-envoy-gateway.md index df19dcc09d5..bc9d2960706 100644 --- a/site/content/en/v0.4.0/design/extending-envoy-gateway.md +++ b/site/content/en/v0.4/design/extending-envoy-gateway.md @@ -314,11 +314,11 @@ Extending Envoy Gateway by using an external extension server which makes use of [Envoy]: https://www.envoyproxy.io/ [Envoy specific configuration (xDS)]: https://www.envoyproxy.io/docs/envoy/v1.25.1/configuration/configuration [v1beta1]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io%2fv1beta1 -[rate limiting]: https://gateway.envoyproxy.io/v0.3.0/user/rate-limit.html -[authentication]: https://gateway.envoyproxy.io/v0.3.0/user/authn.html +[rate limiting]: ../user/rate-limit +[authentication]: ../user/authn [HTTPRoute]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRoute [GRPCRoute]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1alpha2.GRPCRoute -[EnvoyGateway config]: https://gateway.envoyproxy.io/v0.3.0/api/config_types.html#envoygateway +[EnvoyGateway config]: ../api/config_types#envoygateway [controller-runtime]: https://github.com/kubernetes-sigs/controller-runtime [Unstructured]: https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured [Listener]: https://www.envoyproxy.io/docs/envoy/v1.23.0/api-v3/config/listener/v3/listener.proto#config-listener-v3-listener diff --git a/site/content/en/v0.4.0/design/gatewayapi-translator.md b/site/content/en/v0.4/design/gatewayapi-translator.md similarity index 100% rename from site/content/en/v0.4.0/design/gatewayapi-translator.md rename to site/content/en/v0.4/design/gatewayapi-translator.md diff --git a/site/content/en/v0.4.0/design/goals.md b/site/content/en/v0.4/design/goals.md similarity index 100% rename from site/content/en/v0.4.0/design/goals.md rename to site/content/en/v0.4/design/goals.md diff --git a/site/content/en/v0.4.0/design/rate-limit.md b/site/content/en/v0.4/design/rate-limit.md similarity index 100% rename from site/content/en/v0.4.0/design/rate-limit.md rename to site/content/en/v0.4/design/rate-limit.md diff --git a/site/content/en/v0.4.0/design/request-authentication.md b/site/content/en/v0.4/design/request-authentication.md similarity index 100% rename from site/content/en/v0.4.0/design/request-authentication.md rename to site/content/en/v0.4/design/request-authentication.md diff --git a/site/content/en/v0.4.0/design/system-design.md b/site/content/en/v0.4/design/system-design.md similarity index 96% rename from site/content/en/v0.4.0/design/system-design.md rename to site/content/en/v0.4/design/system-design.md index 16123948ee7..c17c234c13f 100644 --- a/site/content/en/v0.4.0/design/system-design.md +++ b/site/content/en/v0.4/design/system-design.md @@ -159,16 +159,16 @@ The draft for this document is [here][draft_design]. [grl]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_features/global_rate_limiting [rls]: https://github.com/envoyproxy/ratelimit [rlf]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/ratelimit/v3/rate_limit.proto#envoy-v3-api-msg-extensions-filters-http-ratelimit-v3-ratelimit -[crf]: https://gateway-api.sigs.k8s.io/v1alpha2/api-types/httproute/#filters-optional +[crf]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRoute#filters-optional [gwapi_conflicts]: https://gateway-api.sigs.k8s.io/concepts/guidelines/#conflicts [listener]: https://www.envoyproxy.io/docs/envoy/latest/configuration/listeners/listeners#config-listeners [route]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#envoy-v3-api-msg-config-route-v3-route -[be_ref]: https://gateway-api.sigs.k8s.io/v1alpha2/api-types/httproute/#backendrefs-optional +[be_ref]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRoute#backendrefs-optional [cluster]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/cluster.proto#config-cluster-v3-cluster [draft_design]: https://docs.google.com/document/d/1riyTPPYuvNzIhBdrAX8dpfxTmcobWZDSYTTB5NeybuY/edit [cr]: https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/ -[be]: https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.BackendObjectReference +[be]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io%2fv1.BackendObjectReference [svc]: https://kubernetes.io/docs/concepts/services-networking/service/ [ wcd ]: ./watching.md [Issue #37]: https://github.com/envoyproxy/gateway/issues/37 -[roadmap]: roadmap.md +[roadmap]: ../contributions/roadmap diff --git a/site/content/en/v0.4.0/design/tcp-udp-design.md b/site/content/en/v0.4/design/tcp-udp-design.md similarity index 100% rename from site/content/en/v0.4.0/design/tcp-udp-design.md rename to site/content/en/v0.4/design/tcp-udp-design.md diff --git a/site/content/en/v0.4.0/design/watching.md b/site/content/en/v0.4/design/watching.md similarity index 100% rename from site/content/en/v0.4.0/design/watching.md rename to site/content/en/v0.4/design/watching.md diff --git a/site/content/en/v0.4.0/user/_index.md b/site/content/en/v0.4/user/_index.md similarity index 100% rename from site/content/en/v0.4.0/user/_index.md rename to site/content/en/v0.4/user/_index.md diff --git a/site/content/en/v0.4.0/user/authn.md b/site/content/en/v0.4/user/authn.md similarity index 96% rename from site/content/en/v0.4.0/user/authn.md rename to site/content/en/v0.4/user/authn.md index 9f25623bdf7..907e16f752e 100644 --- a/site/content/en/v0.4.0/user/authn.md +++ b/site/content/en/v0.4/user/authn.md @@ -92,5 +92,5 @@ kubectl delete authenticationfilter/jwt-example Checkout the [Developer Guide](../../contributions/develop/) to get involved in the project. [jwt]: https://tools.ietf.org/html/rfc7519 -[AuthenticationFilter]: https://gateway.envoyproxy.io/v0.4.0/api/extension_types.html#authenticationfilter +[AuthenticationFilter]: ../api/config_types#authenticationfilter [jwks]: https://tools.ietf.org/html/rfc7517 diff --git a/site/content/en/v0.4.0/user/customize-envoyproxy.md b/site/content/en/v0.4/user/customize-envoyproxy.md similarity index 95% rename from site/content/en/v0.4.0/user/customize-envoyproxy.md rename to site/content/en/v0.4/user/customize-envoyproxy.md index 0f2b92f2dab..692750b8623 100644 --- a/site/content/en/v0.4.0/user/customize-envoyproxy.md +++ b/site/content/en/v0.4/user/customize-envoyproxy.md @@ -243,11 +243,11 @@ spec: EOF ``` -You can use [egctl translate](https://gateway.envoyproxy.io/v0.4.0/user/egctl.html#validating-gateway-api-configuration) +You can use [egctl translate](../user/egctl#validating-gateway-api-configuration) to get the default xDS Bootstrap configuration used by Envoy Gateway. After applying the config, the bootstrap config will be overridden by the new config you provided. Any errors in the configuration will be surfaced as status within the `GatewayClass` resource. -You can also validate this configuration using [egctl translate](https://gateway.envoyproxy.io/v0.4/user/egctl.html#validating-gateway-api-configuration). +You can also validate this configuration using [egctl translate](../user/egctl#validating-gateway-api-configuration). [Gateway API documentation]: https://gateway-api.sigs.k8s.io/ -[EnvoyProxy]: https://gateway.envoyproxy.io/v0.4.0/api/config_types.html#envoyproxy +[EnvoyProxy]: ../api/config_types#envoyproxy diff --git a/site/content/en/v0.4.0/user/deployment-mode.md b/site/content/en/v0.4/user/deployment-mode.md similarity index 100% rename from site/content/en/v0.4.0/user/deployment-mode.md rename to site/content/en/v0.4/user/deployment-mode.md diff --git a/site/content/en/v0.4.0/user/egctl.md b/site/content/en/v0.4/user/egctl.md similarity index 99% rename from site/content/en/v0.4.0/user/egctl.md rename to site/content/en/v0.4/user/egctl.md index 29f0200f896..3e4b6c79d99 100644 --- a/site/content/en/v0.4.0/user/egctl.md +++ b/site/content/en/v0.4/user/egctl.md @@ -463,7 +463,7 @@ spec: EOF ``` -You can see the output contains a [EnvoyProxy](https://gateway.envoyproxy.io/v0.4/api/config_types.html#envoyproxy) resource that +You can see the output contains a [EnvoyProxy](../api/config_types#envoyproxy) resource that can be used as a starting point to modify the xDS bootstrap resource for the managed Envoy Proxy fleet. ```yaml diff --git a/site/content/en/v0.4.0/user/gatewayapi-support.md b/site/content/en/v0.4/user/gatewayapi-support.md similarity index 96% rename from site/content/en/v0.4.0/user/gatewayapi-support.md rename to site/content/en/v0.4/user/gatewayapi-support.md index 79e07749842..f0938702966 100644 --- a/site/content/en/v0.4.0/user/gatewayapi-support.md +++ b/site/content/en/v0.4/user/gatewayapi-support.md @@ -96,7 +96,7 @@ these types of cross-namespace references. Envoy Gateway supports the following namespace. - Allowing a Gateway's [SecretObjectReference][] to reference a secret in a different namespace. -[system design]: https://gateway.envoyproxy.io/latest/design/system-design.html +[system design]: ../design/system-design [Gateway API]: https://gateway-api.sigs.k8s.io/ [GatewayClass]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.GatewayClass [parameters reference]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.ParametersReference @@ -113,9 +113,9 @@ these types of cross-namespace references. Envoy Gateway supports the following [TLSRoute]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1alpha2.TLSRoute [ReferenceGrant]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io%2fv1beta1.ReferenceGrant [SecretObjectReference]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.SecretObjectReference -[rate limiting]: https://gateway.envoyproxy.io/latest/user/rate-limit.html -[request authentication]: https://gateway.envoyproxy.io/latest/user/authn.html -[EnvoyProxy]: https://gateway.envoyproxy.io/latest/api/config_types.html#envoyproxy +[rate limiting]: ./rate-limit +[request authentication]: ../user/authn +[EnvoyProxy]: ../api/config_types#envoyproxy [resolving conflicts]: https://gateway-api.sigs.k8s.io/concepts/guidelines/?h=conflict#conflicts [ExtensionRefs]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteFilterType [grpc-filter]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1alpha2.GRPCRouteFilter diff --git a/site/content/en/v0.4.0/user/grpc-routing.md b/site/content/en/v0.4/user/grpc-routing.md similarity index 100% rename from site/content/en/v0.4.0/user/grpc-routing.md rename to site/content/en/v0.4/user/grpc-routing.md diff --git a/site/content/en/v0.4.0/user/http-redirect.md b/site/content/en/v0.4/user/http-redirect.md similarity index 100% rename from site/content/en/v0.4.0/user/http-redirect.md rename to site/content/en/v0.4/user/http-redirect.md diff --git a/site/content/en/v0.4.0/user/http-request-headers.md b/site/content/en/v0.4/user/http-request-headers.md similarity index 100% rename from site/content/en/v0.4.0/user/http-request-headers.md rename to site/content/en/v0.4/user/http-request-headers.md diff --git a/site/content/en/v0.4.0/user/http-response-headers.md b/site/content/en/v0.4/user/http-response-headers.md similarity index 100% rename from site/content/en/v0.4.0/user/http-response-headers.md rename to site/content/en/v0.4/user/http-response-headers.md diff --git a/site/content/en/v0.4.0/user/http-routing.md b/site/content/en/v0.4/user/http-routing.md similarity index 100% rename from site/content/en/v0.4.0/user/http-routing.md rename to site/content/en/v0.4/user/http-routing.md diff --git a/site/content/en/v0.4.0/user/http-traffic-splitting.md b/site/content/en/v0.4/user/http-traffic-splitting.md similarity index 100% rename from site/content/en/v0.4.0/user/http-traffic-splitting.md rename to site/content/en/v0.4/user/http-traffic-splitting.md diff --git a/site/content/en/v0.4.0/user/http-urlrewrite.md b/site/content/en/v0.4/user/http-urlrewrite.md similarity index 100% rename from site/content/en/v0.4.0/user/http-urlrewrite.md rename to site/content/en/v0.4/user/http-urlrewrite.md diff --git a/site/content/en/v0.4.0/user/quickstart.md b/site/content/en/v0.4/user/quickstart.md similarity index 96% rename from site/content/en/v0.4.0/user/quickstart.md rename to site/content/en/v0.4/user/quickstart.md index ef4df466c1a..7020707a0c5 100644 --- a/site/content/en/v0.4.0/user/quickstart.md +++ b/site/content/en/v0.4/user/quickstart.md @@ -9,7 +9,7 @@ This guide will help you get started with Envoy Gateway in a few simple steps. A Kubernetes cluster. -__Note:__ Refer to the [Compatibility Matrix](/blog/2022/10/01/versions/) for supported Kubernetes versions. +__Note:__ Refer to the [Compatibility Matrix](/news/releases/matrix/) for supported Kubernetes versions. ## Installation diff --git a/site/content/en/v0.4.0/user/rate-limit.md b/site/content/en/v0.4/user/rate-limit.md similarity index 98% rename from site/content/en/v0.4.0/user/rate-limit.md rename to site/content/en/v0.4/user/rate-limit.md index e932db92627..847115571d6 100644 --- a/site/content/en/v0.4.0/user/rate-limit.md +++ b/site/content/en/v0.4/user/rate-limit.md @@ -625,9 +625,9 @@ EOF kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system ``` -[Global rate limiting]: https://www.envoyproxy.io/docs/envoy/v0.4.0/intro/arch_overview/other_features/global_rate_limiting -[RateLimitFilter]: https://gateway.envoyproxy.io/v0.4.0/api/extension_types.html#ratelimitfilter +[Global rate limiting]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_features/global_rate_limiting +[RateLimitFilter]: ../api/extension_types#ratelimitfilter [Envoy Ratelimit]: https://github.com/envoyproxy/ratelimit -[EnvoyGateway]: https://gateway.envoyproxy.io/v0.4.0/api/config_types.html#envoygateway +[EnvoyGateway]: ../api/config_types#envoygateway [HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute/ [ExtensionRef]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io%2fv1beta1.HTTPRouteFilter diff --git a/site/content/en/v0.4.0/user/secure-gateways.md b/site/content/en/v0.4/user/secure-gateways.md similarity index 100% rename from site/content/en/v0.4.0/user/secure-gateways.md rename to site/content/en/v0.4/user/secure-gateways.md diff --git a/site/content/en/v0.4.0/user/tcp-routing.md b/site/content/en/v0.4/user/tcp-routing.md similarity index 100% rename from site/content/en/v0.4.0/user/tcp-routing.md rename to site/content/en/v0.4/user/tcp-routing.md diff --git a/site/content/en/v0.4.0/user/tls-passthrough.md b/site/content/en/v0.4/user/tls-passthrough.md similarity index 100% rename from site/content/en/v0.4.0/user/tls-passthrough.md rename to site/content/en/v0.4/user/tls-passthrough.md diff --git a/site/content/en/v0.4.0/user/udp-routing.md b/site/content/en/v0.4/user/udp-routing.md similarity index 98% rename from site/content/en/v0.4.0/user/udp-routing.md rename to site/content/en/v0.4/user/udp-routing.md index 5230be9f8f5..c57a8ab1a82 100644 --- a/site/content/en/v0.4.0/user/udp-routing.md +++ b/site/content/en/v0.4/user/udp-routing.md @@ -153,4 +153,4 @@ kubectl delete udproute/coredns Checkout the [Developer Guide](../../contributions/develop/) to get involved in the project. [UDPRoute]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1alpha2.UDPRoute -[UDP proxy documentation]: https://www.envoyproxy.io/docs/envoy/v0.4.0/configuration/listeners/udp_filters/udp_proxy +[UDP proxy documentation]: https://www.envoyproxy.io/docs/envoy/latest/configuration/listeners/udp_filters/udp_proxy diff --git a/site/content/en/v0.5.0/_index.md b/site/content/en/v0.5/_index.md similarity index 100% rename from site/content/en/v0.5.0/_index.md rename to site/content/en/v0.5/_index.md diff --git a/site/content/en/v0.6.0/api/_index.md b/site/content/en/v0.5/api/_index.md similarity index 100% rename from site/content/en/v0.6.0/api/_index.md rename to site/content/en/v0.5/api/_index.md diff --git a/site/content/en/v0.5.0/api/config_types.md b/site/content/en/v0.5/api/config_types.md similarity index 99% rename from site/content/en/v0.5.0/api/config_types.md rename to site/content/en/v0.5/api/config_types.md index 93764201f34..23010b5d035 100644 --- a/site/content/en/v0.5.0/api/config_types.md +++ b/site/content/en/v0.5/api/config_types.md @@ -431,7 +431,7 @@ _Appears in:_ | Field | Description | | --- | --- | -| `controllerName` _string_ | ControllerName defines the name of the Gateway API controller. If unspecified, defaults to "gateway.envoyproxy.io/gatewayclass-controller". See the following for additional details: https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.GatewayClass | +| `controllerName` _string_ | ControllerName defines the name of the Gateway API controller. If unspecified, defaults to "gateway.envoyproxy.io/gatewayclass-controller". See the following for additional details: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.GatewayClass | ## GroupVersionKind diff --git a/site/content/en/v0.5.0/api/extension_types.md b/site/content/en/v0.5/api/extension_types.md similarity index 100% rename from site/content/en/v0.5.0/api/extension_types.md rename to site/content/en/v0.5/api/extension_types.md diff --git a/site/content/en/v0.5.0/contributions/CODEOWNERS.md b/site/content/en/v0.5/contributions/CODEOWNERS.md similarity index 100% rename from site/content/en/v0.5.0/contributions/CODEOWNERS.md rename to site/content/en/v0.5/contributions/CODEOWNERS.md diff --git a/site/content/en/v0.5.0/contributions/CODE_OF_CONDUCT.md b/site/content/en/v0.5/contributions/CODE_OF_CONDUCT.md similarity index 100% rename from site/content/en/v0.5.0/contributions/CODE_OF_CONDUCT.md rename to site/content/en/v0.5/contributions/CODE_OF_CONDUCT.md diff --git a/site/content/en/v0.3.0/contributions/CONTRIBUTING.md b/site/content/en/v0.5/contributions/CONTRIBUTING.md similarity index 97% rename from site/content/en/v0.3.0/contributions/CONTRIBUTING.md rename to site/content/en/v0.5/contributions/CONTRIBUTING.md index f94b2c940e9..b37898e948e 100644 --- a/site/content/en/v0.3.0/contributions/CONTRIBUTING.md +++ b/site/content/en/v0.5/contributions/CONTRIBUTING.md @@ -49,7 +49,7 @@ to the following guidelines for all code, APIs, and documentation: build. If your PR cannot have 100% coverage for some reason please clearly explain why when you open it. * Any PR that changes user-facing behavior **must** have associated documentation in the [docs](https://github.com/envoyproxy/gateway/tree/main/site) folder of the repo as - well as the [changelog](/blog/releases). + well as the [changelog](/news/releases). * All code comments and documentation are expected to have proper English grammar and punctuation. If you are not a fluent English speaker (or a bad writer ;-)) please let us know and we will try to find some help but there are no guarantees. @@ -175,7 +175,7 @@ git config --add alias.c "commit -s" ## Fixing DCO If your PR fails the DCO check, it's necessary to fix the entire commit history in the PR. Best -practice is to [squash](https://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html) +practice is to [squash](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges#squash-and-merge-your-commits) the commit history to a single commit, append the DCO sign-off as described above, and [force push](https://git-scm.com/docs/git-push#git-push---force). For example, if you have 2 commits in your history: diff --git a/site/content/en/v0.5.0/contributions/DEVELOP.md b/site/content/en/v0.5/contributions/DEVELOP.md similarity index 98% rename from site/content/en/v0.5.0/contributions/DEVELOP.md rename to site/content/en/v0.5/contributions/DEVELOP.md index 6f82c4a411f..67500b42915 100644 --- a/site/content/en/v0.5.0/contributions/DEVELOP.md +++ b/site/content/en/v0.5/contributions/DEVELOP.md @@ -158,6 +158,6 @@ and is hosted in the repo. [Envoy admin interface]: https://www.envoyproxy.io/docs/envoy/latest/operations/admin#operations-admin-interface [jwt]: https://tools.ietf.org/html/rfc7519 [jwks]: https://tools.ietf.org/html/rfc7517 -[request authentication]: https://gateway.envoyproxy.io/latest/user/authn.html +[request authentication]: ../user/authn [JWT Debugger]: https://jwt.io/ [JWK Creator]: https://russelldavies.github.io/jwk-creator/ diff --git a/site/content/en/v0.5.0/contributions/DOCS.md b/site/content/en/v0.5/contributions/DOCS.md similarity index 100% rename from site/content/en/v0.5.0/contributions/DOCS.md rename to site/content/en/v0.5/contributions/DOCS.md diff --git a/site/content/en/v0.5.0/contributions/RELEASING.md b/site/content/en/v0.5/contributions/RELEASING.md similarity index 98% rename from site/content/en/v0.5.0/contributions/RELEASING.md rename to site/content/en/v0.5/contributions/RELEASING.md index f84f711b068..206c9f0589d 100644 --- a/site/content/en/v0.5.0/contributions/RELEASING.md +++ b/site/content/en/v0.5/contributions/RELEASING.md @@ -73,7 +73,7 @@ export GITHUB_REMOTE=origin ### Setup cherry picker action -After release branch cut, RM (Release Manager) should add job [cherrypick action](../../../.github/workflows/cherrypick.yaml) for target release. +After release branch cut, RM (Release Manager) should add job [cherrypick action](https://github.com/envoyproxy/gateway/blob/main/.github/workflows/cherrypick.yaml) for target release. Configuration looks like following: diff --git a/site/content/en/v0.5.0/contributions/_index.md b/site/content/en/v0.5/contributions/_index.md similarity index 100% rename from site/content/en/v0.5.0/contributions/_index.md rename to site/content/en/v0.5/contributions/_index.md diff --git a/site/content/en/v0.5.0/contributions/roadmap.md b/site/content/en/v0.5/contributions/roadmap.md similarity index 100% rename from site/content/en/v0.5.0/contributions/roadmap.md rename to site/content/en/v0.5/contributions/roadmap.md diff --git a/site/content/en/v0.5.0/design/_index.md b/site/content/en/v0.5/design/_index.md similarity index 100% rename from site/content/en/v0.5.0/design/_index.md rename to site/content/en/v0.5/design/_index.md diff --git a/site/content/en/v0.5.0/design/accesslog.md b/site/content/en/v0.5/design/accesslog.md similarity index 100% rename from site/content/en/v0.5.0/design/accesslog.md rename to site/content/en/v0.5/design/accesslog.md diff --git a/site/content/en/v0.5.0/design/bootstrap.md b/site/content/en/v0.5/design/bootstrap.md similarity index 99% rename from site/content/en/v0.5.0/design/bootstrap.md rename to site/content/en/v0.5/design/bootstrap.md index 9a8f0c789ef..08c71f978d1 100644 --- a/site/content/en/v0.5.0/design/bootstrap.md +++ b/site/content/en/v0.5/design/bootstrap.md @@ -376,6 +376,6 @@ spec: ``` [Issue 31]: https://github.com/envoyproxy/gateway/issues/31 -[EnvoyProxy]: https://gateway.envoyproxy.io/latest/api/config_types.html#envoyproxy +[EnvoyProxy]: ../api/config_types#envoyproxy [GatewayClass]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.GatewayClass [parametersRef]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.ParametersReference diff --git a/site/content/en/v0.5.0/design/config-api.md b/site/content/en/v0.5/design/config-api.md similarity index 98% rename from site/content/en/v0.5.0/design/config-api.md rename to site/content/en/v0.5/design/config-api.md index ca5380151a8..3762bfb93e9 100644 --- a/site/content/en/v0.5.0/design/config-api.md +++ b/site/content/en/v0.5/design/config-api.md @@ -88,7 +88,7 @@ type Gateway struct { // defaults to "gateway.envoyproxy.io/gatewayclass-controller". See the following // for additional details: // - // https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.GatewayClass + // https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.GatewayClass // // +optional ControllerName string `json:"controllerName,omitempty"` @@ -347,6 +347,6 @@ __Note:__ The NetworkPublishing API is currently undefined and is provided here [issue_51]: https://github.com/envoyproxy/gateway/issues/51 [design_doc]: https://github.com/envoyproxy/gateway/blob/main/docs/design/SYSTEM_DESIGN.md [gw_api]: https://gateway-api.sigs.k8s.io/ -[gc]: https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.GatewayClass +[gc]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.GatewayClass [cr]: https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/ [union]: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#unions diff --git a/site/content/en/v0.5.0/design/egctl.md b/site/content/en/v0.5/design/egctl.md similarity index 100% rename from site/content/en/v0.5.0/design/egctl.md rename to site/content/en/v0.5/design/egctl.md diff --git a/site/content/en/v0.5.0/design/envoy-patch-policy.md b/site/content/en/v0.5/design/envoy-patch-policy.md similarity index 91% rename from site/content/en/v0.5.0/design/envoy-patch-policy.md rename to site/content/en/v0.5/design/envoy-patch-policy.md index d34937d05ef..04081ac0763 100644 --- a/site/content/en/v0.5.0/design/envoy-patch-policy.md +++ b/site/content/en/v0.5/design/envoy-patch-policy.md @@ -167,10 +167,10 @@ patches will work. [Gateway API]: https://gateway-api.sigs.k8s.io/ [Kubernetes]: https://kubernetes.io/docs/tasks/manage-kubernetes-objects/update-api-object-kubectl-patch/ [Kustomize]: https://github.com/kubernetes-sigs/kustomize/blob/master/examples/jsonpatch.md -[Extension APIs]: https://gateway.envoyproxy.io/latest/api/extension_types.html -[RateLimit]: https://gateway.envoyproxy.io/latest/user/rate-limit.html -[EnvoyGateway]: https://gateway.envoyproxy.io/latest/api/config_types.html#envoygateway -[Extending the Control Plane]: https://gateway.envoyproxy.io/latest/design/extending-envoy-gateway.html +[Extension APIs]: ../api/extension_types +[RateLimit]: ../user/rate-limit +[EnvoyGateway]: ../api/config_types#envoygateway +[Extending the Control Plane]: ./extending-envoy-gateway [EnvoyFilter]: https://istio.io/latest/docs/reference/config/networking/envoy-filter -[egctl x translate]: https://gateway.envoyproxy.io/latest/user/egctl.html#egctl-experimental-translate -[Bootstrap configuration using EnvoyProxy API]: https://gateway.envoyproxy.io/latest/user/customize-envoyproxy.html#customize-envoyproxy-bootstrap-config +[egctl x translate]: ../user/egctl#egctl-experimental-translate +[Bootstrap configuration using EnvoyProxy API]: ../user/customize-envoyproxy#customize-envoyproxy-bootstrap-config diff --git a/site/content/en/v0.5.0/design/extending-envoy-gateway.md b/site/content/en/v0.5/design/extending-envoy-gateway.md similarity index 98% rename from site/content/en/v0.5.0/design/extending-envoy-gateway.md rename to site/content/en/v0.5/design/extending-envoy-gateway.md index 0caa870ffb1..7624ceaa2af 100644 --- a/site/content/en/v0.5.0/design/extending-envoy-gateway.md +++ b/site/content/en/v0.5/design/extending-envoy-gateway.md @@ -315,11 +315,11 @@ Extending Envoy Gateway by using an external extension server which makes use of [Envoy]: https://www.envoyproxy.io/ [Envoy specific configuration (xDS)]: https://www.envoyproxy.io/docs/envoy/v1.25.1/configuration/configuration [v1beta1]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io%2fv1beta1 -[rate limiting]: https://gateway.envoyproxy.io/v0.3.0/user/rate-limit.html -[authentication]: https://gateway.envoyproxy.io/v0.3.0/user/authn.html +[rate limiting]: ../user/rate-limit +[authentication]: ../user/authn [HTTPRoute]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRoute [GRPCRoute]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1alpha2.GRPCRoute -[EnvoyGateway config]: https://gateway.envoyproxy.io/v0.3.0/api/config_types.html#envoygateway +[EnvoyGateway config]: ../api/config_types#envoygateway [controller-runtime]: https://github.com/kubernetes-sigs/controller-runtime [Unstructured]: https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured [Listener]: https://www.envoyproxy.io/docs/envoy/v1.23.0/api-v3/config/listener/v3/listener.proto#config-listener-v3-listener diff --git a/site/content/en/v0.5.0/design/gatewayapi-translator.md b/site/content/en/v0.5/design/gatewayapi-translator.md similarity index 100% rename from site/content/en/v0.5.0/design/gatewayapi-translator.md rename to site/content/en/v0.5/design/gatewayapi-translator.md diff --git a/site/content/en/v0.5.0/design/goals.md b/site/content/en/v0.5/design/goals.md similarity index 100% rename from site/content/en/v0.5.0/design/goals.md rename to site/content/en/v0.5/design/goals.md diff --git a/site/content/en/v0.5.0/design/local-envoy-gateway.md b/site/content/en/v0.5/design/local-envoy-gateway.md similarity index 100% rename from site/content/en/v0.5.0/design/local-envoy-gateway.md rename to site/content/en/v0.5/design/local-envoy-gateway.md diff --git a/site/content/en/v0.5.0/design/metrics.md b/site/content/en/v0.5/design/metrics.md similarity index 100% rename from site/content/en/v0.5.0/design/metrics.md rename to site/content/en/v0.5/design/metrics.md diff --git a/site/content/en/v0.5.0/design/pprof.md b/site/content/en/v0.5/design/pprof.md similarity index 100% rename from site/content/en/v0.5.0/design/pprof.md rename to site/content/en/v0.5/design/pprof.md diff --git a/site/content/en/v0.5.0/design/rate-limit.md b/site/content/en/v0.5/design/rate-limit.md similarity index 100% rename from site/content/en/v0.5.0/design/rate-limit.md rename to site/content/en/v0.5/design/rate-limit.md diff --git a/site/content/en/v0.5.0/design/request-authentication.md b/site/content/en/v0.5/design/request-authentication.md similarity index 100% rename from site/content/en/v0.5.0/design/request-authentication.md rename to site/content/en/v0.5/design/request-authentication.md diff --git a/site/content/en/v0.5.0/design/system-design.md b/site/content/en/v0.5/design/system-design.md similarity index 96% rename from site/content/en/v0.5.0/design/system-design.md rename to site/content/en/v0.5/design/system-design.md index 16123948ee7..c40c3e51fc9 100644 --- a/site/content/en/v0.5.0/design/system-design.md +++ b/site/content/en/v0.5/design/system-design.md @@ -159,16 +159,17 @@ The draft for this document is [here][draft_design]. [grl]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_features/global_rate_limiting [rls]: https://github.com/envoyproxy/ratelimit [rlf]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/ratelimit/v3/rate_limit.proto#envoy-v3-api-msg-extensions-filters-http-ratelimit-v3-ratelimit -[crf]: https://gateway-api.sigs.k8s.io/v1alpha2/api-types/httproute/#filters-optional +[crf]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRoute#filters-optional [gwapi_conflicts]: https://gateway-api.sigs.k8s.io/concepts/guidelines/#conflicts [listener]: https://www.envoyproxy.io/docs/envoy/latest/configuration/listeners/listeners#config-listeners [route]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#envoy-v3-api-msg-config-route-v3-route -[be_ref]: https://gateway-api.sigs.k8s.io/v1alpha2/api-types/httproute/#backendrefs-optional +[be_ref]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRoute#backendrefs-optional [cluster]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/cluster.proto#config-cluster-v3-cluster [draft_design]: https://docs.google.com/document/d/1riyTPPYuvNzIhBdrAX8dpfxTmcobWZDSYTTB5NeybuY/edit [cr]: https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/ -[be]: https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.BackendObjectReference +[be]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io%2fv1.BackendObjectReference [svc]: https://kubernetes.io/docs/concepts/services-networking/service/ -[ wcd ]: ./watching.md +[ wcd ]: ./watching [Issue #37]: https://github.com/envoyproxy/gateway/issues/37 -[roadmap]: roadmap.md +[roadmap]: ../contributions/roadmap + diff --git a/site/content/en/v0.5.0/design/tcp-udp-design.md b/site/content/en/v0.5/design/tcp-udp-design.md similarity index 100% rename from site/content/en/v0.5.0/design/tcp-udp-design.md rename to site/content/en/v0.5/design/tcp-udp-design.md diff --git a/site/content/en/v0.5.0/design/tracing.md b/site/content/en/v0.5/design/tracing.md similarity index 100% rename from site/content/en/v0.5.0/design/tracing.md rename to site/content/en/v0.5/design/tracing.md diff --git a/site/content/en/v0.5.0/design/watching.md b/site/content/en/v0.5/design/watching.md similarity index 100% rename from site/content/en/v0.5.0/design/watching.md rename to site/content/en/v0.5/design/watching.md diff --git a/site/content/en/v0.6.0/install/_index.md b/site/content/en/v0.5/install/_index.md similarity index 100% rename from site/content/en/v0.6.0/install/_index.md rename to site/content/en/v0.5/install/_index.md diff --git a/site/content/en/v0.5.0/install/api.md b/site/content/en/v0.5/install/api.md similarity index 100% rename from site/content/en/v0.5.0/install/api.md rename to site/content/en/v0.5/install/api.md diff --git a/site/content/en/v0.5.0/install/install-egctl.md b/site/content/en/v0.5/install/install-egctl.md similarity index 96% rename from site/content/en/v0.5.0/install/install-egctl.md rename to site/content/en/v0.5/install/install-egctl.md index 8534acb7127..86649ac248c 100644 --- a/site/content/en/v0.5.0/install/install-egctl.md +++ b/site/content/en/v0.5/install/install-egctl.md @@ -52,6 +52,6 @@ curl https://gateway.envoyproxy.io/get-egctl.sh | VERSION=latest bash {{% alert title="Next Steps" color="warning" %}} -You can refer to [User Guides](../../user/egctl) to more details about egctl. +You can refer to [User Guides](../user/egctl) to more details about egctl. {{% /alert %}} diff --git a/site/content/en/v0.5.0/install/install-helm.md b/site/content/en/v0.5/install/install-helm.md similarity index 97% rename from site/content/en/v0.5.0/install/install-helm.md rename to site/content/en/v0.5/install/install-helm.md index 44e84aaa9df..4e988b07b11 100644 --- a/site/content/en/v0.5.0/install/install-helm.md +++ b/site/content/en/v0.5/install/install-helm.md @@ -10,7 +10,7 @@ Envoy Gateway can be installed via a Helm chart with a few simple steps, dependi ## Before you begin {{% alert title="Compatibility Matrix" color="warning" %}} -Refer to the [Version Compatibility Matrix](/blog/2022/10/01/versions/) to learn more. +Refer to the [Version Compatibility Matrix](/news/releases/matrix/) to learn more. {{% /alert %}} The Envoy Gateway Helm chart is hosted by DockerHub. @@ -138,5 +138,5 @@ These are the ports used by Envoy Gateway and the managed Envoy Proxy. | Heath Check | 0.0.0.0 | 19001 | {{% alert title="Next Steps" color="warning" %}} -Envoy Gateway should now be successfully installed and running, but in order to experience more abilities of Envoy Gateway, you can refer to [User Guides](../../user). +Envoy Gateway should now be successfully installed and running, but in order to experience more abilities of Envoy Gateway, you can refer to [User Guides](../user). {{% /alert %}} diff --git a/site/content/en/v0.5.0/install/install-yaml.md b/site/content/en/v0.5/install/install-yaml.md similarity index 90% rename from site/content/en/v0.5.0/install/install-yaml.md rename to site/content/en/v0.5/install/install-yaml.md index 28f6bec4a76..fcf0c55b37d 100644 --- a/site/content/en/v0.5.0/install/install-yaml.md +++ b/site/content/en/v0.5/install/install-yaml.md @@ -17,7 +17,7 @@ Envoy Gateway is designed to run in Kubernetes for production. The most essentia * The `kubectl` command-line tool {{% alert title="Compatibility Matrix" color="warning" %}} -Refer to the [Version Compatibility Matrix](/blog/2022/10/01/versions/) to learn more. +Refer to the [Version Compatibility Matrix](/news/releases/matrix/) to learn more. {{% /alert %}} ## Install with YAML @@ -36,4 +36,4 @@ Refer to the [Developer Guide](../../contributions/develop) to learn more. 2. Next Steps - Envoy Gateway should now be successfully installed and running, but in order to experience more abilities of Envoy Gateway, you can refer to [User Guides](../../user). + Envoy Gateway should now be successfully installed and running, but in order to experience more abilities of Envoy Gateway, you can refer to [User Guides](../user). diff --git a/site/content/en/v0.5.0/user/_index.md b/site/content/en/v0.5/user/_index.md similarity index 100% rename from site/content/en/v0.5.0/user/_index.md rename to site/content/en/v0.5/user/_index.md diff --git a/site/content/en/v0.5.0/user/authn.md b/site/content/en/v0.5/user/authn.md similarity index 96% rename from site/content/en/v0.5.0/user/authn.md rename to site/content/en/v0.5/user/authn.md index 3762e6f814b..77954272288 100644 --- a/site/content/en/v0.5.0/user/authn.md +++ b/site/content/en/v0.5/user/authn.md @@ -92,5 +92,5 @@ kubectl delete authenticationfilter/jwt-example Checkout the [Developer Guide](../../contributions/develop/) to get involved in the project. [jwt]: https://tools.ietf.org/html/rfc7519 -[AuthenticationFilter]: https://gateway.envoyproxy.io/v0.5.0/api/extension_types.html#authenticationfilter +[AuthenticationFilter]: ../api/extension_types#authenticationfilter [jwks]: https://tools.ietf.org/html/rfc7517 diff --git a/site/content/en/v0.5.0/user/customize-envoyproxy.md b/site/content/en/v0.5/user/customize-envoyproxy.md similarity index 96% rename from site/content/en/v0.5.0/user/customize-envoyproxy.md rename to site/content/en/v0.5/user/customize-envoyproxy.md index 24bdbec7033..937f5b82435 100644 --- a/site/content/en/v0.5.0/user/customize-envoyproxy.md +++ b/site/content/en/v0.5/user/customize-envoyproxy.md @@ -304,12 +304,12 @@ spec: EOF ``` -You can use [egctl translate](https://gateway.envoyproxy.io/v0.5.0/user/egctl.html#validating-gateway-api-configuration) +You can use [egctl translate](./egctl#validating-gateway-api-configuration) to get the default xDS Bootstrap configuration used by Envoy Gateway. After applying the config, the bootstrap config will be overridden by the new config you provided. Any errors in the configuration will be surfaced as status within the `GatewayClass` resource. -You can also validate this configuration using [egctl translate](https://gateway.envoyproxy.io/v0.5.0/user/egctl.html#validating-gateway-api-configuration). +You can also validate this configuration using [egctl translate](./egctl.html#validating-gateway-api-configuration). [Gateway API documentation]: https://gateway-api.sigs.k8s.io/ -[EnvoyProxy]: https://gateway.envoyproxy.io/v0.5.0/api/config_types.html#envoyproxy +[EnvoyProxy]: ../api/config_types#envoyproxy diff --git a/site/content/en/v0.5.0/user/deployment-mode.md b/site/content/en/v0.5/user/deployment-mode.md similarity index 100% rename from site/content/en/v0.5.0/user/deployment-mode.md rename to site/content/en/v0.5/user/deployment-mode.md diff --git a/site/content/en/v0.5.0/user/egctl.md b/site/content/en/v0.5/user/egctl.md similarity index 99% rename from site/content/en/v0.5.0/user/egctl.md rename to site/content/en/v0.5/user/egctl.md index a09b1b43481..4977d2f6c87 100644 --- a/site/content/en/v0.5.0/user/egctl.md +++ b/site/content/en/v0.5/user/egctl.md @@ -444,7 +444,7 @@ spec: EOF ``` -You can see the output contains a [EnvoyProxy](https://gateway.envoyproxy.io/v0.5.0/api/config_types.html#envoyproxy) resource that +You can see the output contains a [EnvoyProxy](../api/config_types#envoyproxy) resource that can be used as a starting point to modify the xDS bootstrap resource for the managed Envoy Proxy fleet. ```yaml diff --git a/site/content/en/v0.5.0/user/envoy-patch-policy.md b/site/content/en/v0.5/user/envoy-patch-policy.md similarity index 94% rename from site/content/en/v0.5.0/user/envoy-patch-policy.md rename to site/content/en/v0.5/user/envoy-patch-policy.md index ae237f2f6ae..cf1f1d78abe 100644 --- a/site/content/en/v0.5.0/user/envoy-patch-policy.md +++ b/site/content/en/v0.5/user/envoy-patch-policy.md @@ -194,9 +194,9 @@ across versions for these reasons * Envoy Gateway might alter the xDS translation creating a different xDS output such as changing the `name` field of resources. -[EnvoyPatchPolicy]: https://gateway.envoyproxy.io/v0.5.0/api/extension_types.html#envoypatchpolicy -[EnvoyGateway]: https://gateway.envoyproxy.io/v0.5.0/api/config_types.html#envoygateway +[EnvoyPatchPolicy]: ../api/extension_types#envoypatchpolicy +[EnvoyGateway]: ../api/config_types#envoygateway [JSON Patch]: https://datatracker.ietf.org/doc/html/rfc6902 -[xDS]: https://www.envoyproxy.io/docs/envoy/v0.5.0/intro/arch_overview/operations/dynamic_configuration -[Local Reply Modification]: https://www.envoyproxy.io/docs/envoy/v0.5.0/configuration/http/http_conn_man/local_reply -[egctl x translate]: https://gateway.envoyproxy.io/v0.5.0/user/egctl.html#egctl-experimental-translate +[xDS]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/operations/dynamic_configuration +[Local Reply Modification]: https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/local_reply +[egctl x translate]: ./egctl#egctl-experimental-translate diff --git a/site/content/en/v0.5.0/user/gateway-address.md b/site/content/en/v0.5/user/gateway-address.md similarity index 100% rename from site/content/en/v0.5.0/user/gateway-address.md rename to site/content/en/v0.5/user/gateway-address.md diff --git a/site/content/en/v0.5.0/user/gatewayapi-support.md b/site/content/en/v0.5/user/gatewayapi-support.md similarity index 96% rename from site/content/en/v0.5.0/user/gatewayapi-support.md rename to site/content/en/v0.5/user/gatewayapi-support.md index 368745d2f75..28505934c47 100644 --- a/site/content/en/v0.5.0/user/gatewayapi-support.md +++ b/site/content/en/v0.5/user/gatewayapi-support.md @@ -94,7 +94,7 @@ these types of cross-namespace references. Envoy Gateway supports the following namespace. - Allowing a Gateway's [SecretObjectReference][] to reference a secret in a different namespace. -[system design]: https://gateway.envoyproxy.io/v0.5.0/design/system-design.html +[system design]: ../design/system-design [Gateway API]: https://gateway-api.sigs.k8s.io/ [GatewayClass]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.GatewayClass [parameters reference]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.ParametersReference @@ -110,9 +110,9 @@ these types of cross-namespace references. Envoy Gateway supports the following [TLSRoute]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1alpha2.TLSRoute [ReferenceGrant]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io%2fv1beta1.ReferenceGrant [SecretObjectReference]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.SecretObjectReference -[rate limiting]: https://gateway.envoyproxy.io/v0.5.0/user/rate-limit.html -[request authentication]: https://gateway.envoyproxy.io/v0.5.0/user/authn.html -[EnvoyProxy]: https://gateway.envoyproxy.io/v0.5.0/api/config_types.html#envoyproxy +[rate limiting]: ./rate-limit +[request authentication]: ./authn +[EnvoyProxy]: ../api/config_types#envoyproxy [resolving conflicts]: https://gateway-api.sigs.k8s.io/concepts/guidelines/?h=conflict#conflicts [ExtensionRefs]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteFilterType [grpc-filter]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1alpha2.GRPCRouteFilter diff --git a/site/content/en/v0.5.0/user/grpc-routing.md b/site/content/en/v0.5/user/grpc-routing.md similarity index 100% rename from site/content/en/v0.5.0/user/grpc-routing.md rename to site/content/en/v0.5/user/grpc-routing.md diff --git a/site/content/en/v0.5.0/user/http-redirect.md b/site/content/en/v0.5/user/http-redirect.md similarity index 100% rename from site/content/en/v0.5.0/user/http-redirect.md rename to site/content/en/v0.5/user/http-redirect.md diff --git a/site/content/en/v0.5.0/user/http-request-headers.md b/site/content/en/v0.5/user/http-request-headers.md similarity index 100% rename from site/content/en/v0.5.0/user/http-request-headers.md rename to site/content/en/v0.5/user/http-request-headers.md diff --git a/site/content/en/v0.5.0/user/http-request-mirroring.md b/site/content/en/v0.5/user/http-request-mirroring.md similarity index 100% rename from site/content/en/v0.5.0/user/http-request-mirroring.md rename to site/content/en/v0.5/user/http-request-mirroring.md diff --git a/site/content/en/v0.5.0/user/http-response-headers.md b/site/content/en/v0.5/user/http-response-headers.md similarity index 100% rename from site/content/en/v0.5.0/user/http-response-headers.md rename to site/content/en/v0.5/user/http-response-headers.md diff --git a/site/content/en/v0.5.0/user/http-routing.md b/site/content/en/v0.5/user/http-routing.md similarity index 100% rename from site/content/en/v0.5.0/user/http-routing.md rename to site/content/en/v0.5/user/http-routing.md diff --git a/site/content/en/v0.5.0/user/http-traffic-splitting.md b/site/content/en/v0.5/user/http-traffic-splitting.md similarity index 100% rename from site/content/en/v0.5.0/user/http-traffic-splitting.md rename to site/content/en/v0.5/user/http-traffic-splitting.md diff --git a/site/content/en/v0.5.0/user/http-urlrewrite.md b/site/content/en/v0.5/user/http-urlrewrite.md similarity index 100% rename from site/content/en/v0.5.0/user/http-urlrewrite.md rename to site/content/en/v0.5/user/http-urlrewrite.md diff --git a/site/content/en/v0.5.0/user/proxy-observability.md b/site/content/en/v0.5/user/proxy-observability.md similarity index 100% rename from site/content/en/v0.5.0/user/proxy-observability.md rename to site/content/en/v0.5/user/proxy-observability.md diff --git a/site/content/en/v0.5.0/user/quickstart.md b/site/content/en/v0.5/user/quickstart.md similarity index 96% rename from site/content/en/v0.5.0/user/quickstart.md rename to site/content/en/v0.5/user/quickstart.md index 8443d6f323f..be0ae418896 100644 --- a/site/content/en/v0.5.0/user/quickstart.md +++ b/site/content/en/v0.5/user/quickstart.md @@ -9,7 +9,7 @@ This guide will help you get started with Envoy Gateway in a few simple steps. A Kubernetes cluster. -__Note:__ Refer to the [Compatibility Matrix](/blog/2022/10/01/versions/) for supported Kubernetes versions. +__Note:__ Refer to the [Compatibility Matrix](/news/releases/matrix/) for supported Kubernetes versions. ## Installation diff --git a/site/content/en/v0.5.0/user/rate-limit.md b/site/content/en/v0.5/user/rate-limit.md similarity index 98% rename from site/content/en/v0.5.0/user/rate-limit.md rename to site/content/en/v0.5/user/rate-limit.md index bddca2d52bb..5f97900c494 100644 --- a/site/content/en/v0.5.0/user/rate-limit.md +++ b/site/content/en/v0.5/user/rate-limit.md @@ -802,9 +802,9 @@ EOF kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system ``` -[Global Rate Limiting]: https://www.envoyproxy.io/docs/envoy/v0.5.0/intro/arch_overview/other_features/global_rate_limiting -[RateLimitFilter]: https://gateway.envoyproxy.io/v0.5.0/api/extension_types.html#ratelimitfilter +[Global Rate Limiting]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_features/global_rate_limiting +[RateLimitFilter]: ../api/extension_types#ratelimitfilter [Envoy Ratelimit]: https://github.com/envoyproxy/ratelimit -[EnvoyGateway]: https://gateway.envoyproxy.io/v0.5.0/api/config_types.html#envoygateway +[EnvoyGateway]: ../api/config_types#envoygateway [HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute/ [ExtensionRef]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io%2fv1beta1.HTTPRouteFilter diff --git a/site/content/en/v0.5.0/user/secure-gateways.md b/site/content/en/v0.5/user/secure-gateways.md similarity index 100% rename from site/content/en/v0.5.0/user/secure-gateways.md rename to site/content/en/v0.5/user/secure-gateways.md diff --git a/site/content/en/v0.5.0/user/tcp-routing.md b/site/content/en/v0.5/user/tcp-routing.md similarity index 100% rename from site/content/en/v0.5.0/user/tcp-routing.md rename to site/content/en/v0.5/user/tcp-routing.md diff --git a/site/content/en/v0.5.0/user/tls-cert-manager.md b/site/content/en/v0.5/user/tls-cert-manager.md similarity index 100% rename from site/content/en/v0.5.0/user/tls-cert-manager.md rename to site/content/en/v0.5/user/tls-cert-manager.md diff --git a/site/content/en/v0.5.0/user/tls-passthrough.md b/site/content/en/v0.5/user/tls-passthrough.md similarity index 100% rename from site/content/en/v0.5.0/user/tls-passthrough.md rename to site/content/en/v0.5/user/tls-passthrough.md diff --git a/site/content/en/v0.5.0/user/tls-termination.md b/site/content/en/v0.5/user/tls-termination.md similarity index 100% rename from site/content/en/v0.5.0/user/tls-termination.md rename to site/content/en/v0.5/user/tls-termination.md diff --git a/site/content/en/v0.5.0/user/udp-routing.md b/site/content/en/v0.5/user/udp-routing.md similarity index 98% rename from site/content/en/v0.5.0/user/udp-routing.md rename to site/content/en/v0.5/user/udp-routing.md index f5eaf0c5541..330f2bdfc01 100644 --- a/site/content/en/v0.5.0/user/udp-routing.md +++ b/site/content/en/v0.5/user/udp-routing.md @@ -153,4 +153,4 @@ kubectl delete udproute/coredns Checkout the [Developer Guide](../../contributions/develop/) to get involved in the project. [UDPRoute]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1alpha2.UDPRoute -[UDP proxy documentation]: https://www.envoyproxy.io/docs/envoy/v0.5.0/configuration/listeners/udp_filters/udp_proxy +[UDP proxy documentation]: https://www.envoyproxy.io/docs/envoy/latest/configuration/listeners/udp_filters/udp_proxy diff --git a/site/content/en/v0.6.0/contributions/CONTRIBUTING.md b/site/content/en/v0.6.0/contributions/CONTRIBUTING.md deleted file mode 100644 index f94b2c940e9..00000000000 --- a/site/content/en/v0.6.0/contributions/CONTRIBUTING.md +++ /dev/null @@ -1,190 +0,0 @@ ---- -title: "Contributing" -description: "This section tells how to contribute to Envoy Gateway." -weight: 3 ---- - -We welcome contributions from the community. Please carefully review the [project goals](/about) -and following guidelines to streamline your contributions. - -## Communication - -* Before starting work on a major feature, please contact us via GitHub or Slack. We will ensure no - one else is working on it and ask you to open a GitHub issue. -* A "major feature" is defined as any change that is > 100 LOC altered (not including tests), or - changes any user-facing behavior. We will use the GitHub issue to discuss the feature and come to - agreement. This is to prevent your time being wasted, as well as ours. The GitHub review process - for major features is also important so that [affiliations with commit access](../codeowners) can - come to agreement on the design. If it's appropriate to write a design document, the document must - be hosted either in the GitHub issue, or linked to from the issue and hosted in a world-readable - location. -* Small patches and bug fixes don't need prior communication. - -## Inclusivity - -The Envoy Gateway community has an explicit goal to be inclusive to all. As such, all PRs must adhere -to the following guidelines for all code, APIs, and documentation: - -* The following words and phrases are not allowed: - * *Whitelist*: use allowlist instead. - * *Blacklist*: use denylist or blocklist instead. - * *Master*: use primary instead. - * *Slave*: use secondary or replica instead. -* Documentation should be written in an inclusive style. The [Google developer - documentation](https://developers.google.com/style/inclusive-documentation) contains an excellent - reference on this topic. -* The above policy is not considered definitive and may be amended in the future as industry best - practices evolve. Additional comments on this topic may be provided by maintainers during code - review. - -## Submitting a PR - -* Fork the repo. -* Hack -* DCO sign-off each commit. This can be done with `git commit -s`. -* Submit your PR. -* Tests will automatically run for you. -* We will **not** merge any PR that is not passing tests. -* PRs are expected to have 100% test coverage for added code. This can be verified with a coverage - build. If your PR cannot have 100% coverage for some reason please clearly explain why when you - open it. -* Any PR that changes user-facing behavior **must** have associated documentation in the [docs](https://github.com/envoyproxy/gateway/tree/main/site) folder of the repo as - well as the [changelog](/blog/releases). -* All code comments and documentation are expected to have proper English grammar and punctuation. - If you are not a fluent English speaker (or a bad writer ;-)) please let us know and we will try - to find some help but there are no guarantees. -* Your PR title should be descriptive, and generally start with type that contains a subsystem name with `()` if necessary - and summary followed by a colon. format `chore/docs/feat/fix/refactor/style/test: summary`. - Examples: - * "docs: fix grammar error" - * "feat(translator): add new feature" - * "fix: fix xx bug" - * "chore: change ci & build tools etc" -* Your PR commit message will be used as the commit message when your PR is merged. You should - update this field if your PR diverges during review. -* Your PR description should have details on what the PR does. If it fixes an existing issue it - should end with "Fixes #XXX". -* If your PR is co-authored or based on an earlier PR from another contributor, - please attribute them with `Co-authored-by: name `. See - GitHub's [multiple author - guidance](https://help.github.com/en/github/committing-changes-to-your-project/creating-a-commit-with-multiple-authors) - for further details. -* When all tests are passing and all other conditions described herein are satisfied, a maintainer - will be assigned to review and merge the PR. -* Once you submit a PR, *please do not rebase it*. It's much easier to review if subsequent commits - are new commits and/or merges. We squash and merge so the number of commits you have in the PR - doesn't matter. -* We expect that once a PR is opened, it will be actively worked on until it is merged or closed. - We reserve the right to close PRs that are not making progress. This is generally defined as no - changes for 7 days. Obviously PRs that are closed due to lack of activity can be reopened later. - Closing stale PRs helps us to keep on top of all the work currently in flight. - -## Maintainer PR Review Policy - -* See [CODEOWNERS.md](../codeowners) for the current list of maintainers. -* A maintainer representing a different affiliation from the PR owner is required to review and - approve the PR. -* When the project matures, it is expected that a "domain expert" for the code the PR touches should - review the PR. This person does not require commit access, just domain knowledge. -* The above rules may be waived for PRs which only update docs or comments, or trivial changes to - tests and tools (where trivial is decided by the maintainer in question). -* If there is a question on who should review a PR please discuss in Slack. -* Anyone is welcome to review any PR that they want, whether they are a maintainer or not. -* Please make sure that the PR title, commit message, and description are updated if the PR changes - significantly during review. -* Please **clean up the title and body** before merging. By default, GitHub fills the squash merge - title with the original title, and the commit body with every individual commit from the PR. - The maintainer doing the merge should make sure the title follows the guidelines above and should - overwrite the body with the original commit message from the PR (cleaning it up if necessary) - while preserving the PR author's final DCO sign-off. - -## Decision making - -This is a new and complex project, and we need to make a lot of decisions very quickly. -To this end, we've settled on this process for making (possibly contentious) decisions: - -* For decisions that need a record, we create an issue. -* In that issue, we discuss opinions, then a maintainer can call for a vote in a comment. -* Maintainers can cast binding votes on that comment by reacting or replying in another comment. -* Non-maintainer community members are welcome to cast non-binding votes by either of these methods. -* Voting will be resolved by simple majority. -* In the event of deadlocks, the question will be put to steering instead. - -## DCO: Sign your work - -The sign-off is a simple line at the end of the explanation for the -patch, which certifies that you wrote it or otherwise have the right to -pass it on as an open-source patch. The rules are pretty simple: if you -can certify the below (from -[developercertificate.org](https://developercertificate.org/)): - -``` -Developer Certificate of Origin -Version 1.1 - -Copyright (C) 2004, 2006 The Linux Foundation and its contributors. -660 York Street, Suite 102, -San Francisco, CA 94110 USA - -Everyone is permitted to copy and distribute verbatim copies of this -license document, but changing it is not allowed. - - -Developer's Certificate of Origin 1.1 - -By making a contribution to this project, I certify that: - -(a) The contribution was created in whole or in part by me and I - have the right to submit it under the open source license - indicated in the file; or - -(b) The contribution is based upon previous work that, to the best - of my knowledge, is covered under an appropriate open source - license and I have the right under that license to submit that - work with modifications, whether created in whole or in part - by me, under the same open source license (unless I am - permitted to submit under a different license), as indicated - in the file; or - -(c) The contribution was provided directly to me by some other - person who certified (a), (b) or (c) and I have not modified - it. - -(d) I understand and agree that this project and the contribution - are public and that a record of the contribution (including all - personal information I submit with it, including my sign-off) is - maintained indefinitely and may be redistributed consistent with - this project or the open source license(s) involved. -``` - -then you just add a line to every git commit message: - - Signed-off-by: Joe Smith - -using your real name (sorry, no pseudonyms or anonymous contributions.) - -You can add the sign-off when creating the git commit via `git commit -s`. - -If you want this to be automatic you can set up some aliases: - -```bash -git config --add alias.amend "commit -s --amend" -git config --add alias.c "commit -s" -``` - -## Fixing DCO - -If your PR fails the DCO check, it's necessary to fix the entire commit history in the PR. Best -practice is to [squash](https://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html) -the commit history to a single commit, append the DCO sign-off as described above, and [force -push](https://git-scm.com/docs/git-push#git-push---force). For example, if you have 2 commits in -your history: - -```bash -git rebase -i HEAD^^ -(interactive squash + DCO append) -git push origin -f -``` - -Note, that in general rewriting history in this way is a hindrance to the review process and this -should only be done to correct a DCO mistake. diff --git a/site/content/en/v0.6.0/contributions/DEVELOP.md b/site/content/en/v0.6.0/contributions/DEVELOP.md deleted file mode 100644 index 6f82c4a411f..00000000000 --- a/site/content/en/v0.6.0/contributions/DEVELOP.md +++ /dev/null @@ -1,163 +0,0 @@ ---- -title: "Developer Guide" -description: "This section tells how to develop Envoy Gateway." -weight: 2 ---- - -Envoy Gateway is built using a [make][]-based build system. Our CI is based on [Github Actions][] using [workflows][]. - -## Prerequisites - -### go - -* Version: 1.20 -* Installation Guide: https://go.dev/doc/install - -### make - -* Recommended Version: 4.0 or later -* Installation Guide: https://www.gnu.org/software/make - -### docker - -* Optional when you want to build a Docker image or run `make` inside Docker. -* Recommended Version: 20.10.16 -* Installation Guide: https://docs.docker.com/engine/install - -### python3 - -* Need a `python3` program -* Must have a functioning `venv` module; this is part of the standard - library, but some distributions (such as Debian and Ubuntu) replace - it with a stub and require you to install a `python3-venv` package - separately. - -## Quickstart - -* Run `make help` to see all the available targets to build, test and run Envoy Gateway. - -### Building - -* Run `make build` to build all the binaries. -* Run `make build BINS="envoy-gateway"` to build the Envoy Gateway binary. -* Run `make build BINS="egctl"` to build the egctl binary. - -__Note:__ The binaries get generated in the `bin/$OS/$ARCH` directory, for example, `bin/linux/amd64/`. - -### Testing - -* Run `make test` to run the golang tests. - -* Run `make testdata` to generate the golden YAML testdata files. - -### Running Linters - -* Run `make lint` to make sure your code passes all the linter checks. -__Note:__ The `golangci-lint` configuration resides [here](https://github.com/envoyproxy/gateway/blob/main/tools/linter/golangci-lint/.golangci.yml). - -### Building and Pushing the Image - -* Run `IMAGE=docker.io/you/gateway-dev make image` to build the docker image. -* Run `IMAGE=docker.io/you/gateway-dev make push-multiarch` to build and push the multi-arch docker image. - -__Note:__ Replace `IMAGE` with your registry's image name. - -### Deploying Envoy Gateway for Test/Dev - -* Run `make create-cluster` to create a [Kind][] cluster. - -#### Option 1: Use the Latest [gateway-dev][] Image - -* Run `TAG=latest make kube-deploy` to deploy Envoy Gateway in the Kind cluster using the latest image. Replace `latest` - to use a different image tag. - -#### Option 2: Use a Custom Image - -* Run `make kube-install-image` to build an image from the tip of your current branch and load it in the Kind cluster. -* Run `IMAGE_PULL_POLICY=IfNotPresent make kube-deploy` to install Envoy Gateway into the Kind cluster using your custom image. - -### Deploying Envoy Gateway in Kubernetes - -* Run `TAG=latest make kube-deploy` to deploy Envoy Gateway using the latest image into a Kubernetes cluster (linked to - the current kube context). Preface the command with `IMAGE` or replace `TAG` to use a different Envoy Gateway image or - tag. -* Run `make kube-undeploy` to uninstall Envoy Gateway from the cluster. - -__Note:__ Envoy Gateway is tested against Kubernetes v1.24.0. - -### Demo Setup - -* Run `make kube-demo` to deploy a demo backend service, gatewayclass, gateway and httproute resource -(similar to steps outlined in the [Quickstart][] docs) and test the configuration. -* Run `make kube-demo-undeploy` to delete the resources created by the `make kube-demo` command. - -### Run Gateway API Conformance Tests - -The commands below deploy Envoy Gateway to a Kubernetes cluster and run the Gateway API conformance tests. Refer to the -Gateway API [conformance homepage][] to learn more about the tests. If Envoy Gateway is already installed, run -`TAG=latest make run-conformance` to run the conformance tests. - -#### On a Linux Host - -* Run `TAG=latest make conformance` to create a Kind cluster, install Envoy Gateway using the latest [gateway-dev][] - image, and run Gateway API conformance tests. - -#### On a Mac Host - -Since Mac doesn't support [directly exposing][] the Docker network to the Mac host, use one of the following -workarounds to run conformance tests: - -* Deploy your own Kubernetes cluster or use Docker Desktop with [Kubernetes support][] and then run - `TAG=latest make kube-deploy run-conformance`. This will install Envoy Gateway using the latest [gateway-dev][] image - to the Kubernetes cluster using the current kubectl context and run the conformance tests. Use `make kube-undeploy` to - uninstall Envoy Gateway. -* Install and run [Docker Mac Net Connect][mac_connect] and then run `TAG=latest make conformance`. - -__Note:__ Preface commands with `IMAGE` or replace `TAG` to use a different Envoy Gateway image or tag. If `TAG` -is unspecified, the short SHA of your current branch is used. - -### Debugging the Envoy Config - -An easy way to view the envoy config that Envoy Gateway is using is to port-forward to the admin interface port -(currently `19000`) on the Envoy deployment that corresponds to a Gateway so that it can be accessed locally. - -Get the name of the Envoy deployment. The following example is for Gateway `eg` in the `default` namespace: - -```shell -export ENVOY_DEPLOYMENT=$(kubectl get deploy -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}') -``` - -Port forward the admin interface port: - -```shell -kubectl port-forward deploy/${ENVOY_DEPLOYMENT} -n envoy-gateway-system 19000:19000 -``` - -Now you are able to view the running Envoy configuration by navigating to `127.0.0.1:19000/config_dump`. - -There are many other endpoints on the [Envoy admin interface][] that may be helpful when debugging. - -### JWT Testing - -An example [JSON Web Token (JWT)][jwt] and [JSON Web Key Set (JWKS)][jwks] are used for the [request authentication][] -user guide. The JWT was created by the [JWT Debugger][], using the `RS256` algorithm. The public key from the JWTs -verify signature was copied to [JWK Creator][] for generating the JWK. The JWK Creator was configured with matching -settings, i.e. `Signing` public key use and the `RS256` algorithm. The generated JWK was wrapped in a JWKS structure -and is hosted in the repo. - -[Quickstart]: https://github.com/envoyproxy/gateway/blob/main/docs/latest/user/quickstart.md -[make]: https://www.gnu.org/software/make/ -[Github Actions]: https://docs.github.com/en/actions -[workflows]: https://github.com/envoyproxy/gateway/tree/main/.github/workflows -[Kind]: https://kind.sigs.k8s.io/ -[conformance homepage]: https://gateway-api.sigs.k8s.io/concepts/conformance/ -[directly exposing]: https://kind.sigs.k8s.io/docs/user/loadbalancer/ -[Kubernetes support]: https://docs.docker.com/desktop/kubernetes/ -[gateway-dev]: https://hub.docker.com/r/envoyproxy/gateway-dev/tags -[mac_connect]: https://github.com/chipmk/docker-mac-net-connect -[Envoy admin interface]: https://www.envoyproxy.io/docs/envoy/latest/operations/admin#operations-admin-interface -[jwt]: https://tools.ietf.org/html/rfc7519 -[jwks]: https://tools.ietf.org/html/rfc7517 -[request authentication]: https://gateway.envoyproxy.io/latest/user/authn.html -[JWT Debugger]: https://jwt.io/ -[JWK Creator]: https://russelldavies.github.io/jwk-creator/ diff --git a/site/content/en/v0.6.0/_index.md b/site/content/en/v0.6/_index.md similarity index 100% rename from site/content/en/v0.6.0/_index.md rename to site/content/en/v0.6/_index.md diff --git a/site/content/en/v1.0.2/api/_index.md b/site/content/en/v0.6/api/_index.md similarity index 100% rename from site/content/en/v1.0.2/api/_index.md rename to site/content/en/v0.6/api/_index.md diff --git a/site/content/en/v0.6.0/api/extension_types.md b/site/content/en/v0.6/api/extension_types.md similarity index 99% rename from site/content/en/v0.6.0/api/extension_types.md rename to site/content/en/v0.6/api/extension_types.md index 08b53e29c85..351356d5fb4 100644 --- a/site/content/en/v0.6.0/api/extension_types.md +++ b/site/content/en/v0.6/api/extension_types.md @@ -780,7 +780,7 @@ _Appears in:_ | Field | Description | | --- | --- | -| `controllerName` _string_ | ControllerName defines the name of the Gateway API controller. If unspecified, defaults to "gateway.envoyproxy.io/gatewayclass-controller". See the following for additional details: https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1.GatewayClass | +| `controllerName` _string_ | ControllerName defines the name of the Gateway API controller. If unspecified, defaults to "gateway.envoyproxy.io/gatewayclass-controller". See the following for additional details: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.GatewayClass | #### GlobalRateLimit diff --git a/site/content/en/v0.6.0/contributions/CODEOWNERS.md b/site/content/en/v0.6/contributions/CODEOWNERS.md similarity index 100% rename from site/content/en/v0.6.0/contributions/CODEOWNERS.md rename to site/content/en/v0.6/contributions/CODEOWNERS.md diff --git a/site/content/en/v0.6.0/contributions/CODE_OF_CONDUCT.md b/site/content/en/v0.6/contributions/CODE_OF_CONDUCT.md similarity index 100% rename from site/content/en/v0.6.0/contributions/CODE_OF_CONDUCT.md rename to site/content/en/v0.6/contributions/CODE_OF_CONDUCT.md diff --git a/site/content/en/v0.6/contributions/CONTRIBUTING.md b/site/content/en/v0.6/contributions/CONTRIBUTING.md new file mode 100644 index 00000000000..b37898e948e --- /dev/null +++ b/site/content/en/v0.6/contributions/CONTRIBUTING.md @@ -0,0 +1,190 @@ +--- +title: "Contributing" +description: "This section tells how to contribute to Envoy Gateway." +weight: 3 +--- + +We welcome contributions from the community. Please carefully review the [project goals](/about) +and following guidelines to streamline your contributions. + +## Communication + +* Before starting work on a major feature, please contact us via GitHub or Slack. We will ensure no + one else is working on it and ask you to open a GitHub issue. +* A "major feature" is defined as any change that is > 100 LOC altered (not including tests), or + changes any user-facing behavior. We will use the GitHub issue to discuss the feature and come to + agreement. This is to prevent your time being wasted, as well as ours. The GitHub review process + for major features is also important so that [affiliations with commit access](../codeowners) can + come to agreement on the design. If it's appropriate to write a design document, the document must + be hosted either in the GitHub issue, or linked to from the issue and hosted in a world-readable + location. +* Small patches and bug fixes don't need prior communication. + +## Inclusivity + +The Envoy Gateway community has an explicit goal to be inclusive to all. As such, all PRs must adhere +to the following guidelines for all code, APIs, and documentation: + +* The following words and phrases are not allowed: + * *Whitelist*: use allowlist instead. + * *Blacklist*: use denylist or blocklist instead. + * *Master*: use primary instead. + * *Slave*: use secondary or replica instead. +* Documentation should be written in an inclusive style. The [Google developer + documentation](https://developers.google.com/style/inclusive-documentation) contains an excellent + reference on this topic. +* The above policy is not considered definitive and may be amended in the future as industry best + practices evolve. Additional comments on this topic may be provided by maintainers during code + review. + +## Submitting a PR + +* Fork the repo. +* Hack +* DCO sign-off each commit. This can be done with `git commit -s`. +* Submit your PR. +* Tests will automatically run for you. +* We will **not** merge any PR that is not passing tests. +* PRs are expected to have 100% test coverage for added code. This can be verified with a coverage + build. If your PR cannot have 100% coverage for some reason please clearly explain why when you + open it. +* Any PR that changes user-facing behavior **must** have associated documentation in the [docs](https://github.com/envoyproxy/gateway/tree/main/site) folder of the repo as + well as the [changelog](/news/releases). +* All code comments and documentation are expected to have proper English grammar and punctuation. + If you are not a fluent English speaker (or a bad writer ;-)) please let us know and we will try + to find some help but there are no guarantees. +* Your PR title should be descriptive, and generally start with type that contains a subsystem name with `()` if necessary + and summary followed by a colon. format `chore/docs/feat/fix/refactor/style/test: summary`. + Examples: + * "docs: fix grammar error" + * "feat(translator): add new feature" + * "fix: fix xx bug" + * "chore: change ci & build tools etc" +* Your PR commit message will be used as the commit message when your PR is merged. You should + update this field if your PR diverges during review. +* Your PR description should have details on what the PR does. If it fixes an existing issue it + should end with "Fixes #XXX". +* If your PR is co-authored or based on an earlier PR from another contributor, + please attribute them with `Co-authored-by: name `. See + GitHub's [multiple author + guidance](https://help.github.com/en/github/committing-changes-to-your-project/creating-a-commit-with-multiple-authors) + for further details. +* When all tests are passing and all other conditions described herein are satisfied, a maintainer + will be assigned to review and merge the PR. +* Once you submit a PR, *please do not rebase it*. It's much easier to review if subsequent commits + are new commits and/or merges. We squash and merge so the number of commits you have in the PR + doesn't matter. +* We expect that once a PR is opened, it will be actively worked on until it is merged or closed. + We reserve the right to close PRs that are not making progress. This is generally defined as no + changes for 7 days. Obviously PRs that are closed due to lack of activity can be reopened later. + Closing stale PRs helps us to keep on top of all the work currently in flight. + +## Maintainer PR Review Policy + +* See [CODEOWNERS.md](../codeowners) for the current list of maintainers. +* A maintainer representing a different affiliation from the PR owner is required to review and + approve the PR. +* When the project matures, it is expected that a "domain expert" for the code the PR touches should + review the PR. This person does not require commit access, just domain knowledge. +* The above rules may be waived for PRs which only update docs or comments, or trivial changes to + tests and tools (where trivial is decided by the maintainer in question). +* If there is a question on who should review a PR please discuss in Slack. +* Anyone is welcome to review any PR that they want, whether they are a maintainer or not. +* Please make sure that the PR title, commit message, and description are updated if the PR changes + significantly during review. +* Please **clean up the title and body** before merging. By default, GitHub fills the squash merge + title with the original title, and the commit body with every individual commit from the PR. + The maintainer doing the merge should make sure the title follows the guidelines above and should + overwrite the body with the original commit message from the PR (cleaning it up if necessary) + while preserving the PR author's final DCO sign-off. + +## Decision making + +This is a new and complex project, and we need to make a lot of decisions very quickly. +To this end, we've settled on this process for making (possibly contentious) decisions: + +* For decisions that need a record, we create an issue. +* In that issue, we discuss opinions, then a maintainer can call for a vote in a comment. +* Maintainers can cast binding votes on that comment by reacting or replying in another comment. +* Non-maintainer community members are welcome to cast non-binding votes by either of these methods. +* Voting will be resolved by simple majority. +* In the event of deadlocks, the question will be put to steering instead. + +## DCO: Sign your work + +The sign-off is a simple line at the end of the explanation for the +patch, which certifies that you wrote it or otherwise have the right to +pass it on as an open-source patch. The rules are pretty simple: if you +can certify the below (from +[developercertificate.org](https://developercertificate.org/)): + +``` +Developer Certificate of Origin +Version 1.1 + +Copyright (C) 2004, 2006 The Linux Foundation and its contributors. +660 York Street, Suite 102, +San Francisco, CA 94110 USA + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + + +Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +(b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. +``` + +then you just add a line to every git commit message: + + Signed-off-by: Joe Smith + +using your real name (sorry, no pseudonyms or anonymous contributions.) + +You can add the sign-off when creating the git commit via `git commit -s`. + +If you want this to be automatic you can set up some aliases: + +```bash +git config --add alias.amend "commit -s --amend" +git config --add alias.c "commit -s" +``` + +## Fixing DCO + +If your PR fails the DCO check, it's necessary to fix the entire commit history in the PR. Best +practice is to [squash](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges#squash-and-merge-your-commits) +the commit history to a single commit, append the DCO sign-off as described above, and [force +push](https://git-scm.com/docs/git-push#git-push---force). For example, if you have 2 commits in +your history: + +```bash +git rebase -i HEAD^^ +(interactive squash + DCO append) +git push origin -f +``` + +Note, that in general rewriting history in this way is a hindrance to the review process and this +should only be done to correct a DCO mistake. diff --git a/site/content/en/v0.4.0/contributions/DEVELOP.md b/site/content/en/v0.6/contributions/DEVELOP.md similarity index 98% rename from site/content/en/v0.4.0/contributions/DEVELOP.md rename to site/content/en/v0.6/contributions/DEVELOP.md index 6f82c4a411f..366524eb9c7 100644 --- a/site/content/en/v0.4.0/contributions/DEVELOP.md +++ b/site/content/en/v0.6/contributions/DEVELOP.md @@ -158,6 +158,6 @@ and is hosted in the repo. [Envoy admin interface]: https://www.envoyproxy.io/docs/envoy/latest/operations/admin#operations-admin-interface [jwt]: https://tools.ietf.org/html/rfc7519 [jwks]: https://tools.ietf.org/html/rfc7517 -[request authentication]: https://gateway.envoyproxy.io/latest/user/authn.html +[request authentication]: ../user/jwt-authentication [JWT Debugger]: https://jwt.io/ [JWK Creator]: https://russelldavies.github.io/jwk-creator/ diff --git a/site/content/en/v0.6.0/contributions/DOCS.md b/site/content/en/v0.6/contributions/DOCS.md similarity index 100% rename from site/content/en/v0.6.0/contributions/DOCS.md rename to site/content/en/v0.6/contributions/DOCS.md diff --git a/site/content/en/v0.6.0/contributions/RELEASING.md b/site/content/en/v0.6/contributions/RELEASING.md similarity index 97% rename from site/content/en/v0.6.0/contributions/RELEASING.md rename to site/content/en/v0.6/contributions/RELEASING.md index 50d2db76abd..5abb7ba4503 100644 --- a/site/content/en/v0.6.0/contributions/RELEASING.md +++ b/site/content/en/v0.6/contributions/RELEASING.md @@ -6,7 +6,10 @@ description: "This section tells the release process of Envoy Gateway." This document guides maintainers through the process of creating an Envoy Gateway release. - [Release Candidate](#release-candidate) + - [Prerequisites](#prerequisites) + - [Setup cherry picker action](#setup-cherry-picker-action) - [Minor Release](#minor-release) + - [Prerequisites](#prerequisites-1) - [Announce the Release](#announce-the-release) ## Release Candidate @@ -73,7 +76,7 @@ export GITHUB_REMOTE=origin ### Setup cherry picker action -After release branch cut, RM (Release Manager) should add job [cherrypick action](../../../.github/workflows/cherrypick.yaml) for target release. +After release branch cut, RM (Release Manager) should add job [cherrypick action](https://github.com/envoyproxy/gateway/blob/main/.github/workflows/cherrypick.yaml) for target release. Configuration looks like following: diff --git a/site/content/en/v0.6.0/contributions/_index.md b/site/content/en/v0.6/contributions/_index.md similarity index 100% rename from site/content/en/v0.6.0/contributions/_index.md rename to site/content/en/v0.6/contributions/_index.md diff --git a/site/content/en/v0.6.0/contributions/roadmap.md b/site/content/en/v0.6/contributions/roadmap.md similarity index 100% rename from site/content/en/v0.6.0/contributions/roadmap.md rename to site/content/en/v0.6/contributions/roadmap.md diff --git a/site/content/en/v0.6.0/design/_index.md b/site/content/en/v0.6/design/_index.md similarity index 100% rename from site/content/en/v0.6.0/design/_index.md rename to site/content/en/v0.6/design/_index.md diff --git a/site/content/en/v0.6.0/design/accesslog.md b/site/content/en/v0.6/design/accesslog.md similarity index 100% rename from site/content/en/v0.6.0/design/accesslog.md rename to site/content/en/v0.6/design/accesslog.md diff --git a/site/content/en/v0.6.0/design/backend-traffic-policy.md b/site/content/en/v0.6/design/backend-traffic-policy.md similarity index 100% rename from site/content/en/v0.6.0/design/backend-traffic-policy.md rename to site/content/en/v0.6/design/backend-traffic-policy.md diff --git a/site/content/en/v0.6.0/design/bootstrap.md b/site/content/en/v0.6/design/bootstrap.md similarity index 99% rename from site/content/en/v0.6.0/design/bootstrap.md rename to site/content/en/v0.6/design/bootstrap.md index c0581347a24..743a0d9bad8 100644 --- a/site/content/en/v0.6.0/design/bootstrap.md +++ b/site/content/en/v0.6/design/bootstrap.md @@ -376,6 +376,6 @@ spec: ``` [Issue 31]: https://github.com/envoyproxy/gateway/issues/31 -[EnvoyProxy]: ../../api/extension_types#envoyproxy +[EnvoyProxy]: ../api/extension_types#envoyproxy [GatewayClass]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.GatewayClass [parametersRef]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.ParametersReference diff --git a/site/content/en/v0.6.0/design/client-traffic-policy.md b/site/content/en/v0.6/design/client-traffic-policy.md similarity index 100% rename from site/content/en/v0.6.0/design/client-traffic-policy.md rename to site/content/en/v0.6/design/client-traffic-policy.md diff --git a/site/content/en/v0.6.0/design/config-api.md b/site/content/en/v0.6/design/config-api.md similarity index 99% rename from site/content/en/v0.6.0/design/config-api.md rename to site/content/en/v0.6/design/config-api.md index 1c6f3057848..89b7b0d838a 100644 --- a/site/content/en/v0.6.0/design/config-api.md +++ b/site/content/en/v0.6/design/config-api.md @@ -88,7 +88,7 @@ type Gateway struct { // defaults to "gateway.envoyproxy.io/gatewayclass-controller". See the following // for additional details: // - // https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.GatewayClass + // https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.GatewayClass // // +optional ControllerName string `json:"controllerName,omitempty"` diff --git a/site/content/en/v0.6.0/design/eg-metrics.md b/site/content/en/v0.6/design/eg-metrics.md similarity index 100% rename from site/content/en/v0.6.0/design/eg-metrics.md rename to site/content/en/v0.6/design/eg-metrics.md diff --git a/site/content/en/v0.6.0/design/egctl.md b/site/content/en/v0.6/design/egctl.md similarity index 100% rename from site/content/en/v0.6.0/design/egctl.md rename to site/content/en/v0.6/design/egctl.md diff --git a/site/content/en/v0.6.0/design/envoy-patch-policy.md b/site/content/en/v0.6/design/envoy-patch-policy.md similarity index 95% rename from site/content/en/v0.6.0/design/envoy-patch-policy.md rename to site/content/en/v0.6/design/envoy-patch-policy.md index 83ccb035b4d..1aa441ef114 100644 --- a/site/content/en/v0.6.0/design/envoy-patch-policy.md +++ b/site/content/en/v0.6/design/envoy-patch-policy.md @@ -167,10 +167,10 @@ patches will work. [Gateway API]: https://gateway-api.sigs.k8s.io/ [Kubernetes]: https://kubernetes.io/docs/tasks/manage-kubernetes-objects/update-api-object-kubectl-patch/ [Kustomize]: https://github.com/kubernetes-sigs/kustomize/blob/master/examples/jsonpatch.md -[Extension APIs]: ../../api/extension_types/ -[RateLimit]: ../../user/rate-limit/ -[EnvoyGateway]: ../../api/extension_types#envoygateway +[Extension APIs]: ../api/extension_types +[RateLimit]: ../user/rate-limit +[EnvoyGateway]: ../api/extension_types#envoygateway [Extending the Control Plane]: ../extending-envoy-gateway/ [EnvoyFilter]: https://istio.io/latest/docs/reference/config/networking/envoy-filter -[egctl x translate]: ../../user/egctl#egctl-experimental-translate -[Bootstrap configuration using EnvoyProxy API]: ../../user/customize-envoyproxy#customize-envoyproxy-bootstrap-config +[egctl x translate]: ../user/egctl#egctl-experimental-translate +[Bootstrap configuration using EnvoyProxy API]: ../user/customize-envoyproxy#customize-envoyproxy-bootstrap-config diff --git a/site/content/en/v0.6.0/design/extending-envoy-gateway.md b/site/content/en/v0.6/design/extending-envoy-gateway.md similarity index 99% rename from site/content/en/v0.6.0/design/extending-envoy-gateway.md rename to site/content/en/v0.6/design/extending-envoy-gateway.md index 104bd804542..4976b16754e 100644 --- a/site/content/en/v0.6.0/design/extending-envoy-gateway.md +++ b/site/content/en/v0.6/design/extending-envoy-gateway.md @@ -315,11 +315,11 @@ Extending Envoy Gateway by using an external extension server which makes use of [Envoy]: https://www.envoyproxy.io/ [Envoy specific configuration (xDS)]: https://www.envoyproxy.io/docs/envoy/v1.25.1/configuration/configuration [v1]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1 -[rate limiting]: ../../user/rate-limit/ -[authentication]: ../../user/jwt-authentication/ +[rate limiting]: ../user/rate-limit +[authentication]: ../user/jwt-authentication [HTTPRoute]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRoute [GRPCRoute]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.GRPCRoute -[EnvoyGateway config]: ../../api/extension_types#envoygateway +[EnvoyGateway config]: ../api/extension_types#envoygateway [controller-runtime]: https://github.com/kubernetes-sigs/controller-runtime [Unstructured]: https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured [Listener]: https://www.envoyproxy.io/docs/envoy/v1.23.0/api-v3/config/listener/v3/listener.proto#config-listener-v3-listener diff --git a/site/content/en/v0.6.0/design/gatewayapi-translator.md b/site/content/en/v0.6/design/gatewayapi-translator.md similarity index 100% rename from site/content/en/v0.6.0/design/gatewayapi-translator.md rename to site/content/en/v0.6/design/gatewayapi-translator.md diff --git a/site/content/en/v0.6.0/design/goals.md b/site/content/en/v0.6/design/goals.md similarity index 100% rename from site/content/en/v0.6.0/design/goals.md rename to site/content/en/v0.6/design/goals.md diff --git a/site/content/en/v0.6.0/design/local-envoy-gateway.md b/site/content/en/v0.6/design/local-envoy-gateway.md similarity index 100% rename from site/content/en/v0.6.0/design/local-envoy-gateway.md rename to site/content/en/v0.6/design/local-envoy-gateway.md diff --git a/site/content/en/v0.6.0/design/metrics.md b/site/content/en/v0.6/design/metrics.md similarity index 100% rename from site/content/en/v0.6.0/design/metrics.md rename to site/content/en/v0.6/design/metrics.md diff --git a/site/content/en/v0.6.0/design/pprof.md b/site/content/en/v0.6/design/pprof.md similarity index 100% rename from site/content/en/v0.6.0/design/pprof.md rename to site/content/en/v0.6/design/pprof.md diff --git a/site/content/en/v0.6.0/design/rate-limit.md b/site/content/en/v0.6/design/rate-limit.md similarity index 100% rename from site/content/en/v0.6.0/design/rate-limit.md rename to site/content/en/v0.6/design/rate-limit.md diff --git a/site/content/en/v0.6.0/design/security-policy.md b/site/content/en/v0.6/design/security-policy.md similarity index 100% rename from site/content/en/v0.6.0/design/security-policy.md rename to site/content/en/v0.6/design/security-policy.md diff --git a/site/content/en/v0.6.0/design/system-design.md b/site/content/en/v0.6/design/system-design.md similarity index 97% rename from site/content/en/v0.6.0/design/system-design.md rename to site/content/en/v0.6/design/system-design.md index 956482ffcc3..c64846873b1 100644 --- a/site/content/en/v0.6.0/design/system-design.md +++ b/site/content/en/v0.6/design/system-design.md @@ -159,11 +159,11 @@ The draft for this document is [here][draft_design]. [grl]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_features/global_rate_limiting [rls]: https://github.com/envoyproxy/ratelimit [rlf]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/ratelimit/v3/rate_limit.proto#envoy-v3-api-msg-extensions-filters-http-ratelimit-v3-ratelimit -[crf]: https://gateway-api.sigs.k8s.io/v1alpha2/api-types/httproute/#filters-optional +[crf]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRoute#filters-optional [gwapi_conflicts]: https://gateway-api.sigs.k8s.io/concepts/guidelines/#conflicts [listener]: https://www.envoyproxy.io/docs/envoy/latest/configuration/listeners/listeners#config-listeners [route]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#envoy-v3-api-msg-config-route-v3-route -[be_ref]: https://gateway-api.sigs.k8s.io/v1alpha2/api-types/httproute/#backendrefs-optional +[be_ref]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRoute#backendrefs-optional [cluster]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/cluster.proto#config-cluster-v3-cluster [draft_design]: https://docs.google.com/document/d/1riyTPPYuvNzIhBdrAX8dpfxTmcobWZDSYTTB5NeybuY/edit [cr]: https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/ diff --git a/site/content/en/v0.6.0/design/tcp-udp-design.md b/site/content/en/v0.6/design/tcp-udp-design.md similarity index 100% rename from site/content/en/v0.6.0/design/tcp-udp-design.md rename to site/content/en/v0.6/design/tcp-udp-design.md diff --git a/site/content/en/v0.6.0/design/tracing.md b/site/content/en/v0.6/design/tracing.md similarity index 100% rename from site/content/en/v0.6.0/design/tracing.md rename to site/content/en/v0.6/design/tracing.md diff --git a/site/content/en/v0.6.0/design/watching.md b/site/content/en/v0.6/design/watching.md similarity index 100% rename from site/content/en/v0.6.0/design/watching.md rename to site/content/en/v0.6/design/watching.md diff --git a/site/content/en/v1.0.2/install/_index.md b/site/content/en/v0.6/install/_index.md similarity index 100% rename from site/content/en/v1.0.2/install/_index.md rename to site/content/en/v0.6/install/_index.md diff --git a/site/content/en/v0.6.0/install/api.md b/site/content/en/v0.6/install/api.md similarity index 100% rename from site/content/en/v0.6.0/install/api.md rename to site/content/en/v0.6/install/api.md diff --git a/site/content/en/v0.6.0/install/install-egctl.md b/site/content/en/v0.6/install/install-egctl.md similarity index 100% rename from site/content/en/v0.6.0/install/install-egctl.md rename to site/content/en/v0.6/install/install-egctl.md diff --git a/site/content/en/v0.6.0/install/install-helm.md b/site/content/en/v0.6/install/install-helm.md similarity index 97% rename from site/content/en/v0.6.0/install/install-helm.md rename to site/content/en/v0.6/install/install-helm.md index 7bb4b63952b..37c1c649152 100644 --- a/site/content/en/v0.6.0/install/install-helm.md +++ b/site/content/en/v0.6/install/install-helm.md @@ -10,7 +10,7 @@ Envoy Gateway can be installed via a Helm chart with a few simple steps, dependi ## Before you begin {{% alert title="Compatibility Matrix" color="warning" %}} -Refer to the [Version Compatibility Matrix](/blog/2022/10/01/versions/) to learn more. +Refer to the [Version Compatibility Matrix](/news/releases/matrix/) to learn more. {{% /alert %}} The Envoy Gateway Helm chart is hosted by DockerHub. @@ -140,5 +140,5 @@ These are the ports used by Envoy Gateway and the managed Envoy Proxy. | Heath Check | 0.0.0.0 | 19001 | {{% alert title="Next Steps" color="warning" %}} -Envoy Gateway should now be successfully installed and running, but in order to experience more abilities of Envoy Gateway, you can refer to [User Guides](../../user). +Envoy Gateway should now be successfully installed and running, but in order to experience more abilities of Envoy Gateway, you can refer to [User Guides](../user). {{% /alert %}} diff --git a/site/content/en/v0.6.0/install/install-yaml.md b/site/content/en/v0.6/install/install-yaml.md similarity index 90% rename from site/content/en/v0.6.0/install/install-yaml.md rename to site/content/en/v0.6/install/install-yaml.md index 0b617d34be6..e00cf0f2733 100644 --- a/site/content/en/v0.6.0/install/install-yaml.md +++ b/site/content/en/v0.6/install/install-yaml.md @@ -17,7 +17,7 @@ Envoy Gateway is designed to run in Kubernetes for production. The most essentia * The `kubectl` command-line tool {{% alert title="Compatibility Matrix" color="warning" %}} -Refer to the [Version Compatibility Matrix](/blog/2022/10/01/versions/) to learn more. +Refer to the [Version Compatibility Matrix](/news/releases/matrix/) to learn more. {{% /alert %}} ## Install with YAML @@ -36,4 +36,4 @@ Refer to the [Developer Guide](/contributions/develop) to learn more. 2. Next Steps - Envoy Gateway should now be successfully installed and running, but in order to experience more abilities of Envoy Gateway, you can refer to [User Guides](/latest/user). + Envoy Gateway should now be successfully installed and running, but in order to experience more abilities of Envoy Gateway, you can refer to [User Guides](../user). diff --git a/site/content/en/v0.6.0/user/_index.md b/site/content/en/v0.6/user/_index.md similarity index 100% rename from site/content/en/v0.6.0/user/_index.md rename to site/content/en/v0.6/user/_index.md diff --git a/site/content/en/v0.6.0/user/cors.md b/site/content/en/v0.6/user/cors.md similarity index 92% rename from site/content/en/v0.6.0/user/cors.md rename to site/content/en/v0.6/user/cors.md index d8867ccb8d2..4c7d694e0a7 100644 --- a/site/content/en/v0.6.0/user/cors.md +++ b/site/content/en/v0.6/user/cors.md @@ -11,7 +11,7 @@ This instantiated resource can be linked to a [Gateway][Gateway], [HTTPRoute][HT ## Prerequisites -Follow the steps from the [Quickstart](../quickstart) guide to install Envoy Gateway and the example manifest. +Follow the steps from the [Quickstart](./quickstart) guide to install Envoy Gateway and the example manifest. Before proceeding, you should be able to query the example backend using HTTP. ## Configuration @@ -53,7 +53,7 @@ kubectl get securitypolicy/cors-example -o yaml ## Testing -Ensure the `GATEWAY_HOST` environment variable from the [Quickstart](../quickstart) guide is set. If not, follow the +Ensure the `GATEWAY_HOST` environment variable from the [Quickstart](./quickstart) guide is set. If not, follow the Quickstart instructions to set the variable. ```shell @@ -102,7 +102,7 @@ its configuration. It won't deny any requests. The browsers are responsible for ## Clean-Up -Follow the steps from the [Quickstart](../quickstart) guide to uninstall Envoy Gateway and the example manifest. +Follow the steps from the [Quickstart](./quickstart) guide to uninstall Envoy Gateway and the example manifest. Delete the SecurityPolicy: diff --git a/site/content/en/v0.6.0/user/customize-envoyproxy.md b/site/content/en/v0.6/user/customize-envoyproxy.md similarity index 97% rename from site/content/en/v0.6.0/user/customize-envoyproxy.md rename to site/content/en/v0.6/user/customize-envoyproxy.md index 7f9bfb7cdc0..5835bd73b6c 100644 --- a/site/content/en/v0.6.0/user/customize-envoyproxy.md +++ b/site/content/en/v0.6/user/customize-envoyproxy.md @@ -8,7 +8,7 @@ Service. To learn more about GatewayClass and ParametersRef, please refer to [Ga ## Installation -Follow the steps from the [Quickstart Guide](../quickstart) to install Envoy Gateway and the example manifest. +Follow the steps from the [Quickstart Guide](./quickstart) to install Envoy Gateway and the example manifest. Before proceeding, you should be able to query the example backend using HTTP. ## Add GatewayClass ParametersRef @@ -318,5 +318,5 @@ Any errors in the configuration will be surfaced as status within the `GatewayCl You can also validate this configuration using [egctl translate][]. [Gateway API documentation]: https://gateway-api.sigs.k8s.io/ -[EnvoyProxy]: ../../api/extension_types#envoyproxy -[egctl translate]: ../egctl#validating-gateway-api-configuration +[EnvoyProxy]: ../api/extension_types#envoyproxy +[egctl translate]: ./egctl#validating-gateway-api-configuration diff --git a/site/content/en/v0.6.0/user/deployment-mode.md b/site/content/en/v0.6/user/deployment-mode.md similarity index 100% rename from site/content/en/v0.6.0/user/deployment-mode.md rename to site/content/en/v0.6/user/deployment-mode.md diff --git a/site/content/en/v0.6.0/user/egctl.md b/site/content/en/v0.6/user/egctl.md similarity index 99% rename from site/content/en/v0.6.0/user/egctl.md rename to site/content/en/v0.6/user/egctl.md index 937c783cc02..999ccc82903 100644 --- a/site/content/en/v0.6.0/user/egctl.md +++ b/site/content/en/v0.6/user/egctl.md @@ -396,7 +396,7 @@ spec: EOF ``` -You can see the output contains a [EnvoyProxy](../../api/extension_types#envoyproxy) resource that +You can see the output contains a [EnvoyProxy](../api/extension_types#envoyproxy) resource that can be used as a starting point to modify the xDS bootstrap resource for the managed Envoy Proxy fleet. ```yaml diff --git a/site/content/en/v0.6.0/user/envoy-patch-policy.md b/site/content/en/v0.6/user/envoy-patch-policy.md similarity index 93% rename from site/content/en/v0.6.0/user/envoy-patch-policy.md rename to site/content/en/v0.6/user/envoy-patch-policy.md index 9bf6459d2cd..da09f4a3263 100644 --- a/site/content/en/v0.6.0/user/envoy-patch-policy.md +++ b/site/content/en/v0.6/user/envoy-patch-policy.md @@ -22,7 +22,7 @@ not exposed by Envoy Gateway APIs today. ### Prerequisites -* Follow the steps from the [Quickstart](../quickstart) guide to install Envoy Gateway and the example manifest. +* Follow the steps from the [Quickstart](./quickstart) guide to install Envoy Gateway and the example manifest. Before proceeding, you should be able to query the example backend using HTTP. ### Enable EnvoyPatchPolicy @@ -194,9 +194,9 @@ across versions for these reasons * Envoy Gateway might alter the xDS translation creating a different xDS output such as changing the `name` field of resources. -[EnvoyPatchPolicy]: ../../api/extension_types#envoypatchpolicy -[EnvoyGateway]: ../../api/extension_types#envoygateway +[EnvoyPatchPolicy]: ../api/extension_types#envoypatchpolicy +[EnvoyGateway]: ../api/extension_types#envoygateway [JSON Patch]: https://datatracker.ietf.org/doc/html/rfc6902 -[xDS]: https://www.envoyproxy.io/docs/envoy/v0.6.0/intro/arch_overview/operations/dynamic_configuration -[Local Reply Modification]: https://www.envoyproxy.io/docs/envoy/v0.6.0/configuration/http/http_conn_man/local_reply -[egctl x translate]: ../egctl#egctl-experimental-translate +[xDS]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/operations/dynamic_configuration +[Local Reply Modification]: https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/local_reply +[egctl x translate]: ./egctl#egctl-experimental-translate diff --git a/site/content/en/v0.6.0/user/gateway-address.md b/site/content/en/v0.6/user/gateway-address.md similarity index 100% rename from site/content/en/v0.6.0/user/gateway-address.md rename to site/content/en/v0.6/user/gateway-address.md diff --git a/site/content/en/v0.6.0/user/gateway-api-metrics.md b/site/content/en/v0.6/user/gateway-api-metrics.md similarity index 91% rename from site/content/en/v0.6.0/user/gateway-api-metrics.md rename to site/content/en/v0.6/user/gateway-api-metrics.md index fef51bde69f..3f787e33a50 100644 --- a/site/content/en/v0.6.0/user/gateway-api-metrics.md +++ b/site/content/en/v0.6/user/gateway-api-metrics.md @@ -7,7 +7,7 @@ The project also provides example dashboard for visualising the metrics using Gr ## Prerequisites -Follow the steps from the [Quickstart Guide](../quickstart) to install Envoy Gateway and the example manifest. +Follow the steps from the [Quickstart Guide](./quickstart) to install Envoy Gateway and the example manifest. Before proceeding, you should be able to query the example backend using HTTP. Run the following commands to install the metrics stack, with the Gateway API State Metrics configuration, on your kubernetes cluster: @@ -29,7 +29,7 @@ kubectl -n monitoring rollout status --watch --timeout=5m statefulset/prometheus kubectl -n monitoring port-forward service/prometheus-k8s 9090:9090 > /dev/null & ``` -Navigate to [http://localhost:9090](http://localhost:9090). +Navigate to `http://localhost:9090`. Metrics can be queried from the 'Graph' tab e.g. `gatewayapi_gateway_created` See the [Gateway API State Metrics README](https://github.com/Kuadrant/gateway-api-state-metrics/tree/main#metrics) for the full list of Gateway API metrics available. @@ -47,7 +47,7 @@ kubectl -n monitoring wait --timeout=5m deployment/grafana --for=condition=Avail kubectl -n monitoring port-forward service/grafana 3000:3000 > /dev/null & ``` -Navigate to [http://localhost:3000](http://localhost:3000) and sign in with admin/admin. +Navigate to `http://localhost:3000` and sign in with admin/admin. The Gateway API State dashboards will be available in the 'Default' folder and tagged with 'gateway-api'. See the [Gateway API State Metrics README](https://github.com/Kuadrant/gateway-api-state-metrics/tree/main#dashboards) for further information on available dashboards. diff --git a/site/content/en/v0.6.0/user/gatewayapi-support.md b/site/content/en/v0.6/user/gatewayapi-support.md similarity index 98% rename from site/content/en/v0.6.0/user/gatewayapi-support.md rename to site/content/en/v0.6/user/gatewayapi-support.md index 064bb5b2b1b..107edf886cc 100644 --- a/site/content/en/v0.6.0/user/gatewayapi-support.md +++ b/site/content/en/v0.6/user/gatewayapi-support.md @@ -110,9 +110,9 @@ these types of cross-namespace references. Envoy Gateway supports the following [TLSRoute]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.TLSRoute [ReferenceGrant]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.ReferenceGrant [SecretObjectReference]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.SecretObjectReference -[rate limiting]: ../rate-limit/ -[request authentication]: ../jwt-authentication/ -[EnvoyProxy]: ../../api/extension_types#envoyproxy +[rate limiting]: ./rate-limit +[request authentication]: ./jwt-authentication +[EnvoyProxy]: ../api/extension_types#envoyproxy [resolving conflicts]: https://gateway-api.sigs.k8s.io/concepts/guidelines/?h=conflict#conflicts [ExtensionRefs]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRouteFilterType [grpc-filter]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.GRPCRouteFilter diff --git a/site/content/en/v0.6.0/user/grafana-integration.md b/site/content/en/v0.6/user/grafana-integration.md similarity index 71% rename from site/content/en/v0.6.0/user/grafana-integration.md rename to site/content/en/v0.6/user/grafana-integration.md index ec2d9ce5c70..62ba4ceaf85 100644 --- a/site/content/en/v0.6.0/user/grafana-integration.md +++ b/site/content/en/v0.6/user/grafana-integration.md @@ -7,7 +7,7 @@ This guide shows you how to visualise the metrics exposed to prometheus using gr ## Prerequisites -Follow the steps from the [Quickstart Guide](../quickstart) to install Envoy Gateway and the example manifest. +Follow the steps from the [Quickstart Guide](./quickstart) to install Envoy Gateway and the example manifest. Before proceeding, you should be able to query the example backend using HTTP. Follow the steps from the [Proxy Observability](../proxy-observability#Metrics) to enable prometheus metrics. @@ -40,7 +40,7 @@ GRAFANA_IP=$(kubectl get svc grafana -n monitoring -o jsonpath='{.status.loadBal To visualise metrics from Prometheus, we have to connect Grafana with Prometheus. If you installed Grafana from the command from prerequisites sections, the prometheus datasource should be already configured. -You can also add the data source manually by following the instructions from [Grafana Docs](https://grafana.com/docs/grafana/v0.6.0/datasources/prometheus/configure-prometheus-data-source/). +You can also add the data source manually by following the instructions from [Grafana Docs](https://grafana.com/docs/grafana/latest/datasources/prometheus/configure-prometheus-data-source/). ## Accessing Grafana @@ -50,16 +50,16 @@ To log in to Grafana, use the credentials `admin:admin`. Envoy Gateway has examples of dashboard for you to get started: -### [Envoy Global](https://raw.githubusercontent.com/envoyproxy/gateway/main/charts/gateway-addons-helm/dashboards/envoy-global.json) +### Envoy Proxy Global -![Envoy Global](/img/envoy-global-dashboard.png) +![Envoy Proxy Global](/img/envoy-proxy-global-dashboard.png) -### [Envoy Clusters](https://raw.githubusercontent.com/envoyproxy/gateway/main/charts/gateway-addons-helm/dashboards/envoy-clusters.json) +### Envoy Clusters ![Envoy Clusters](/img/envoy-clusters-dashboard.png) -### [Envoy Pod Resources](https://raw.githubusercontent.com/envoyproxy/gateway/main/charts/gateway-addons-helm/dashboards/envoy-pod-resource.json) +### Envoy Pod Resources -![Envoy Pod Resources](/img/envoy-pod-resources-dashboard.png) +![Envoy Pod Resources](/img/resources-monitor-dashboard.png) -You can load the above dashboards in your Grafana to get started. Please refer to Grafana docs for [importing dashboards](https://grafana.com/docs/grafana/v0.6.0/dashboards/manage-dashboards/#import-a-dashboard). +You can load the above dashboards in your Grafana to get started. Please refer to Grafana docs for [importing dashboards](https://grafana.com/docs/grafana/latest/dashboards/manage-dashboards/#import-a-dashboard). diff --git a/site/content/en/v0.6.0/user/grpc-routing.md b/site/content/en/v0.6/user/grpc-routing.md similarity index 100% rename from site/content/en/v0.6.0/user/grpc-routing.md rename to site/content/en/v0.6/user/grpc-routing.md diff --git a/site/content/en/v0.6.0/user/http-redirect.md b/site/content/en/v0.6/user/http-redirect.md similarity index 98% rename from site/content/en/v0.6.0/user/http-redirect.md rename to site/content/en/v0.6/user/http-redirect.md index 1ef88e53fa3..2fec0b521a9 100644 --- a/site/content/en/v0.6.0/user/http-redirect.md +++ b/site/content/en/v0.6/user/http-redirect.md @@ -9,7 +9,7 @@ learn more about HTTP routing, refer to the [Gateway API documentation][]. ## Prerequisites -Follow the steps from the [Quickstart](../quickstart) to install Envoy Gateway and the example manifest. +Follow the steps from the [Quickstart](./quickstart) to install Envoy Gateway and the example manifest. Before proceeding, you should be able to query the example backend using HTTPS. ## Redirects diff --git a/site/content/en/v0.6.0/user/http-request-headers.md b/site/content/en/v0.6/user/http-request-headers.md similarity index 98% rename from site/content/en/v0.6.0/user/http-request-headers.md rename to site/content/en/v0.6/user/http-request-headers.md index 25b675a16d7..6372aacbf02 100644 --- a/site/content/en/v0.6.0/user/http-request-headers.md +++ b/site/content/en/v0.6/user/http-request-headers.md @@ -14,7 +14,7 @@ client. ## Prerequisites -Follow the steps from the [Quickstart Guide](../quickstart) to install Envoy Gateway and the example manifest. +Follow the steps from the [Quickstart Guide](./quickstart) to install Envoy Gateway and the example manifest. Before proceeding, you should be able to query the example backend using HTTP. ## Adding Request Headers diff --git a/site/content/en/v0.6.0/user/http-request-mirroring.md b/site/content/en/v0.6/user/http-request-mirroring.md similarity index 99% rename from site/content/en/v0.6.0/user/http-request-mirroring.md rename to site/content/en/v0.6/user/http-request-mirroring.md index db6bcca33f2..9f2aac4b6ce 100644 --- a/site/content/en/v0.6.0/user/http-request-mirroring.md +++ b/site/content/en/v0.6/user/http-request-mirroring.md @@ -244,7 +244,7 @@ EOF Error from server: error when creating "STDIN": admission webhook "validate.gateway.networking.k8s.io" denied the request: spec.rules[0].filters: Invalid value: "RequestMirror": cannot be used multiple times in the same rule ``` -[Quickstart Guide]: ../quickstart/ +[Quickstart Guide]: ./quickstart/ [HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute/ [backendRefs]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.BackendRef [HTTPRequestMirrorFilter]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRequestMirrorFilter diff --git a/site/content/en/v0.6.0/user/http-response-headers.md b/site/content/en/v0.6/user/http-response-headers.md similarity index 98% rename from site/content/en/v0.6.0/user/http-response-headers.md rename to site/content/en/v0.6/user/http-response-headers.md index 76cf33362b4..1a3ab351ebf 100644 --- a/site/content/en/v0.6.0/user/http-response-headers.md +++ b/site/content/en/v0.6/user/http-response-headers.md @@ -12,7 +12,7 @@ upstream service. ## Prerequisites -Follow the steps from the [Quickstart Guide](../quickstart) to install Envoy Gateway and the example manifest. +Follow the steps from the [Quickstart Guide](./quickstart) to install Envoy Gateway and the example manifest. Before proceeding, you should be able to query the example backend using HTTP. ## Adding Response Headers diff --git a/site/content/en/v0.6.0/user/http-routing.md b/site/content/en/v0.6/user/http-routing.md similarity index 100% rename from site/content/en/v0.6.0/user/http-routing.md rename to site/content/en/v0.6/user/http-routing.md diff --git a/site/content/en/v0.6.0/user/http-traffic-splitting.md b/site/content/en/v0.6/user/http-traffic-splitting.md similarity index 98% rename from site/content/en/v0.6.0/user/http-traffic-splitting.md rename to site/content/en/v0.6/user/http-traffic-splitting.md index 49e6ca89c2f..6d3257af415 100644 --- a/site/content/en/v0.6.0/user/http-traffic-splitting.md +++ b/site/content/en/v0.6/user/http-traffic-splitting.md @@ -8,7 +8,7 @@ with status code `500` for all requests that would have been sent to that backen ## Installation -Follow the steps from the [Quickstart Guide](../quickstart) to install Envoy Gateway and the example manifest. +Follow the steps from the [Quickstart Guide](./quickstart) to install Envoy Gateway and the example manifest. Before proceeding, you should be able to query the example backend using HTTP. ## Single backendRef diff --git a/site/content/en/v0.6.0/user/http-urlrewrite.md b/site/content/en/v0.6/user/http-urlrewrite.md similarity index 98% rename from site/content/en/v0.6.0/user/http-urlrewrite.md rename to site/content/en/v0.6/user/http-urlrewrite.md index 945a24a7a44..b806a355b2c 100644 --- a/site/content/en/v0.6.0/user/http-urlrewrite.md +++ b/site/content/en/v0.6/user/http-urlrewrite.md @@ -7,7 +7,7 @@ used on a Route rule. This MUST NOT be used on the same Route rule as a HTTPRequ ## Prerequisites -Follow the steps from the [Quickstart Guide](../quickstart) to install Envoy Gateway and the example manifest. +Follow the steps from the [Quickstart Guide](./quickstart) to install Envoy Gateway and the example manifest. Before proceeding, you should be able to query the example backend using HTTP. ## Rewrite URL Prefix Path diff --git a/site/content/en/v0.6.0/user/jwt-authentication.md b/site/content/en/v0.6/user/jwt-authentication.md similarity index 94% rename from site/content/en/v0.6.0/user/jwt-authentication.md rename to site/content/en/v0.6/user/jwt-authentication.md index e11358c22fa..6c04873a10f 100644 --- a/site/content/en/v0.6.0/user/jwt-authentication.md +++ b/site/content/en/v0.6/user/jwt-authentication.md @@ -11,7 +11,7 @@ This instantiated resource can be linked to a [Gateway][Gateway], [HTTPRoute][HT ## Prerequisites -Follow the steps from the [Quickstart](../quickstart) guide to install Envoy Gateway and the example manifest. +Follow the steps from the [Quickstart](./quickstart) guide to install Envoy Gateway and the example manifest. For GRPC - follow the steps from the [GRPC Routing](../grpc-routing/) example. Before proceeding, you should be able to query the example backend using HTTP or GRPC. @@ -71,7 +71,7 @@ kubectl get securitypolicy/jwt-example -o yaml ## Testing -Ensure the `GATEWAY_HOST` environment variable from the [Quickstart](../quickstart) guide is set. If not, follow the +Ensure the `GATEWAY_HOST` environment variable from the [Quickstart](./quickstart) guide is set. If not, follow the Quickstart instructions to set the variable. ```shell @@ -150,7 +150,7 @@ You should see the below response ## Clean-Up -Follow the steps from the [Quickstart](../quickstart) guide to uninstall Envoy Gateway and the example manifest. +Follow the steps from the [Quickstart](./quickstart) guide to uninstall Envoy Gateway and the example manifest. Delete the SecurityPolicy: diff --git a/site/content/en/v0.6.0/user/multicluster-service.md b/site/content/en/v0.6/user/multicluster-service.md similarity index 100% rename from site/content/en/v0.6.0/user/multicluster-service.md rename to site/content/en/v0.6/user/multicluster-service.md diff --git a/site/content/en/v0.6.0/user/proxy-observability.md b/site/content/en/v0.6/user/proxy-observability.md similarity index 96% rename from site/content/en/v0.6.0/user/proxy-observability.md rename to site/content/en/v0.6/user/proxy-observability.md index 8601a0de7a8..77b391eadfb 100644 --- a/site/content/en/v0.6.0/user/proxy-observability.md +++ b/site/content/en/v0.6/user/proxy-observability.md @@ -7,7 +7,7 @@ This guide show you how to config proxy observability, includes metrics, logs, a ## Prerequisites -Follow the steps from the [Quickstart Guide](../quickstart) to install Envoy Gateway and the example manifest. +Follow the steps from the [Quickstart Guide](./quickstart) to install Envoy Gateway and the example manifest. Before proceeding, you should be able to query the example backend using HTTP. [FluentBit](https://fluentbit.io/) is used to collect logs from the EnvoyProxy instances and forward them to Loki. Install FluentBit: @@ -87,7 +87,7 @@ curl localhost:19001/metrics | grep "default/backend/rule/0/match/0-www" ## Logs -By default, Envoy Gateway send logs to stdout in [default text format](https://www.envoyproxy.io/docs/envoy/v0.6.0/configuration/observability/access_log/usage.html#default-format-string). +By default, Envoy Gateway send logs to stdout in [default text format](https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage.html#default-format-string). Verify logs from loki: ```shell diff --git a/site/content/en/v0.6.0/user/quickstart.md b/site/content/en/v0.6/user/quickstart.md similarity index 96% rename from site/content/en/v0.6.0/user/quickstart.md rename to site/content/en/v0.6/user/quickstart.md index 2e47dd6f26b..6a35933590e 100644 --- a/site/content/en/v0.6.0/user/quickstart.md +++ b/site/content/en/v0.6/user/quickstart.md @@ -9,7 +9,7 @@ This guide will help you get started with Envoy Gateway in a few simple steps. A Kubernetes cluster. -__Note:__ Refer to the [Compatibility Matrix](/blog/2022/10/01/versions/) for supported Kubernetes versions. +__Note:__ Refer to the [Compatibility Matrix](/news/releases/matrix/) for supported Kubernetes versions. __Note:__ In case your Kubernetes cluster, does not have a LoadBalancer implementation, we recommend installing one so the `Gateway` resource has an Address associated with it. We recommend using [MetalLB](https://metallb.universe.tf/installation/). diff --git a/site/content/en/v0.6.0/user/rate-limit.md b/site/content/en/v0.6/user/rate-limit.md similarity index 98% rename from site/content/en/v0.6.0/user/rate-limit.md rename to site/content/en/v0.6/user/rate-limit.md index 8f5867413ce..5a8cf305bbd 100644 --- a/site/content/en/v0.6.0/user/rate-limit.md +++ b/site/content/en/v0.6/user/rate-limit.md @@ -21,7 +21,7 @@ can be linked to a [Gateway][], [HTTPRoute][] or [GRPCRoute][] resource. ### Install Envoy Gateway -* Follow the steps from the [Quickstart Guide](../quickstart) to install Envoy Gateway and the HTTPRoute example manifest. +* Follow the steps from the [Quickstart Guide](./quickstart) to install Envoy Gateway and the HTTPRoute example manifest. Before proceeding, you should be able to query the example backend using HTTP. ### Install Redis @@ -817,10 +817,10 @@ EOF kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system ``` -[Global Rate Limiting]: https://www.envoyproxy.io/docs/envoy/v0.6.0/intro/arch_overview/other_features/global_rate_limiting -[BackendTrafficPolicy]: ../../api/extension_types#backendtrafficpolicy +[Global Rate Limiting]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_features/global_rate_limiting +[BackendTrafficPolicy]: ../api/extension_types#backendtrafficpolicy [Envoy Ratelimit]: https://github.com/envoyproxy/ratelimit -[EnvoyGateway]: ../../api/extension_types#envoygateway +[EnvoyGateway]: ../api/extension_types#envoygateway [Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway/ [HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute/ [GRPCRoute]: https://gateway-api.sigs.k8s.io/api-types/grpcroute/ diff --git a/site/content/en/v0.6.0/user/secure-gateways.md b/site/content/en/v0.6/user/secure-gateways.md similarity index 89% rename from site/content/en/v0.6.0/user/secure-gateways.md rename to site/content/en/v0.6/user/secure-gateways.md index 805aeb8b1a6..1c1551fb92a 100644 --- a/site/content/en/v0.6.0/user/secure-gateways.md +++ b/site/content/en/v0.6/user/secure-gateways.md @@ -11,7 +11,7 @@ testing and demonstration purposes only. ## Installation -Follow the steps from the [Quickstart Guide](../quickstart) to install Envoy Gateway and the example manifest. +Follow the steps from the [Quickstart Guide](./quickstart) to install Envoy Gateway and the example manifest. Before proceeding, you should be able to query the example backend using HTTP. ## TLS Certificates @@ -242,7 +242,7 @@ Lastly, test connectivity using the above [Testing section](#testing). ## Clean-Up -Follow the steps from the [Quickstart Guide](../quickstart) to uninstall Envoy Gateway and the example manifest. +Follow the steps from the [Quickstart Guide](./quickstart) to uninstall Envoy Gateway and the example manifest. Delete the Secrets: @@ -257,14 +257,14 @@ This section gives a walkthrough to generate RSA and ECDSA derived certificates ## Prerequisites -Follow the steps from the [Quickstart Guide](../quickstart) to install Envoy Gateway and the example manifest. +Follow the steps from the [Quickstart Guide](./quickstart) to install Envoy Gateway and the example manifest. Before proceeding, you should be able to query the example backend using HTTP. -Follow the steps in the [TLS Certificates](../secure-gateways#tls-certificates) section in the guide to generate self-signed RSA derived Server certificate and private key, and configure those in the Gateway listener configuration to terminate HTTPS traffic. +Follow the steps in the [TLS Certificates](./secure-gateways#tls-certificates) section in the guide to generate self-signed RSA derived Server certificate and private key, and configure those in the Gateway listener configuration to terminate HTTPS traffic. ## Pre-checks -While testing in [Cluster without External LoadBalancer Support](../secure-gateways#clusters-without-external-loadbalancer-support), we can query the example app through Envoy proxy while enforcing an RSA cipher, as shown below: +While testing in [Cluster without External LoadBalancer Support](./secure-gateways#clusters-without-external-loadbalancer-support), we can query the example app through Envoy proxy while enforcing an RSA cipher, as shown below: ```shell curl -v -HHost:www.example.com --resolve "www.example.com:8443:127.0.0.1" \ @@ -295,7 +295,7 @@ Moving forward in the doc, we will be configuring the existing Gateway listener ## TLS Certificates -Reuse the CA certificate and key pair generated in the [Secure Gateways](../secure-gateways#tls-certificates) guide and use this CA to sign both RSA and ECDSA Server certificates. +Reuse the CA certificate and key pair generated in the [Secure Gateways](./secure-gateways#tls-certificates) guide and use this CA to sign both RSA and ECDSA Server certificates. Note the CA certificate and key names are `example.com.crt` and `example.com.key` respectively. @@ -369,14 +369,14 @@ This sections gives a walkthrough to generate multiple certificates correspondin ## Prerequisites -Follow the steps from the [Quickstart Guide](../quickstart) to install Envoy Gateway and the example manifest. +Follow the steps from the [Quickstart Guide](./quickstart) to install Envoy Gateway and the example manifest. Before proceeding, you should be able to query the example backend using HTTP. -Follow the steps in the [TLS Certificates](../secure-gateways#tls-certificates) section in the guide to generate self-signed RSA derived Server certificate and private key, and configure those in the Gateway listener configuration to terminate HTTPS traffic. +Follow the steps in the [TLS Certificates](./secure-gateways#tls-certificates) section in the guide to generate self-signed RSA derived Server certificate and private key, and configure those in the Gateway listener configuration to terminate HTTPS traffic. ## Additional Configurations -Using the [TLS Certificates](../secure-gateways#tls-certificates) section in the guide we first generate additional Secret for another Host `www.sample.com`. +Using the [TLS Certificates](./secure-gateways#tls-certificates) section in the guide we first generate additional Secret for another Host `www.sample.com`. ```shell openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -subj '/O=sample Inc./CN=sample.com' -keyout sample.com.key -out sample.com.crt @@ -446,7 +446,7 @@ Since the multiple certificates are configured on the same Gateway listener, Env ### Clusters with External LoadBalancer Support -Refer to the steps mentioned earlier in the guide under [Testing in clusters with External LoadBalancer Support](../secure-gateways#clusters-with-external-loadbalancer-support) +Refer to the steps mentioned earlier in the guide under [Testing in clusters with External LoadBalancer Support](./secure-gateways#clusters-with-external-loadbalancer-support) ## Next Steps diff --git a/site/content/en/v0.6.0/user/tcp-routing.md b/site/content/en/v0.6/user/tcp-routing.md similarity index 100% rename from site/content/en/v0.6.0/user/tcp-routing.md rename to site/content/en/v0.6/user/tcp-routing.md diff --git a/site/content/en/v0.6.0/user/tls-cert-manager.md b/site/content/en/v0.6/user/tls-cert-manager.md similarity index 99% rename from site/content/en/v0.6.0/user/tls-cert-manager.md rename to site/content/en/v0.6/user/tls-cert-manager.md index 7776fbb2413..aeb5e1a0759 100644 --- a/site/content/en/v0.6.0/user/tls-cert-manager.md +++ b/site/content/en/v0.6/user/tls-cert-manager.md @@ -18,7 +18,7 @@ Changing to the Let's Encrypt production environment is straight-forward after t ## Installation -Follow the steps from the [Quickstart Guide](../quickstart) to install Envoy Gateway and the example manifest. +Follow the steps from the [Quickstart Guide](./quickstart) to install Envoy Gateway and the example manifest. Before proceeding, you should be able to query the example backend using HTTP. ## Deploying cert-manager @@ -432,5 +432,5 @@ eg-https kubernetes.io/tls 3 42m ## See Also -* [Secure Gateways](../secure-gateways/) +* [Secure Gateways](./secure-gateways) * [Securing gateway.networking.k8s.io Gateway Resources](https://cert-manager.io/docs/usage/gateway/) diff --git a/site/content/en/v0.6.0/user/tls-passthrough.md b/site/content/en/v0.6/user/tls-passthrough.md similarity index 94% rename from site/content/en/v0.6.0/user/tls-passthrough.md rename to site/content/en/v0.6/user/tls-passthrough.md index aab53254cc9..3390af7ea8a 100644 --- a/site/content/en/v0.6.0/user/tls-passthrough.md +++ b/site/content/en/v0.6/user/tls-passthrough.md @@ -12,7 +12,7 @@ to terminate the TLS connection, while the Gateway routes the requests to the ap ## Installation -Follow the steps from the [Quickstart Guide](../quickstart) to install Envoy Gateway and the example manifest. +Follow the steps from the [Quickstart Guide](./quickstart) to install Envoy Gateway and the example manifest. Before proceeding, you should be able to query the example backend using HTTP. ## TLS Certificates @@ -106,7 +106,7 @@ curl -v -HHost:passthrough.example.com --resolve "passthrough.example.com:6443:$ ## Clean-Up -Follow the steps from the [Quickstart Guide](../quickstart) to uninstall Envoy Gateway and the example manifest. +Follow the steps from the [Quickstart Guide](./quickstart) to uninstall Envoy Gateway and the example manifest. Delete the Secret: diff --git a/site/content/en/v0.6.0/user/tls-termination.md b/site/content/en/v0.6/user/tls-termination.md similarity index 96% rename from site/content/en/v0.6.0/user/tls-termination.md rename to site/content/en/v0.6/user/tls-termination.md index 4ac72aac7af..706f95b71ca 100644 --- a/site/content/en/v0.6.0/user/tls-termination.md +++ b/site/content/en/v0.6/user/tls-termination.md @@ -10,7 +10,7 @@ This guide will walk through the steps required to configure TLS Terminate mode ## Installation -Follow the steps from the [Quickstart Guide](../quickstart) to install Envoy Gateway. +Follow the steps from the [Quickstart Guide](./quickstart) to install Envoy Gateway. ## TLS Certificates Generate the certificates and keys used by the Gateway to terminate client TLS connections. diff --git a/site/content/en/v0.6.0/user/udp-routing.md b/site/content/en/v0.6/user/udp-routing.md similarity index 96% rename from site/content/en/v0.6.0/user/udp-routing.md rename to site/content/en/v0.6/user/udp-routing.md index 8d80fe789cf..20a77ca17cb 100644 --- a/site/content/en/v0.6.0/user/udp-routing.md +++ b/site/content/en/v0.6/user/udp-routing.md @@ -137,7 +137,7 @@ _udp.foo.bar.com. 0 IN SRV 0 0 42376 . ## Clean-Up -Follow the steps from the [Quickstart Guide](../quickstart) to uninstall Envoy Gateway. +Follow the steps from the [Quickstart Guide](./quickstart) to uninstall Envoy Gateway. Delete the CoreDNS example manifest and the UDPRoute: @@ -153,4 +153,4 @@ kubectl delete udproute/coredns Checkout the [Developer Guide](../../contributions/develop/) to get involved in the project. [UDPRoute]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.UDPRoute -[UDP proxy documentation]: https://www.envoyproxy.io/docs/envoy/v0.6.0/configuration/listeners/udp_filters/udp_proxy +[UDP proxy documentation]: https://www.envoyproxy.io/docs/envoy/latest/configuration/listeners/udp_filters/udp_proxy diff --git a/site/content/en/v1.0.2/releases/_index.md b/site/content/en/v1.0.2/releases/_index.md deleted file mode 100644 index 382eb1dd20d..00000000000 --- a/site/content/en/v1.0.2/releases/_index.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: "Releases" -weight: 90 -description: This section includes Releases of Envoy Gateway. ---- diff --git a/site/content/en/v1.0.2/releases/v0.1.0.md b/site/content/en/v1.0.2/releases/v0.1.0.md deleted file mode 100644 index 3d55118a846..00000000000 --- a/site/content/en/v1.0.2/releases/v0.1.0.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: "v0.1.0" -publishdate: 2022-05-16 ---- - -Date: May 16, 2022 - -## Documentation -- The initial open source release describing project goals and high-level design. diff --git a/site/content/en/v1.0.2/releases/v0.2.0.md b/site/content/en/v1.0.2/releases/v0.2.0.md deleted file mode 100644 index 6ebad0cf8f2..00000000000 --- a/site/content/en/v1.0.2/releases/v0.2.0.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: "v0.2.0" -publishdate: 2022-10-19 ---- - -Date: October 19, 2022 - -## Documentation -- Added Config API, translator, roadmap, and message bus design documentation. -- Added documentation for releasing Envoy Gateway. -- Added user guides for configuring common tasks, e.g. HTTP request routing. -- Added support for the Sphinx documentation generator. - -## API -- Added the EnvoyGateway API type for configuring Envoy Gateway. -- Added the EnvoyProxy API type for configuring managed Envoys. - -## CI Tooling Testing -- Added tooling to build, run, etc. Envoy Gateway. -- Added Gateway API conformance tests. -- Added Make-based tooling to fetch all tools so checks (code lint, spellchecks) and tests can be run locally. -- Added support for releasing latest artifacts to GitHub. -- Added code coverage with a minimum 60% threshold. - -## IR -- Added xds and infra IRs to decouple user-facing APIs from Envoy Gateway. -- Added IR validation. - -## Translator -- Added the gatewayapi translator to translate Gateway API and associated resources to the IR and manage the status of Gateway API resources. -- Added the xDS translator to translate the xds IR to xDS resources. - -## Message Service -- Added infra and xds IR watchable map messages for inter-component communication. -- Added a Runner to each Envoy Gateway component to support pub/sub between components. -- Added support for managing multiple separate Envoy proxy fleets. - -## Infra Manager -- Added Kubernetes Infra Manager to manage Envoy infrastructure running in a Kubernetes cluster. -- Added support for managing a separate Envoy infrastructure per Gateway. - -## Providers -- Added the Kubernetes provider with support for managing GatewayClass, Gateway, HTTPRoute, ReferenceGrant, and TLSRoute resources. -- Due to Issue #539, a ReferenceGrant is not removed from the system when unreferenced. -- Due to Issue #577, TLSRoute is not being tested for Gateway API conformance. -- Added watchers for dependent resources of managed Envoy infrastructure to trigger reconciliation. -- Added support for labeling managed infrastructure using Gateway namespace/name labels. -- Added support for finalizing the managed GatewayClass. - -## xDS -- Added xDS server support to configure managed Envoys using Delta xDS. -- Added initial support for mTLS between the xDS server and managed Envoys. -- Due to envoyproxy/go-control-plane Issue #599, Envoy Gateway logs the private key of HTTPS listeners. diff --git a/site/content/en/v1.0.2/releases/v0.3.0.md b/site/content/en/v1.0.2/releases/v0.3.0.md deleted file mode 100644 index 4eacf8c45c8..00000000000 --- a/site/content/en/v1.0.2/releases/v0.3.0.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -title: "v0.3.0" -publishdate: 2023-02-09 ---- - -Date: February 09, 2023 - -## Documentation -- Added Global Rate Limit User Docs -- Added Request Authentication User Docs -- Added TCP Routing User Docs -- Added UDP Routing User Docs -- Added GRPC Routing User Docs -- Added HTTP Response Headers User Docs -- Added TCP and UDP Proxy Design Docs -- Added egctl Design Docs -- Added Rate Limit Design Docs -- Added Request Authentication Design Docs -- Added Support for Versioned Docs -- Added Support for Multiple Release Versions -- Added Release Details Docs -- Added API Docs Generating Tooling -- Refactored Layout for User Docs - -## API -- Upgraded to v0.6.1 Gateway API -- Added Support for the TCPRoute API -- Added Support for the UDPRoute API -- Added Support for the GRPCRoute API -- Added Support for HTTPRoute URLRewrite Filter -- Added Support for HTTPRoute RequestMirror Filter -- Added Support for HTTPRoute ResponseHeaderModifier Filter -- Added Support for Request Authentication -- Added Support for Global Rate Limiting -- Added Support for Routes ReferenceGrant -- Added Support for Namespace Server Config Type -- Added initial management of Envoy Proxy deployment via EnvoyProxy API - -## CI Tooling Testing -- Fixed Make Image Failed in Darwin -- Fixed Wait for Job Succeeded before conformance test -- Upgraded Echoserver Image Tag -- Added Support for User-Facing Version -- Added Support for Testing EG against Multiple Kubernetes Versions - -## Conformance -- Enabled GatewayClassObservedGenerationBump conformance test -- Enabled GatewayInvalidTLSConfiguration conformance test -- Enabled GatewayInvalidRouteKind conformance test -- Enabled HTTPRouteReferenceGrant conformance test -- Enabled HTTPRouteMethodMatching conformance test -- Enabled HTTPRoutePartiallyInvalidViaInvalidReferenceGrant conformance test -- Enabled HTTPRouteInvalidParentRefNotMatchingListenerPort conformance test -- (Currently EG passes all conformance tests except redirect and gateway/httproute ObservedGenerationBump tests. Redirect tests are failing due to a possible issue with the way upstream conformance tests have made assumptions. Skip them for now until below issues #992 #993 #994 are resolved) - -## IR -- Added TCP Listener per TLSRoute - -## Translator -- Fixes Remove Stale Listener Condition -- Added Support for Suffix Matches for Headers -- Added Support for HTTP Method Matching to HTTPRoute -- Added Support for Regex Match Type -- Added Support for HTTPQueryParamMatch - -## Providers -- Refactored Kubernetes Provider to Single Reconciler -- Upgraded Kube Provider Test Data Manifests to v0.6.1 -- Removed Duplicate Settings from Bootstrap Config -- Updated Certgen to Use EG Namespace Env -- Added EnvoyProxy to Translator and Kube Infra Manager -- Upgraded Envoyproxy Image to envoy-dev latest in Main -- Removed EG Logs Private Key - -## xDS -- Fixed Start xDS Server Watchable Map Panics -- Enabled Access Logging for xDS Components diff --git a/site/content/en/v1.0.2/releases/v0.4.0-rc.1.md b/site/content/en/v1.0.2/releases/v0.4.0-rc.1.md deleted file mode 100644 index 927069f641f..00000000000 --- a/site/content/en/v1.0.2/releases/v0.4.0-rc.1.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -title: "v0.4.0-rc.1" -publishdate: 2023-04-13 ---- - -Date: April 13, 2023 - -## Documentation -- Added Docs for Installing and Using egctl - -## Installation -- Added Helm Installation Support -- Added Support for Ratelimiting Based On IP Subnet -- Added Gateway API Support Doc - -## API -- Upgraded to Gateway API v0.6.2 -- Added Support for Custom Envoy Proxy Bootstrap Config -- Added Support for Configuring the Envoy Proxy Image and Service -- Added Support for Configuring Annotations, Resources, and Securitycontext Settings on Ratelimit Infra and Envoy Proxy -- Added Support for Using Multiple Certificates on a Single Fully Qualified Domain Name -- Gateway Status Address is now Populated for ClusterIP type Envoy Services -- Envoy Proxy Pod and Container SecurityContext is now Configurable -- Added Custom Envoy Gateway Extensions Framework -- Added Support for Service Method Match in GRPCRoute - -## CI Tooling Testing -- Fixed CI Flakes During Helm Install -- Added Test To Ensure Static xDS Cluster Has Same Field Values as Dynamic Cluster -- Added egctl to Build and Test CI Workflow -- Code Coverage Thresholds are now Enforced by CI -- Fixed latest-release-check CI Job Failures -- Added Auto Release Tooling for Charts - -## Conformance -- Enabled GatewayWithAttachedRoutes Test -- Enabled Enable HTTPRouteInvalidParentRefNotMatchingSectionName Test -- Enabled Enable HTTPRouteDisallowedKind Test -- Re-Enabled Gateway/HTTPRouteObservedGenerationBump Test - -## Translator -- Added Support for Dynamic GatewayControllerName in Route Status - -## Providers -- Update GatewayClass Status Based on EnvoyProxy Config Validation - -## xDS -- Added EDS Support -- Fixed PathSeparatedPrefix and Optimized Logic for Prefixes Ending With Trailing Slash -- Updated Deprecated RegexMatcher -- Refactored Authn and Ratelimit Features to Reuse buildXdsCluster - -## Cli -- Added egctl CLI Tool -- Added egctl Support for Dry Runs of Gateway API Config -- Added egctl Support for Dumping Envoy Proxy xDS Resources diff --git a/site/content/en/v1.0.2/releases/v0.5.0.md b/site/content/en/v1.0.2/releases/v0.5.0.md deleted file mode 100644 index ce1bd6b9188..00000000000 --- a/site/content/en/v1.0.2/releases/v0.5.0.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -title: "v0.5.0" -publishdate: 2023-07-26 ---- - -Date: July 26, 2023 - -## Documentation -- Added Docs for Installation page using Helm -- Added Docs for Cert Manager Integration -- Added Docs for Presentation Links -- Added Docs for configuring multiple TLS Certificates per Listener - -## Installation -- Added Support for configuring Envoy Gateway Label and Annotations using Helm -- Increased default Resource defaults for Envoy Gateway to 100m CPU and 256Mi Memory -- Fixes Helm values for EnvoyGateway startup configuration -- Added opt-in field to skip creating control plane TLS Certificates allowing users to bring their own certificates. - -## API -- Upgraded to Gateway API v0.7.1 -- Added Support for EnvoyPatchPolicy -- Added Support for EnvoyProxy Telemetry - Access Logging, Traces and Metrics -- Added Support for configuring EnvoyProxy Pod Labels -- Added Support for configuring EnvoyProxy Deployment Strategy Settings, Volumes and Volume Mounts -- Added Support for configuring EnvoyProxy as a NodePort Type Service -- Added Support for Distinct RateLimiting for IP Addresses -- Added Support for converting JWT Claims to Headers, to be used for RateLimiting -- Added Admin Server for Envoy Gateway -- Added Pprof Debug Support for Envoy Gateway -- Added Support to Watch for Resources in Select Namespaces -### Breaking Changes -- Renamed field in EnvoyGateway API from Extension to ExtensionManager - -## CI Tooling Testing -- Added Retest Github Action -- Added CherryPick Github Action -- Added E2E Step in Github CI Workflow -- Added RateLimit E2E Tests -- Added JWT Claim based RateLimit E2E Tests -- Added Access Logging E2E tests -- Added Metrics E2E tests -- Added Tracing E2E tests - -## Conformance -- Enabled GatewayWithAttachedRoutes Test -- Enabled HttpRouteRequestMirror Test -- Skipped HTTPRouteRedirectPortAndScheme Test - -## Translator -### Breaking Changes -- Renamed IR resources from - to / - which also affects generated Xds Resources - -## Providers -- Reconcile Node resources to be able to compute Status Addresses for Gateway -- Discard Status before publishing Provider resources to reduce memory consumption - -## xDS -- Fix Init Race in Xds Runner when starting Xds Server and receiving Xds Input -- Switched to Xds SOTW Server for RateLimit Service Configuration -- Added Control Plane TLS between EnvoyProxy and RateLimit Service -- Enabled adding RateLimit Headers when RateLimit is set -- Allowed GRPCRoute and HTTPRoute to be linked to the same HTTPS Listener -- Set ALPN in the Xds Listener with TLS enabled. -- Added Best Practices Default Edge Settings to Xds Resources -- Compute and Publish EnvoyPatchPolicy status from xds-translator runner - -## Cli -- Added egctl x translate Support to generate default missing Resources -- Added egctl x translate Support for AuthenticationFilter and EnvoyPatchPolicy diff --git a/site/content/en/v1.0.2/releases/v0.6.0-rc.1.md b/site/content/en/v1.0.2/releases/v0.6.0-rc.1.md deleted file mode 100644 index 5141bc27966..00000000000 --- a/site/content/en/v1.0.2/releases/v0.6.0-rc.1.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -title: "v0.6.0-rc.1" -publishdate: 2023-10-27 ---- - -Date: Oct 27, 2023 - -## Documentation -- Introduced a new website based on Hugo -- Added Grafana dashboards and integration docs for EnvoyProxy metrics -- Added Grafana integration docs for Gateway API metrics - -## Installation -- Added Support for configuring Envoy Gateway Label and Annotations using Helm -- Increased default Resource defaults for Envoy Gateway to 100m CPU and 256Mi Memory -- Fixes Helm values for EnvoyGateway startup configuration -- Added opt-in field to skip creating control plane TLS Certificates allowing users to bring their own certificates. - -## API -- Upgraded to Gateway API v1.0.0 -- Added the ClientTrafficPolicy CRD with Keep Alive Support -- Added the BackendTrafficPolicy CRD with RateLimit and LoadBalancer Support -- Added the SecurityPolicy CRD with CORS and JWT Support -- Added EnvoyGateway Metrics with Prometheus and OpenTelemetry support -- Added Support for InitContainers in EnvoyProxy CRD -- Added Support for LoadBalancerIP in EnvoyProxy CRD -- Added Support for AllocateLoadBalancerNodePorts in EnvoyProxy CRD -- Added Support for LoadBalancerClass in EnvoyProxy CRD -- Added Support for selecting EnvoyProxy stats to be generated -- Added Support for enabling EnvoyProxy Virtual Host metrics -- Added Support for Merging Gateway resources onto the same infrastructure - -### Breaking changes -- Removed the AuthenticationFilter CRD -- Removed the RateLimitFilter CRD -- Enabled EnvoyProxy Prometheus Endpoint by default with an option to disable it -- Updated the Bootstrap field within the EnvoyProxy CRD with an additional value -- field to specify bootstrap config - -## watchable -- Improved caching of resource by implementing a compare function agnostic of resource order - -## Translator -### Breaking changes -- Added support for routing to EndpointSlice endpoints -- Added support for HTTPRoute Timeouts -- Added support for multiple RequestMirror filters per HTTPRoute rule -- Use / instead of - in IR Route Names -- Added Support to ignore ports in Host header - -## Providers -- Added the generationChangedPredicate to most resources to limit resource reconiliation -- Improved reconiliation by using the same enqueue request for all resources -- Added support for reconciling ServiceImport CRD -- Added support for selectively watching resources based on Namespace Selector - -## XDS -- Fixed Layered Runtime warnings -- Upgraded to the latest version of go-control-plane that fixed xDS Resource ordering issues for ADS. -- Added HTTP2 Keep Alives to the xds connection - -## Cli -- Added Support for egctl stats command - diff --git a/site/content/en/v1.0.2/releases/v1.0.1.md b/site/content/en/v1.0.2/releases/v1.0.1.md deleted file mode 100644 index 0b77719474a..00000000000 --- a/site/content/en/v1.0.2/releases/v1.0.1.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -title: "v1.0.1" -publishdate: 2024-04-09 ---- - -Date: April 9, 2024 - -## Installation -- Updated EnvoyProxy version to v1.29.3 -- Fixed certgen to support creating the hmac secret during an upgrade - -## Translator -- Fixed nil secret in resourceversiontable -- Add missing http filters to the http filter chain when ClientTrafficPolicy and MergeGateways is enabled -- Allow websockets when url rewrite is enabled -- Set the Host header for http health checker -- Fixed double slashes in redirect URL -- Allow ClientTrafficPolicy to attach to multiple http (non https) listeners within the same Gateway -- Set path prefix for the http ext auth service -- Set the route matching precedence order to Exact > RegularExpression > PathPrefix -- Fixed infraIR duplicate port translation for merged gateways -- Set SpawnUpstreamSpan to true -- Allow rate limit to work with multiple listeners - -## Infra-manager -- Skip creating infra resources when the InfraIR has empty listeners - diff --git a/site/content/en/v1.0.2/releases/v1.0.2.md b/site/content/en/v1.0.2/releases/v1.0.2.md deleted file mode 100644 index 8dee06cbf8a..00000000000 --- a/site/content/en/v1.0.2/releases/v1.0.2.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: "v1.0.2" -publishdate: 2024-06-12 ---- - -Date: June 12, 2024 - -## Installation -- Updated EnvoyProxy to 1.29.5 -- Use Patch API for infra-client -- Use ServerSideApply instead of CreateOrUpdate for infra-client - -## Testing -- Fixed failures due to an expired certificate in one of the translator tests - -## Translator -- Use - for naming service and container ports -- Added proxy protocol always as first listenerFilter -- Set ignoreCase for header matchers in extAuth -- Added backend TLS SAN validation -- Fixed ReplaceFullPath not working for root path (/) - -## Providers -- Fixed duplicated xroutes are added to gatewayapi Resources -- Fixed security policy reference grant from field type -- Fixed Route extension filters with different types but the same name and namespace aren't correctly cached -- Fixed secrets/configmap updates to trigger a controller reconcile by removing the generationChanged predicate -- Removed namespace restriction for EnvoyProxy parametersRef - diff --git a/site/content/en/v1.0/_index.md b/site/content/en/v1.0/_index.md new file mode 100644 index 00000000000..92ae8586885 --- /dev/null +++ b/site/content/en/v1.0/_index.md @@ -0,0 +1,15 @@ ++++ +title = "Welcome to Envoy Gateway" +linktitle = "Documentation" +description = "Envoy Gateway Documents" + +[[cascade]] +type = "docs" ++++ + +Envoy Gateway is an open source project for managing [Envoy Proxy](https://www.envoyproxy.io/) as a standalone or Kubernetes-based application +gateway. [Gateway API](https://gateway-api.sigs.k8s.io/) resources are used to dynamically provision and configure the managed Envoy Proxies. + +![architecture](/img/traffic.png) + +## Ready to get started? diff --git a/site/content/en/v1.0/api/_index.md b/site/content/en/v1.0/api/_index.md new file mode 100644 index 00000000000..396d9ffcefc --- /dev/null +++ b/site/content/en/v1.0/api/_index.md @@ -0,0 +1,5 @@ +--- +title: "API" +description: This section includes APIs of Envoy Gateway. +weight: 80 +--- diff --git a/site/content/en/v1.0.2/api/extension_types.md b/site/content/en/v1.0/api/extension_types.md similarity index 100% rename from site/content/en/v1.0.2/api/extension_types.md rename to site/content/en/v1.0/api/extension_types.md diff --git a/site/content/en/v1.0/install/_index.md b/site/content/en/v1.0/install/_index.md new file mode 100644 index 00000000000..b4c6f79c6fd --- /dev/null +++ b/site/content/en/v1.0/install/_index.md @@ -0,0 +1,5 @@ +--- +title: "Installation" +description: This section includes installation related contents of Envoy Gateway. +weight: 70 +--- diff --git a/site/content/en/v1.0.2/install/api.md b/site/content/en/v1.0/install/api.md similarity index 100% rename from site/content/en/v1.0.2/install/api.md rename to site/content/en/v1.0/install/api.md diff --git a/site/content/en/v1.0.2/install/custom-cert.md b/site/content/en/v1.0/install/custom-cert.md similarity index 100% rename from site/content/en/v1.0.2/install/custom-cert.md rename to site/content/en/v1.0/install/custom-cert.md diff --git a/site/content/en/v1.0/install/install-egctl.md b/site/content/en/v1.0/install/install-egctl.md new file mode 100644 index 00000000000..cbd82385740 --- /dev/null +++ b/site/content/en/v1.0/install/install-egctl.md @@ -0,0 +1,72 @@ +--- +title: "Install egctl" +weight: -80 +--- + +{{% alert title="What is egctl?" color="primary" %}} + +`egctl` is a command line tool to provide additional functionality for Envoy Gateway users. + +{{% /alert %}} + + +This task shows how to install the egctl CLI. egctl can be installed either from source, or from pre-built binary releases. + +### From The Envoy Gateway Project + +The Envoy Gateway project provides two ways to fetch and install egctl. These are the official methods to get egctl releases. Installation through those methods can be found below the official methods. + +{{< tabpane text=true >}} +{{% tab header="From the Binary Releases" %}} + +Every [release](https://github.com/envoyproxy/gateway/releases) of egctl provides binary releases for a variety of OSes. These binary versions can be manually downloaded and installed. + +1. Download your [desired version](https://github.com/envoyproxy/gateway/releases) +2. Unpack it (tar -zxvf egctl_latest_linux_amd64.tar.gz) +3. Find the egctl binary in the unpacked directory, and move it to its desired destination (mv bin/linux/amd64/egctl /usr/local/bin/egctl) + +From there, you should be able to run: `egctl help`. + +{{% /tab %}} +{{% tab header="From Script" %}} + +`egctl` now has an installer script that will automatically grab the latest release version of egctl and install it locally. + +You can fetch that script, and then execute it locally. It's well documented so that you can read through it and understand what it is doing before you run it. + +```shell +curl -fsSL -o get-egctl.sh https://gateway.envoyproxy.io/get-egctl.sh + +chmod +x get-egctl.sh + +# get help info of the +bash get-egctl.sh --help + +# install the latest development version of egctl +bash VERSION=latest get-egctl.sh +``` + +Yes, you can just use the below command if you want to live on the edge. + +```shell +curl -fsSL https://gateway.envoyproxy.io/get-egctl.sh | VERSION=latest bash +``` + +{{% /tab %}} + +{{% tab header="From Homebrew" %}} + +You can also install egctl using homebrew: + +```shell +brew install egctl +``` + +{{% /tab %}} +{{< /tabpane >}} + +{{% alert title="Next Steps" color="warning" %}} + +You can refer to the [Use egctl task](../tasks/operations/egctl) for more details about egctl. + +{{% /alert %}} diff --git a/site/content/en/v1.0.2/install/install-helm.md b/site/content/en/v1.0/install/install-helm.md similarity index 100% rename from site/content/en/v1.0.2/install/install-helm.md rename to site/content/en/v1.0/install/install-helm.md diff --git a/site/content/en/v1.0.2/install/install-yaml.md b/site/content/en/v1.0/install/install-yaml.md similarity index 100% rename from site/content/en/v1.0.2/install/install-yaml.md rename to site/content/en/v1.0/install/install-yaml.md diff --git a/site/content/en/v1.0/tasks/_index.md b/site/content/en/v1.0/tasks/_index.md new file mode 100644 index 00000000000..49e8595328b --- /dev/null +++ b/site/content/en/v1.0/tasks/_index.md @@ -0,0 +1,5 @@ +--- +title: "Tasks" +weight: 2 +description: Learn Envoy Gateway hands-on through tasks +--- diff --git a/site/content/en/v1.0.2/tasks/extensibility/_index.md b/site/content/en/v1.0/tasks/extensibility/_index.md similarity index 100% rename from site/content/en/v1.0.2/tasks/extensibility/_index.md rename to site/content/en/v1.0/tasks/extensibility/_index.md diff --git a/site/content/en/v1.0.2/tasks/extensibility/envoy-patch-policy.md b/site/content/en/v1.0/tasks/extensibility/envoy-patch-policy.md similarity index 100% rename from site/content/en/v1.0.2/tasks/extensibility/envoy-patch-policy.md rename to site/content/en/v1.0/tasks/extensibility/envoy-patch-policy.md diff --git a/site/content/en/v1.0.2/tasks/observability/_index.md b/site/content/en/v1.0/tasks/observability/_index.md similarity index 100% rename from site/content/en/v1.0.2/tasks/observability/_index.md rename to site/content/en/v1.0/tasks/observability/_index.md diff --git a/site/content/en/v1.0/tasks/observability/gateway-api-metrics.md b/site/content/en/v1.0/tasks/observability/gateway-api-metrics.md new file mode 100644 index 00000000000..bd9e5b89317 --- /dev/null +++ b/site/content/en/v1.0/tasks/observability/gateway-api-metrics.md @@ -0,0 +1,59 @@ +--- +title: "Gateway API Metrics" +--- + +Resource metrics for Gateway API objects are available using the [Gateway API State Metrics][gasm] project. +The project also provides example dashboard for visualising the metrics using Grafana, and example alerts using Prometheus & Alertmanager. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +Run the following commands to install the metrics stack, with the Gateway API State Metrics configuration, on your kubernetes cluster: + +```shell +kubectl apply --server-side -f https://raw.githubusercontent.com/Kuadrant/gateway-api-state-metrics/main/config/examples/kube-prometheus/bundle_crd.yaml +kubectl apply -f https://raw.githubusercontent.com/Kuadrant/gateway-api-state-metrics/main/config/examples/kube-prometheus/bundle.yaml +``` + +## Metrics and Alerts + +To access the Prometheus UI, wait for the statefulset to be ready, then use the port-forward command: + +```shell +# This first command may fail if the statefulset has not been created yet. +# In that case, try again until you get a message like 'Waiting for 2 pods to be ready...' +# or 'statefulset rolling update complete 2 pods...' +kubectl -n monitoring rollout status --watch --timeout=5m statefulset/prometheus-k8s +kubectl -n monitoring port-forward service/prometheus-k8s 9090:9090 > /dev/null & +``` + +Navigate to `http://localhost:9090`. +Metrics can be queried from the 'Graph' tab e.g. `gatewayapi_gateway_created` +See the [Gateway API State Metrics README][gasm-readme] for the full list of Gateway API metrics available. + +Alerts can be seen in the 'Alerts' tab. +Gateway API specific alerts will be grouped under the 'gateway-api.rules' heading. + +***Note:*** Alerts are defined in a PrometheusRules custom resource in the 'monitoring' namespace. You can modify the alert rules by updating this resource. + +## Dashboards + +To view the dashboards in Grafana, wait for the deployment to be ready, then use the port-forward command: + +```shell +kubectl -n monitoring wait --timeout=5m deployment/grafana --for=condition=Available +kubectl -n monitoring port-forward service/grafana 3000:3000 > /dev/null & +``` + +Navigate to `http://localhost:3000` and sign in with admin/admin. +The Gateway API State dashboards will be available in the 'Default' folder and tagged with 'gateway-api'. +See the [Gateway API State Metrics README][gasm-dashboards] for further information on available dashboards. + +***Note:*** Dashboards are loaded from configmaps. You can modify the dashboards in the Grafana UI, however you will need to export them from the UI and update the json in the configmaps to persist changes. + + +[gasm]: https://github.com/Kuadrant/gateway-api-state-metrics +[gasm-readme]: https://github.com/Kuadrant/gateway-api-state-metrics/tree/main#metrics +[gasm-dashboards]: https://github.com/Kuadrant/gateway-api-state-metrics/tree/main#dashboards diff --git a/site/content/en/v1.0.2/tasks/observability/grafana-integration.md b/site/content/en/v1.0/tasks/observability/grafana-integration.md similarity index 81% rename from site/content/en/v1.0.2/tasks/observability/grafana-integration.md rename to site/content/en/v1.0/tasks/observability/grafana-integration.md index e2da53b308a..3e0ee3345b8 100644 --- a/site/content/en/v1.0.2/tasks/observability/grafana-integration.md +++ b/site/content/en/v1.0/tasks/observability/grafana-integration.md @@ -50,16 +50,16 @@ To log in to Grafana, use the credentials `admin:admin`. Envoy Gateway has examples of dashboard for you to get started: -### [Envoy Global](https://raw.githubusercontent.com/envoyproxy/gateway/main/charts/gateway-addons-helm/dashboards/envoy-global.json) +### Envoy Proxy Global -![Envoy Global](/img/envoy-global-dashboard.png) +![Envoy Proxy Global](/img/envoy-proxy-global-dashboard.png) -### [Envoy Clusters](https://raw.githubusercontent.com/envoyproxy/gateway/main/charts/gateway-addons-helm/dashboards/envoy-clusters.json) +### Envoy Clusters ![Envoy Clusters](/img/envoy-clusters-dashboard.png) -### [Envoy Pod Resources](https://raw.githubusercontent.com/envoyproxy/gateway/main/charts/gateway-addons-helm/dashboards/envoy-pod-resource.json) +### Envoy Pod Resources -![Envoy Pod Resources](/img/envoy-pod-resources-dashboard.png) +![Envoy Pod Resources](/img/resources-monitor-dashboard.png) You can load the above dashboards in your Grafana to get started. Please refer to Grafana docs for [importing dashboards](https://grafana.com/docs/grafana/latest/dashboards/manage-dashboards/#import-a-dashboard). diff --git a/site/content/en/v1.0.2/tasks/observability/proxy-observability.md b/site/content/en/v1.0/tasks/observability/proxy-observability.md similarity index 100% rename from site/content/en/v1.0.2/tasks/observability/proxy-observability.md rename to site/content/en/v1.0/tasks/observability/proxy-observability.md diff --git a/site/content/en/v1.0.2/tasks/operations/_index.md b/site/content/en/v1.0/tasks/operations/_index.md similarity index 100% rename from site/content/en/v1.0.2/tasks/operations/_index.md rename to site/content/en/v1.0/tasks/operations/_index.md diff --git a/site/content/en/v1.0.2/tasks/operations/customize-envoyproxy.md b/site/content/en/v1.0/tasks/operations/customize-envoyproxy.md similarity index 100% rename from site/content/en/v1.0.2/tasks/operations/customize-envoyproxy.md rename to site/content/en/v1.0/tasks/operations/customize-envoyproxy.md diff --git a/site/content/en/v1.0.2/tasks/operations/deployment-mode.md b/site/content/en/v1.0/tasks/operations/deployment-mode.md similarity index 100% rename from site/content/en/v1.0.2/tasks/operations/deployment-mode.md rename to site/content/en/v1.0/tasks/operations/deployment-mode.md diff --git a/site/content/en/v1.0.2/tasks/operations/egctl.md b/site/content/en/v1.0/tasks/operations/egctl.md similarity index 100% rename from site/content/en/v1.0.2/tasks/operations/egctl.md rename to site/content/en/v1.0/tasks/operations/egctl.md diff --git a/site/content/en/v1.0.2/tasks/quickstart.md b/site/content/en/v1.0/tasks/quickstart.md similarity index 100% rename from site/content/en/v1.0.2/tasks/quickstart.md rename to site/content/en/v1.0/tasks/quickstart.md diff --git a/site/content/en/v1.0.2/tasks/security/_index.md b/site/content/en/v1.0/tasks/security/_index.md similarity index 100% rename from site/content/en/v1.0.2/tasks/security/_index.md rename to site/content/en/v1.0/tasks/security/_index.md diff --git a/site/content/en/v1.0.2/tasks/security/backend-tls.md b/site/content/en/v1.0/tasks/security/backend-tls.md similarity index 96% rename from site/content/en/v1.0.2/tasks/security/backend-tls.md rename to site/content/en/v1.0/tasks/security/backend-tls.md index 9eadf4dd9d8..1f84ea4db81 100644 --- a/site/content/en/v1.0.2/tasks/security/backend-tls.md +++ b/site/content/en/v1.0/tasks/security/backend-tls.md @@ -13,7 +13,7 @@ Envoy Gateway supports the Gateway-API defined [BackendTLSPolicy][]. ## Installation -Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Follow the steps from the [Quickstart][] to install Envoy Gateway and the example manifest. ## TLS Certificates @@ -28,7 +28,7 @@ openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -subj '/O=example In Create a certificate and a private key for `www.example.com`: ```shell -openssl req -out www.example.com.csr -newkey rsa:2048 -nodes -keyout www.example.com.key -subj "/CN=www.example.com/O=example organization" +openssl req -out www.example.com.csr -newkey rsa:2048 -nodes -keyout www.example.com.key -subj "/CN=www.example.com/O=example organization" -addext "subjectAltName = DNS:www.example.com" openssl x509 -req -days 365 -CA ca.crt -CAkey ca.key -set_serial 0 -in www.example.com.csr -out www.example.com.crt ``` @@ -209,4 +209,5 @@ Inspect the output and see that the response contains the details of the TLS han } ``` +[Quickstart]: ../quickstart [BackendTLSPolicy]: https://gateway-api.sigs.k8s.io/api-types/backendtlspolicy/ diff --git a/site/content/en/v1.0.2/tasks/security/basic-auth.md b/site/content/en/v1.0/tasks/security/basic-auth.md similarity index 100% rename from site/content/en/v1.0.2/tasks/security/basic-auth.md rename to site/content/en/v1.0/tasks/security/basic-auth.md diff --git a/site/content/en/v1.0.2/tasks/security/cors.md b/site/content/en/v1.0/tasks/security/cors.md similarity index 100% rename from site/content/en/v1.0.2/tasks/security/cors.md rename to site/content/en/v1.0/tasks/security/cors.md diff --git a/site/content/en/v1.0.2/tasks/security/ext-auth.md b/site/content/en/v1.0/tasks/security/ext-auth.md similarity index 100% rename from site/content/en/v1.0.2/tasks/security/ext-auth.md rename to site/content/en/v1.0/tasks/security/ext-auth.md diff --git a/site/content/en/v1.0.2/tasks/security/jwt-authentication.md b/site/content/en/v1.0/tasks/security/jwt-authentication.md similarity index 100% rename from site/content/en/v1.0.2/tasks/security/jwt-authentication.md rename to site/content/en/v1.0/tasks/security/jwt-authentication.md diff --git a/site/content/en/v1.0.2/tasks/security/mutual-tls.md b/site/content/en/v1.0/tasks/security/mutual-tls.md similarity index 100% rename from site/content/en/v1.0.2/tasks/security/mutual-tls.md rename to site/content/en/v1.0/tasks/security/mutual-tls.md diff --git a/site/content/en/v1.0.2/tasks/security/oidc.md b/site/content/en/v1.0/tasks/security/oidc.md similarity index 100% rename from site/content/en/v1.0.2/tasks/security/oidc.md rename to site/content/en/v1.0/tasks/security/oidc.md diff --git a/site/content/en/v1.0.2/tasks/security/secure-gateways.md b/site/content/en/v1.0/tasks/security/secure-gateways.md similarity index 100% rename from site/content/en/v1.0.2/tasks/security/secure-gateways.md rename to site/content/en/v1.0/tasks/security/secure-gateways.md diff --git a/site/content/en/v1.0.2/tasks/security/threat-model.md b/site/content/en/v1.0/tasks/security/threat-model.md similarity index 98% rename from site/content/en/v1.0.2/tasks/security/threat-model.md rename to site/content/en/v1.0/tasks/security/threat-model.md index c1bba3f9726..f5083875107 100644 --- a/site/content/en/v1.0.2/tasks/security/threat-model.md +++ b/site/content/en/v1.0/tasks/security/threat-model.md @@ -396,7 +396,7 @@ When considering internal threat actors, we chose to follow the [security model] **Threat**: Reduced API gateway availability due to an attacker\'s maliciously crafted request (e.g., QoD) potentially inducing a Denial of Service (DoS) attack. - **Recommendation**: To ensure high availability and to mitigate potential security threats, adhere to the Envoy Gateway documentation for the configuration of a [rate-limiting](https://gateway.envoyproxy.io/v0.6.0/user/rate-limit/) filter and load balancing. + **Recommendation**: To ensure high availability and to mitigate potential security threats, adhere to the Envoy Gateway documentation for the configuration of a [rate-limiting](../traffic/global-rate-limit) filter and load balancing. Further, adhere to best practices for configuring Envoy Proxy as an edge proxy documented [here](https://www.envoyproxy.io/docs/envoy/latest/configuration/best_practices/edge#configuring-envoy-as-an-edge-proxy) within the EnvoyProxy docs. This involves configuring TCP and HTTP proxies with specific settings, including restricting access to the admin endpoint, setting the [overload manager](https://www.envoyproxy.io/docs/envoy/latest/configuration/operations/overload_manager/overload_manager#config-overload-manager) and [listener](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) / [cluster](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) buffer limits, enabling [use_remote_address](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-use-remote-address), setting [connection and stream timeouts](https://www.envoyproxy.io/docs/envoy/latest/faq/configuration/timeouts#faq-configuration-timeouts), limiting [maximum concurrent streams](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-field-config-core-v3-http2protocoloptions-max-concurrent-streams), setting [initial stream window size limit](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-field-config-core-v3-http2protocoloptions-initial-stream-window-size), and configuring action on [headers_with_underscores](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-field-config-core-v3-httpprotocoloptions-headers-with-underscores-action). @@ -603,7 +603,7 @@ Set runAsUser and runAsGroup security context options to specific UIDs (e.g., ru |EGTM-008|EGTM-EG-003|Envoy Gateway| There is a risk of a threat actor misconfiguring static config and compromising the integrity of Envoy Gateway, ultimately leading to the compromised confidentiality, integrity, or availability of tenant data and cluster resources.

| Accidental or deliberate misconfiguration of static configuration leads to a misconfigured deployment of Envoy Gateway, for example logging parameters could be modified or global rate limiting configuration misconfigured.

|Medium| Implement a GitOps model, utilising Kubernetes\' Role-Based Access Control (RBAC) and adhering to the principle of least privilege to minimise human intervention on the cluster. For instance, tools like [ArgoCD](https://argo-cd.readthedocs.io/en/stable/) can be used for declarative GitOps deployments, ensuring all changes are tracked and reviewed. Additionally, configure your source control management (SCM) system to include mandatory pull request (PR) reviews, commit signing, and protected branches to ensure only authorised changes can be committed to the start-up configuration. | |EGTM-010|EGTM-CS-005|Container Security| There is a risk that a threat actor exploits a weak pod security context, compromising the CIA of a node and the resources / services which run on it.

| Threat Actor who has compromised a pod exploits weak security context to escape to a node, potentially leading to the compromise of Envoy Proxy or Gateway running on the same node.

|Medium| To mitigate this risk, apply [Pod Security Standards](https://kubernetes.io/docs/concepts/security/pod-security-standards/) at a minimum of [Baseline](https://kubernetes.io/docs/concepts/security/pod-security-standards/#baseline) level to all namespaces, especially those containing Envoy Gateway and Proxy Pods. Pod security standards are implemented through K8s [Pod Security Admission](https://kubernetes.io/docs/concepts/security/pod-security-admission/) to provide [admission control modes](https://kubernetes.io/docs/concepts/security/pod-security-admission/#pod-security-admission-labels-for-namespaces) (enforce, audit, and warn) for namespaces. Pod security standards can be enforced by namespace labels as shown [here](https://kubernetes.io/docs/tasks/configure-pod-container/enforce-standards-namespace-labels/), to enforce a baseline level of pod security to specific namespaces.

Further enhance the security by implementing a sandboxing solution such as [gVisor](https://gvisor.dev/) for Envoy Gateway and Proxy Pods to isolate the application from the host kernel. This can be set within the runtimeClassName of the Pod specification. | |EGTM-012|EGTM-GW-004|Gateway API| There is a risk that a threat actor could abuse excessive RBAC privileges to create ReferenceGrant resources. These resources could then be used to create cross-namespace communication, leading to unauthorised access to the application. This could compromise the confidentiality and integrity of resources and configuration in the affected namespaces and potentially disrupt the availability of services that rely on these object references.

| A ReferenceGrant is created, which validates traffic to cross namespace trust boundaries without a valid business reason, such as a route in one tenant\'s namespace referencing a backend in another.

|Medium| Ensure that the ability to create ReferenceGrant resources is restricted to the minimum number of people. Pay special attention to ClusterRoles that allow that action. | -|EGTM-018|EGTM-GW-006|Gateway API| There is a risk that malicious requests could lead to a Denial of Service (DoS) attack, thereby reducing API gateway availability due to misconfigurations in rate-limiting or load balancing controls, or a lack of route timeout enforcement.

| Reduced API gateway availability due to an attacker\'s maliciously crafted request (e.g., QoD) potentially inducing a Denial of Service (DoS) attack.

|Medium| To ensure high availability and to mitigate potential security threats, adhere to the Envoy Gateway documentation for the configuration of a [rate-limiting](https://gateway.envoyproxy.io/v0.6.0/user/rate-limit/) filter and load balancing.

Further, adhere to best practices for configuring Envoy Proxy as an edge proxy documented [here](https://www.envoyproxy.io/docs/envoy/latest/configuration/best_practices/edge#configuring-envoy-as-an-edge-proxy) within the EnvoyProxy docs. This involves configuring TCP and HTTP proxies with specific settings, including restricting access to the admin endpoint, setting the [overload manager](https://www.envoyproxy.io/docs/envoy/latest/configuration/operations/overload_manager/overload_manager#config-overload-manager) and [listener](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) / [cluster](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) buffer limits, enabling [use_remote_address](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-use-remote-address), setting [connection and stream timeouts](https://www.envoyproxy.io/docs/envoy/latest/faq/configuration/timeouts#faq-configuration-timeouts), limiting [maximum concurrent streams](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-field-config-core-v3-http2protocoloptions-max-concurrent-streams), setting [initial stream window size limit](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-field-config-core-v3-http2protocoloptions-initial-stream-window-size), and configuring action on [headers_with_underscores](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-field-config-core-v3-httpprotocoloptions-headers-with-underscores-action).

[Path normalisation](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-normalize-path) should be enabled to minimise path confusion vulnerabilities. These measures help protect against volumetric threats such as Denial of Service (DoS)nattacks. Utilise custom resources to implement policy attachment, thereby exposing request limit configuration for route types. | +|EGTM-018|EGTM-GW-006|Gateway API| There is a risk that malicious requests could lead to a Denial of Service (DoS) attack, thereby reducing API gateway availability due to misconfigurations in rate-limiting or load balancing controls, or a lack of route timeout enforcement.

| Reduced API gateway availability due to an attacker\'s maliciously crafted request (e.g., QoD) potentially inducing a Denial of Service (DoS) attack.

|Medium| To ensure high availability and to mitigate potential security threats, adhere to the Envoy Gateway documentation for the configuration of a [rate-limiting](../traffic/global-rate-limit) filter and load balancing.

Further, adhere to best practices for configuring Envoy Proxy as an edge proxy documented [here](https://www.envoyproxy.io/docs/envoy/latest/configuration/best_practices/edge#configuring-envoy-as-an-edge-proxy) within the EnvoyProxy docs. This involves configuring TCP and HTTP proxies with specific settings, including restricting access to the admin endpoint, setting the [overload manager](https://www.envoyproxy.io/docs/envoy/latest/configuration/operations/overload_manager/overload_manager#config-overload-manager) and [listener](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) / [cluster](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) buffer limits, enabling [use_remote_address](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-use-remote-address), setting [connection and stream timeouts](https://www.envoyproxy.io/docs/envoy/latest/faq/configuration/timeouts#faq-configuration-timeouts), limiting [maximum concurrent streams](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-field-config-core-v3-http2protocoloptions-max-concurrent-streams), setting [initial stream window size limit](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-field-config-core-v3-http2protocoloptions-initial-stream-window-size), and configuring action on [headers_with_underscores](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-field-config-core-v3-httpprotocoloptions-headers-with-underscores-action).

[Path normalisation](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-normalize-path) should be enabled to minimise path confusion vulnerabilities. These measures help protect against volumetric threats such as Denial of Service (DoS)nattacks. Utilise custom resources to implement policy attachment, thereby exposing request limit configuration for route types. | |EGTM-019|EGTM-DP-004|Container Security| There is a risk that replay attacks using stolen or reused JSON Web Tokens (JWTs) can compromise transmission integrity, thereby undermining the confidentiality and integrity of the data plane.

| Transmission integrity is compromised due to replay attacks using stolen or reused JSON Web Tokens (JWTs).

|Medium| Comply with JWT best practices for enhanced security, paying special attention to the use of short-lived tokens, which reduce the window of opportunity for a replay attack. The [exp](https://datatracker.ietf.org/doc/html/rfc7519#page-9) claim can be used to set token expiration times. | |EGTM-024|EGTM-EG-008|Envoy Gateway| There is a risk of developers getting more privileges than required due to the use of SecurityPolicy, ClientTrafficPolicy, EnvoyPatchPolicy and BackendTrafficPolicy. These resources can be attached to a Gateway resource. Therefore, a developer with permission to deploy them would be able to modify a Gateway configuration by targeting the gateway in the policy manifest. This conflicts with the [Advanced 4 Tier Model](https://gateway-api.sigs.k8s.io/concepts/security-model/#write-permissions-for-advanced-4-tier-model), where developers do not have write permissions on Gateways.

| Excessive developer permissions lead to a misconfiguration and/or unauthorised access.

|Medium| Considering the Tenant C scenario (represented in the Architecture Diagram), if a developer can create SecurityPolicy, ClientTrafficPolicy, EnvoyPatchPolicy or BackendTrafficPolicy objects in namespace C, they would be able to modify a Gateway configuration by attaching the policy to the gateway. In such scenarios, it is recommended to either:

a. Create a separate namespace, where developers have no permissions, > to host tenant C\'s gateway. Note that, due to design decisions, > the > SecurityPolicy/EnvoyPatchPolicy/ClientTrafficPolicy/BackendTrafficPolicy > object can only target resources deployed in the same namespace. > Therefore, having a separate namespace for the gateway would > prevent developers from attaching the policy to the gateway.

b. Forbid the creation of these policies for developers in namespace C.

On the other hand, in scenarios similar to tenants A and B, where a shared gateway namespace is in place, this issue is more limited. Note that in this scenario, developers don\'t have access to the shared gateway namespace.

In addition, it is important to mention that EnvoyPatchPolicy resources can also be attached to GatewayClass resources. This means that, in order to comply with the Advanced 4 Tier model, individuals with the Application Administrator role should not have access to this resource either. | |EGTM-003|EGTM-EG-001|Envoy Gateway| There is a risk that a threat actor could downgrade the security of proxied connections by configuring a weak set of cipher suites, compromising the confidentiality and integrity of proxied traffic.

| Exploit weak cipher suite configuration to downgrade security of proxied connections.

|Low| Users operating in highly regulated environments may need to tightly control the TLS protocol and associated cipher suites, blocking non-conforming incoming connections to the gateway.

EnvoyProxy bootstrap config can be customised as per the [customise EnvoyProxy](../operations/customize-envoyproxy) documentation. In addition, from v.1.0.0, it is possible to configure common TLS properties for a Gateway or XRoute through the [ClientTrafficPolicy](https://gateway.envoyproxy.io/latest/api/extension_types/#clienttrafficpolicy) object. | diff --git a/site/content/en/v1.0.2/tasks/security/tls-cert-manager.md b/site/content/en/v1.0/tasks/security/tls-cert-manager.md similarity index 100% rename from site/content/en/v1.0.2/tasks/security/tls-cert-manager.md rename to site/content/en/v1.0/tasks/security/tls-cert-manager.md diff --git a/site/content/en/v1.0.2/tasks/security/tls-passthrough.md b/site/content/en/v1.0/tasks/security/tls-passthrough.md similarity index 100% rename from site/content/en/v1.0.2/tasks/security/tls-passthrough.md rename to site/content/en/v1.0/tasks/security/tls-passthrough.md diff --git a/site/content/en/v1.0.2/tasks/security/tls-termination.md b/site/content/en/v1.0/tasks/security/tls-termination.md similarity index 100% rename from site/content/en/v1.0.2/tasks/security/tls-termination.md rename to site/content/en/v1.0/tasks/security/tls-termination.md diff --git a/site/content/en/v1.0/tasks/traffic/_index.md b/site/content/en/v1.0/tasks/traffic/_index.md new file mode 100644 index 00000000000..f884ccdfcb0 --- /dev/null +++ b/site/content/en/v1.0/tasks/traffic/_index.md @@ -0,0 +1,5 @@ +--- +title: "Traffic" +weight: 1 +description: This section includes Traffic Management tasks. +--- diff --git a/site/content/en/v1.0.2/tasks/traffic/circuit-breaker.md b/site/content/en/v1.0/tasks/traffic/circuit-breaker.md similarity index 100% rename from site/content/en/v1.0.2/tasks/traffic/circuit-breaker.md rename to site/content/en/v1.0/tasks/traffic/circuit-breaker.md diff --git a/site/content/en/v1.0.2/tasks/traffic/client-traffic-policy.md b/site/content/en/v1.0/tasks/traffic/client-traffic-policy.md similarity index 100% rename from site/content/en/v1.0.2/tasks/traffic/client-traffic-policy.md rename to site/content/en/v1.0/tasks/traffic/client-traffic-policy.md diff --git a/site/content/en/v1.0.2/tasks/traffic/fault-injection.md b/site/content/en/v1.0/tasks/traffic/fault-injection.md similarity index 100% rename from site/content/en/v1.0.2/tasks/traffic/fault-injection.md rename to site/content/en/v1.0/tasks/traffic/fault-injection.md diff --git a/site/content/en/v1.0/tasks/traffic/gateway-address.md b/site/content/en/v1.0/tasks/traffic/gateway-address.md new file mode 100644 index 00000000000..bd87726c139 --- /dev/null +++ b/site/content/en/v1.0/tasks/traffic/gateway-address.md @@ -0,0 +1,68 @@ +--- +title: "Gateway Address" +--- + +The Gateway API provides an optional [Addresses][] field through which Envoy Gateway can set addresses for Envoy Proxy Service. +Depending on the Service Type, the addresses of gateway can be used as: + +- [External IPs](#external-ips) +- [Cluster IP](#cluster-ip) + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. + +## External IPs + +Using the addresses in `Gateway.Spec.Addresses` as the [External IPs][] of Envoy Proxy Service, +this will __require__ the address to be of type `IPAddress` and the [ServiceType][] to be of `LoadBalancer` or `NodePort`. + +The Envoy Gateway deploys Envoy Proxy Service as `LoadBalancer` by default, +so you can set the address of the Gateway directly (the address settings here are for reference only): + +```shell +kubectl patch gateway eg --type=json --patch ' +- op: add + path: /spec/addresses + value: + - type: IPAddress + value: 1.2.3.4 +' +``` + +Verify the Gateway status: + +```shell +kubectl get gateway +``` + +```console +NAME CLASS ADDRESS PROGRAMMED AGE +eg eg 1.2.3.4 True 14m +``` + +Verify the Envoy Proxy Service status: + +```shell +kubectl get service -n envoy-gateway-system +``` + +```console +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +envoy-default-eg-64656661 LoadBalancer 10.96.236.219 1.2.3.4 80:31017/TCP 15m +envoy-gateway ClusterIP 10.96.192.76 18000/TCP 15m +envoy-gateway-metrics-service ClusterIP 10.96.124.73 8443/TCP 15m +``` + +__Note:__ If the `Gateway.Spec.Addresses` is explicitly set, it will be the only addresses that populates the Gateway status. + +## Cluster IP + +Using the addresses in `Gateway.Spec.Addresses` as the [Cluster IP][] of Envoy Proxy Service, +this will __require__ the address to be of type `IPAddress` and the [ServiceType][] to be of `ClusterIP`. + + +[Addresses]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.GatewayAddress +[External IPs]: https://kubernetes.io/docs/concepts/services-networking/service/#external-ips +[Cluster IP]: https://kubernetes.io/docs/concepts/services-networking/service/#type-clusterip +[ServiceType]: ../../../api/extension_types#servicetype diff --git a/site/content/en/v1.0.2/tasks/traffic/gatewayapi-support.md b/site/content/en/v1.0/tasks/traffic/gatewayapi-support.md similarity index 100% rename from site/content/en/v1.0.2/tasks/traffic/gatewayapi-support.md rename to site/content/en/v1.0/tasks/traffic/gatewayapi-support.md diff --git a/site/content/en/v1.0.2/tasks/traffic/global-rate-limit.md b/site/content/en/v1.0/tasks/traffic/global-rate-limit.md similarity index 100% rename from site/content/en/v1.0.2/tasks/traffic/global-rate-limit.md rename to site/content/en/v1.0/tasks/traffic/global-rate-limit.md diff --git a/site/content/en/v1.0.2/tasks/traffic/grpc-routing.md b/site/content/en/v1.0/tasks/traffic/grpc-routing.md similarity index 100% rename from site/content/en/v1.0.2/tasks/traffic/grpc-routing.md rename to site/content/en/v1.0/tasks/traffic/grpc-routing.md diff --git a/site/content/en/v1.0/tasks/traffic/http-redirect.md b/site/content/en/v1.0/tasks/traffic/http-redirect.md new file mode 100644 index 00000000000..b3177e89263 --- /dev/null +++ b/site/content/en/v1.0/tasks/traffic/http-redirect.md @@ -0,0 +1,399 @@ +--- +title: "HTTP Redirects" +--- + +The [HTTPRoute][] resource can issue redirects to clients or rewrite paths sent upstream using filters. Note that +HTTPRoute rules cannot use both filter types at once. Currently, Envoy Gateway only supports __core__ +[HTTPRoute filters][] which consist of `RequestRedirect` and `RequestHeaderModifier` at the time of this writing. To +learn more about HTTP routing, refer to the [Gateway API documentation][]. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTPS. + +## Redirects + +Redirects return HTTP 3XX responses to a client, instructing it to retrieve a different resource. A +[`RequestRedirect` filter][req_filter] instructs Gateways to emit a redirect response to requests that match the rule. +For example, to issue a permanent redirect (301) from HTTP to HTTPS, configure `requestRedirect.statusCode=301` and +`requestRedirect.scheme="https"`: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +__Note:__ `301` (default) and `302` are the only supported statusCodes. + +The HTTPRoute status should indicate that it has been accepted and is bound to the example Gateway. + +```shell +kubectl get httproute/http-to-https-filter-redirect -o yaml +``` + +Get the Gateway's address: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +Querying `redirect.example/get` should result in a `301` response from the example Gateway and redirecting to the +configured redirect hostname. + +```console +$ curl -L -vvv --header "Host: redirect.example" "http://${GATEWAY_HOST}/get" +... +< HTTP/1.1 301 Moved Permanently +< location: https://www.example.com/get +... +``` + +If you followed the steps in the [Secure Gateways](../security/secure-gateways) task, you should be able to curl the redirect +location. + +## HTTP --> HTTPS + +Listeners expose the TLS setting on a per domain or subdomain basis. TLS settings of a listener are applied to all domains that satisfy the hostname criteria. + +Create a root certificate and private key to sign certificates: + +```shell +openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -subj '/CN=example.com' -keyout CA.key -out CA.crt +openssl req -out example.com.csr -newkey rsa:2048 -nodes -keyout tls.key -subj "/CN=example.com" +``` + +Generate a self-signed wildcard certificate for `example.com` with `*.example.com` extension + +```shell +cat <}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Check for any TLS certificate issues on the gateway. + +```bash +kubectl -n default describe gateway eg +``` + +Create two HTTPRoutes and attach them to the HTTP and HTTPS listeners using the [sectionName][] field. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Curl the example app through http listener: + +```bash +curl --verbose --header "Host: www.example.com" http://$GATEWAY_HOST/get +``` + +Curl the example app through https listener: + +```bash +curl -v -H 'Host:www.example.com' --resolve "www.example.com:443:$GATEWAY_HOST" \ +--cacert CA.crt https://www.example.com:443/get +``` + + +## Path Redirects + +Path redirects use an HTTP Path Modifier to replace either entire paths or path prefixes. For example, the HTTPRoute +below will issue a 302 redirect to all `path.redirect.example` requests whose path begins with `/get` to `/status/200`. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The HTTPRoute status should indicate that it has been accepted and is bound to the example Gateway. + +```shell +kubectl get httproute/http-filter-path-redirect -o yaml +``` + +Querying `path.redirect.example` should result in a `302` response from the example Gateway and a redirect location +containing the configured redirect path. + +Query the `path.redirect.example` host: + +```shell +curl -vvv --header "Host: path.redirect.example" "http://${GATEWAY_HOST}/get" +``` + +You should receive a `302` with a redirect location of `http://path.redirect.example/status/200`. + +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute/ +[HTTPRoute filters]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.HTTPRouteFilter +[Gateway API documentation]: https://gateway-api.sigs.k8s.io/ +[req_filter]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.HTTPRequestRedirectFilter +[sectionName]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.CommonRouteSpec diff --git a/site/content/en/v1.0.2/tasks/traffic/http-request-headers.md b/site/content/en/v1.0/tasks/traffic/http-request-headers.md similarity index 100% rename from site/content/en/v1.0.2/tasks/traffic/http-request-headers.md rename to site/content/en/v1.0/tasks/traffic/http-request-headers.md diff --git a/site/content/en/v1.0.2/tasks/traffic/http-request-mirroring.md b/site/content/en/v1.0/tasks/traffic/http-request-mirroring.md similarity index 100% rename from site/content/en/v1.0.2/tasks/traffic/http-request-mirroring.md rename to site/content/en/v1.0/tasks/traffic/http-request-mirroring.md diff --git a/site/content/en/v1.0.2/tasks/traffic/http-response-headers.md b/site/content/en/v1.0/tasks/traffic/http-response-headers.md similarity index 100% rename from site/content/en/v1.0.2/tasks/traffic/http-response-headers.md rename to site/content/en/v1.0/tasks/traffic/http-response-headers.md diff --git a/site/content/en/v1.0.2/tasks/traffic/http-routing.md b/site/content/en/v1.0/tasks/traffic/http-routing.md similarity index 100% rename from site/content/en/v1.0.2/tasks/traffic/http-routing.md rename to site/content/en/v1.0/tasks/traffic/http-routing.md diff --git a/site/content/en/v1.0/tasks/traffic/http-timeouts.md b/site/content/en/v1.0/tasks/traffic/http-timeouts.md new file mode 100644 index 00000000000..1eb9beabb24 --- /dev/null +++ b/site/content/en/v1.0/tasks/traffic/http-timeouts.md @@ -0,0 +1,199 @@ +--- +title: "HTTP Timeouts" +--- + +The default request timeout is set to 15 seconds in Envoy Proxy. +The [HTTPRouteTimeouts][] resource allows users to configure request timeouts for an [HTTPRouteRule][]. +This task shows you how to configure timeouts. + +The [HTTPRouteTimeouts][] supports two kinds of timeouts: +- **request**: Request specifies the maximum duration for a gateway to respond to an HTTP request. +- **backendRequest**: BackendRequest specifies a timeout for an individual request from the gateway to a backend. + +__Note:__ The Request duration must be >= BackendRequest duration + +## Installation + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## Verification + +backend has the ability to delay responses; we use it as the backend to control response time. + +### request timeout +We configure the backend to delay responses by 3 seconds, then we set the request timeout to 4 seconds. Envoy Gateway will successfully respond to the request. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +```shell +curl --header "Host: timeout.example.com" http://${GATEWAY_HOST}/?delay=3s -I +``` + +```console +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Mon, 04 Mar 2024 02:34:21 GMT +content-length: 480 +``` + +Then we set the request timeout to 2 seconds. In this case, Envoy Gateway will respond with a timeout. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +```shell +curl --header "Host: timeout.example.com" http://${GATEWAY_HOST}/?delay=3s -v +``` + +```console +* Trying 127.0.0.1:80... +* Connected to 127.0.0.1 (127.0.0.1) port 80 +> GET /?delay=3s HTTP/1.1 +> Host: timeout.example.com +> User-Agent: curl/8.6.0 +> Accept: */* +> + + +< HTTP/1.1 504 Gateway Timeout +< content-length: 24 +< content-type: text/plain +< date: Mon, 04 Mar 2024 02:35:03 GMT +< +* Connection #0 to host 127.0.0.1 left intact +upstream request timeout +``` + +[HTTPRouteTimeouts]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRouteTimeouts +[HTTPRouteRule]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRouteRule diff --git a/site/content/en/v1.0.2/tasks/traffic/http-traffic-splitting.md b/site/content/en/v1.0/tasks/traffic/http-traffic-splitting.md similarity index 100% rename from site/content/en/v1.0.2/tasks/traffic/http-traffic-splitting.md rename to site/content/en/v1.0/tasks/traffic/http-traffic-splitting.md diff --git a/site/content/en/v1.0.2/tasks/traffic/http-urlrewrite.md b/site/content/en/v1.0/tasks/traffic/http-urlrewrite.md similarity index 100% rename from site/content/en/v1.0.2/tasks/traffic/http-urlrewrite.md rename to site/content/en/v1.0/tasks/traffic/http-urlrewrite.md diff --git a/site/content/en/v1.0.2/tasks/traffic/http3.md b/site/content/en/v1.0/tasks/traffic/http3.md similarity index 100% rename from site/content/en/v1.0.2/tasks/traffic/http3.md rename to site/content/en/v1.0/tasks/traffic/http3.md diff --git a/site/content/en/v1.0.2/tasks/traffic/local-rate-limit.md b/site/content/en/v1.0/tasks/traffic/local-rate-limit.md similarity index 100% rename from site/content/en/v1.0.2/tasks/traffic/local-rate-limit.md rename to site/content/en/v1.0/tasks/traffic/local-rate-limit.md diff --git a/site/content/en/v1.0.2/tasks/traffic/multicluster-service.md b/site/content/en/v1.0/tasks/traffic/multicluster-service.md similarity index 100% rename from site/content/en/v1.0.2/tasks/traffic/multicluster-service.md rename to site/content/en/v1.0/tasks/traffic/multicluster-service.md diff --git a/site/content/en/v1.0.2/tasks/traffic/retry.md b/site/content/en/v1.0/tasks/traffic/retry.md similarity index 100% rename from site/content/en/v1.0.2/tasks/traffic/retry.md rename to site/content/en/v1.0/tasks/traffic/retry.md diff --git a/site/content/en/v1.0.2/tasks/traffic/routing-outside-kubernetes.md b/site/content/en/v1.0/tasks/traffic/routing-outside-kubernetes.md similarity index 100% rename from site/content/en/v1.0.2/tasks/traffic/routing-outside-kubernetes.md rename to site/content/en/v1.0/tasks/traffic/routing-outside-kubernetes.md diff --git a/site/content/en/v1.0.2/tasks/traffic/tcp-routing.md b/site/content/en/v1.0/tasks/traffic/tcp-routing.md similarity index 100% rename from site/content/en/v1.0.2/tasks/traffic/tcp-routing.md rename to site/content/en/v1.0/tasks/traffic/tcp-routing.md diff --git a/site/content/en/v1.0.2/tasks/traffic/udp-routing.md b/site/content/en/v1.0/tasks/traffic/udp-routing.md similarity index 100% rename from site/content/en/v1.0.2/tasks/traffic/udp-routing.md rename to site/content/en/v1.0/tasks/traffic/udp-routing.md diff --git a/site/content/en/v1.1/_index.md b/site/content/en/v1.1/_index.md new file mode 100644 index 00000000000..92ae8586885 --- /dev/null +++ b/site/content/en/v1.1/_index.md @@ -0,0 +1,15 @@ ++++ +title = "Welcome to Envoy Gateway" +linktitle = "Documentation" +description = "Envoy Gateway Documents" + +[[cascade]] +type = "docs" ++++ + +Envoy Gateway is an open source project for managing [Envoy Proxy](https://www.envoyproxy.io/) as a standalone or Kubernetes-based application +gateway. [Gateway API](https://gateway-api.sigs.k8s.io/) resources are used to dynamically provision and configure the managed Envoy Proxies. + +![architecture](/img/traffic.png) + +## Ready to get started? diff --git a/site/content/en/v1.1/api/_index.md b/site/content/en/v1.1/api/_index.md new file mode 100644 index 00000000000..396d9ffcefc --- /dev/null +++ b/site/content/en/v1.1/api/_index.md @@ -0,0 +1,5 @@ +--- +title: "API" +description: This section includes APIs of Envoy Gateway. +weight: 80 +--- diff --git a/site/content/en/v1.1/api/extension_types.md b/site/content/en/v1.1/api/extension_types.md new file mode 100644 index 00000000000..4dc4ccd890c --- /dev/null +++ b/site/content/en/v1.1/api/extension_types.md @@ -0,0 +1,3861 @@ ++++ +title = "API Reference" ++++ + + +## Packages +- [gateway.envoyproxy.io/v1alpha1](#gatewayenvoyproxyiov1alpha1) + + +## gateway.envoyproxy.io/v1alpha1 + +Package v1alpha1 contains API schema definitions for the gateway.envoyproxy.io +API group. + + +### Resource Types +- [Backend](#backend) +- [BackendList](#backendlist) +- [BackendTrafficPolicy](#backendtrafficpolicy) +- [BackendTrafficPolicyList](#backendtrafficpolicylist) +- [ClientTrafficPolicy](#clienttrafficpolicy) +- [ClientTrafficPolicyList](#clienttrafficpolicylist) +- [EnvoyExtensionPolicy](#envoyextensionpolicy) +- [EnvoyExtensionPolicyList](#envoyextensionpolicylist) +- [EnvoyGateway](#envoygateway) +- [EnvoyPatchPolicy](#envoypatchpolicy) +- [EnvoyPatchPolicyList](#envoypatchpolicylist) +- [EnvoyProxy](#envoyproxy) +- [SecurityPolicy](#securitypolicy) +- [SecurityPolicyList](#securitypolicylist) + + + +#### ALPNProtocol + +_Underlying type:_ _string_ + +ALPNProtocol specifies the protocol to be negotiated using ALPN + +_Appears in:_ +- [BackendTLSConfig](#backendtlsconfig) +- [ClientTLSSettings](#clienttlssettings) +- [TLSSettings](#tlssettings) + +| Value | Description | +| ----- | ----------- | +| `http/1.0` | HTTPProtocolVersion1_0 specifies that HTTP/1.0 should be negotiable with ALPN
| +| `http/1.1` | HTTPProtocolVersion1_1 specifies that HTTP/1.1 should be negotiable with ALPN
| +| `h2` | HTTPProtocolVersion2 specifies that HTTP/2 should be negotiable with ALPN
| + + +#### ALSEnvoyProxyAccessLog + + + +ALSEnvoyProxyAccessLog defines the gRPC Access Log Service (ALS) sink. +The service must implement the Envoy gRPC Access Log Service streaming API: +https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/accesslog/v3/als.proto +Access log format information is passed in the form of gRPC metadata when the +stream is established. Specifically, the following metadata is passed: + + +- `x-accesslog-text` - The access log format string when a Text format is used. +- `x-accesslog-attr` - JSON encoded key/value pairs when a JSON format is used. + +_Appears in:_ +- [ProxyAccessLogSink](#proxyaccesslogsink) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `backendRefs` | _[BackendRef](#backendref) array_ | true | BackendRefs references a Kubernetes object that represents the gRPC service to which
the access logs will be sent. Currently only Service is supported. | +| `logName` | _string_ | false | LogName defines the friendly name of the access log to be returned in
StreamAccessLogsMessage.Identifier. This allows the access log server
to differentiate between different access logs coming from the same Envoy. | +| `type` | _[ALSEnvoyProxyAccessLogType](#alsenvoyproxyaccesslogtype)_ | true | Type defines the type of accesslog. Supported types are "HTTP" and "TCP". | +| `http` | _[ALSEnvoyProxyHTTPAccessLogConfig](#alsenvoyproxyhttpaccesslogconfig)_ | false | HTTP defines additional configuration specific to HTTP access logs. | + + +#### ALSEnvoyProxyAccessLogType + +_Underlying type:_ _string_ + + + +_Appears in:_ +- [ALSEnvoyProxyAccessLog](#alsenvoyproxyaccesslog) + +| Value | Description | +| ----- | ----------- | +| `HTTP` | ALSEnvoyProxyAccessLogTypeHTTP defines the HTTP access log type and will populate StreamAccessLogsMessage.http_logs.
| +| `TCP` | ALSEnvoyProxyAccessLogTypeTCP defines the TCP access log type and will populate StreamAccessLogsMessage.tcp_logs.
| + + +#### ALSEnvoyProxyHTTPAccessLogConfig + + + + + +_Appears in:_ +- [ALSEnvoyProxyAccessLog](#alsenvoyproxyaccesslog) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `requestHeaders` | _string array_ | false | RequestHeaders defines request headers to include in log entries sent to the access log service. | +| `responseHeaders` | _string array_ | false | ResponseHeaders defines response headers to include in log entries sent to the access log service. | +| `responseTrailers` | _string array_ | false | ResponseTrailers defines response trailers to include in log entries sent to the access log service. | + + +#### ActiveHealthCheck + + + +ActiveHealthCheck defines the active health check configuration. +EG supports various types of active health checking including HTTP, TCP. + +_Appears in:_ +- [HealthCheck](#healthcheck) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `timeout` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | false | Timeout defines the time to wait for a health check response. | +| `interval` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | false | Interval defines the time between active health checks. | +| `unhealthyThreshold` | _integer_ | false | UnhealthyThreshold defines the number of unhealthy health checks required before a backend host is marked unhealthy. | +| `healthyThreshold` | _integer_ | false | HealthyThreshold defines the number of healthy health checks required before a backend host is marked healthy. | +| `type` | _[ActiveHealthCheckerType](#activehealthcheckertype)_ | true | Type defines the type of health checker. | +| `http` | _[HTTPActiveHealthChecker](#httpactivehealthchecker)_ | false | HTTP defines the configuration of http health checker.
It's required while the health checker type is HTTP. | +| `tcp` | _[TCPActiveHealthChecker](#tcpactivehealthchecker)_ | false | TCP defines the configuration of tcp health checker.
It's required while the health checker type is TCP. | + + +#### ActiveHealthCheckPayload + + + +ActiveHealthCheckPayload defines the encoding of the payload bytes in the payload. + +_Appears in:_ +- [HTTPActiveHealthChecker](#httpactivehealthchecker) +- [TCPActiveHealthChecker](#tcpactivehealthchecker) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[ActiveHealthCheckPayloadType](#activehealthcheckpayloadtype)_ | true | Type defines the type of the payload. | +| `text` | _string_ | false | Text payload in plain text. | +| `binary` | _integer array_ | false | Binary payload base64 encoded. | + + +#### ActiveHealthCheckPayloadType + +_Underlying type:_ _string_ + +ActiveHealthCheckPayloadType is the type of the payload. + +_Appears in:_ +- [ActiveHealthCheckPayload](#activehealthcheckpayload) + +| Value | Description | +| ----- | ----------- | +| `Text` | ActiveHealthCheckPayloadTypeText defines the Text type payload.
| +| `Binary` | ActiveHealthCheckPayloadTypeBinary defines the Binary type payload.
| + + +#### ActiveHealthCheckerType + +_Underlying type:_ _string_ + +ActiveHealthCheckerType is the type of health checker. + +_Appears in:_ +- [ActiveHealthCheck](#activehealthcheck) + +| Value | Description | +| ----- | ----------- | +| `HTTP` | ActiveHealthCheckerTypeHTTP defines the HTTP type of health checking.
| +| `TCP` | ActiveHealthCheckerTypeTCP defines the TCP type of health checking.
| + + +#### AppProtocolType + +_Underlying type:_ _string_ + +AppProtocolType defines various backend applications protocols supported by Envoy Gateway + +_Appears in:_ +- [BackendSpec](#backendspec) + +| Value | Description | +| ----- | ----------- | +| `gateway.envoyproxy.io/h2c` | AppProtocolTypeH2C defines the HTTP/2 application protocol.
| +| `gateway.envoyproxy.io/ws` | AppProtocolTypeWS defines the WebSocket over HTTP protocol.
| +| `gateway.envoyproxy.io/wss` | AppProtocolTypeWSS defines the WebSocket over HTTPS protocol.
| + + +#### Authorization + + + +Authorization defines the authorization configuration. + + +Note: if neither `Rules` nor `DefaultAction` is specified, the default action is to deny all requests. + +_Appears in:_ +- [SecurityPolicySpec](#securitypolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `rules` | _[AuthorizationRule](#authorizationrule) array_ | false | Rules defines a list of authorization rules.
These rules are evaluated in order, the first matching rule will be applied,
and the rest will be skipped.

For example, if there are two rules: the first rule allows the request
and the second rule denies it, when a request matches both rules, it will be allowed. | +| `defaultAction` | _[AuthorizationAction](#authorizationaction)_ | false | DefaultAction defines the default action to be taken if no rules match.
If not specified, the default action is Deny. | + + +#### AuthorizationAction + +_Underlying type:_ _string_ + +AuthorizationAction defines the action to be taken if a rule matches. + +_Appears in:_ +- [Authorization](#authorization) +- [AuthorizationRule](#authorizationrule) + +| Value | Description | +| ----- | ----------- | +| `Allow` | AuthorizationActionAllow is the action to allow the request.
| +| `Deny` | AuthorizationActionDeny is the action to deny the request.
| + + +#### AuthorizationRule + + + +AuthorizationRule defines a single authorization rule. + +_Appears in:_ +- [Authorization](#authorization) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `name` | _string_ | false | Name is a user-friendly name for the rule.
If not specified, Envoy Gateway will generate a unique name for the rule.n | +| `action` | _[AuthorizationAction](#authorizationaction)_ | true | Action defines the action to be taken if the rule matches. | +| `principal` | _[Principal](#principal)_ | true | Principal specifies the client identity of a request. | + + +#### BackOffPolicy + + + + + +_Appears in:_ +- [PerRetryPolicy](#perretrypolicy) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `baseInterval` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | true | BaseInterval is the base interval between retries. | +| `maxInterval` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | false | MaxInterval is the maximum interval between retries. This parameter is optional, but must be greater than or equal to the base_interval if set.
The default is 10 times the base_interval | + + +#### Backend + + + +Backend allows the user to configure the endpoints of a backend and +the behavior of the connection from Envoy Proxy to the backend. + +_Appears in:_ +- [BackendList](#backendlist) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `apiVersion` | _string_ | |`gateway.envoyproxy.io/v1alpha1` +| `kind` | _string_ | |`Backend` +| `metadata` | _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#objectmeta-v1-meta)_ | true | Refer to Kubernetes API documentation for fields of `metadata`. | +| `spec` | _[BackendSpec](#backendspec)_ | true | Spec defines the desired state of Backend. | +| `status` | _[BackendStatus](#backendstatus)_ | true | Status defines the current status of Backend. | + + + + + + +#### BackendConnection + + + +BackendConnection allows users to configure connection-level settings of backend + +_Appears in:_ +- [BackendTrafficPolicySpec](#backendtrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `bufferLimit` | _[Quantity](#quantity)_ | false | BufferLimit Soft limit on size of the cluster’s connections read and write buffers.
If unspecified, an implementation defined default is applied (32768 bytes).
For example, 20Mi, 1Gi, 256Ki etc.
Note: that when the suffix is not provided, the value is interpreted as bytes. | + + +#### BackendEndpoint + + + +BackendEndpoint describes a backend endpoint, which can be either a fully-qualified domain name, IP address or unix domain socket +corresponding to Envoy's Address: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/address.proto#config-core-v3-address + +_Appears in:_ +- [BackendSpec](#backendspec) +- [ExtensionService](#extensionservice) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `fqdn` | _[FQDNEndpoint](#fqdnendpoint)_ | false | FQDN defines a FQDN endpoint | +| `ip` | _[IPEndpoint](#ipendpoint)_ | false | IP defines an IP endpoint. Currently, only IPv4 Addresses are supported. | +| `unix` | _[UnixSocket](#unixsocket)_ | false | Unix defines the unix domain socket endpoint | + + +#### BackendList + + + +BackendList contains a list of Backend resources. + + + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `apiVersion` | _string_ | |`gateway.envoyproxy.io/v1alpha1` +| `kind` | _string_ | |`BackendList` +| `metadata` | _[ListMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#listmeta-v1-meta)_ | true | Refer to Kubernetes API documentation for fields of `metadata`. | +| `items` | _[Backend](#backend) array_ | true | | + + +#### BackendRef + + + +BackendRef defines how an ObjectReference that is specific to BackendRef. + +_Appears in:_ +- [ALSEnvoyProxyAccessLog](#alsenvoyproxyaccesslog) +- [ExtProc](#extproc) +- [GRPCExtAuthService](#grpcextauthservice) +- [HTTPExtAuthService](#httpextauthservice) +- [OpenTelemetryEnvoyProxyAccessLog](#opentelemetryenvoyproxyaccesslog) +- [ProxyOpenTelemetrySink](#proxyopentelemetrysink) +- [TracingProvider](#tracingprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `group` | _[Group](#group)_ | false | Group is the group of the referent. For example, "gateway.networking.k8s.io".
When unspecified or empty string, core API group is inferred. | +| `kind` | _[Kind](#kind)_ | false | Kind is the Kubernetes resource kind of the referent. For example
"Service".

Defaults to "Service" when not specified.

ExternalName services can refer to CNAME DNS records that may live
outside of the cluster and as such are difficult to reason about in
terms of conformance. They also may not be safe to forward to (see
CVE-2021-25740 for more information). Implementations SHOULD NOT
support ExternalName Services.

Support: Core (Services with a type other than ExternalName)

Support: Implementation-specific (Services with type ExternalName) | +| `name` | _[ObjectName](#objectname)_ | true | Name is the name of the referent. | +| `namespace` | _[Namespace](#namespace)_ | false | Namespace is the namespace of the backend. When unspecified, the local
namespace is inferred.

Note that when a namespace different than the local namespace is specified,
a ReferenceGrant object is required in the referent namespace to allow that
namespace's owner to accept the reference. See the ReferenceGrant
documentation for details.

Support: Core | +| `port` | _[PortNumber](#portnumber)_ | false | Port specifies the destination port number to use for this resource.
Port is required when the referent is a Kubernetes Service. In this
case, the port number is the service port number, not the target port.
For other resources, destination port might be derived from the referent
resource or this field. | + + +#### BackendSpec + + + +BackendSpec describes the desired state of BackendSpec. + +_Appears in:_ +- [Backend](#backend) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `endpoints` | _[BackendEndpoint](#backendendpoint) array_ | true | Endpoints defines the endpoints to be used when connecting to the backend. | +| `appProtocols` | _[AppProtocolType](#appprotocoltype) array_ | false | AppProtocols defines the application protocols to be supported when connecting to the backend. | + + +#### BackendStatus + + + +BackendStatus defines the state of Backend + +_Appears in:_ +- [Backend](#backend) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `conditions` | _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#condition-v1-meta) array_ | false | Conditions describe the current conditions of the Backend. | + + +#### BackendTLSConfig + + + +BackendTLSConfig describes the BackendTLS configuration for Envoy Proxy. + +_Appears in:_ +- [EnvoyProxySpec](#envoyproxyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `clientCertificateRef` | _[SecretObjectReference](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.SecretObjectReference)_ | false | ClientCertificateRef defines the reference to a Kubernetes Secret that contains
the client certificate and private key for Envoy to use when connecting to
backend services and external services, such as ExtAuth, ALS, OpenTelemetry, etc.
This secret should be located within the same namespace as the Envoy proxy resource that references it. | +| `minVersion` | _[TLSVersion](#tlsversion)_ | false | Min specifies the minimal TLS protocol version to allow.
The default is TLS 1.2 if this is not specified. | +| `maxVersion` | _[TLSVersion](#tlsversion)_ | false | Max specifies the maximal TLS protocol version to allow
The default is TLS 1.3 if this is not specified. | +| `ciphers` | _string array_ | false | Ciphers specifies the set of cipher suites supported when
negotiating TLS 1.0 - 1.2. This setting has no effect for TLS 1.3.
In non-FIPS Envoy Proxy builds the default cipher 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
In builds using BoringSSL FIPS the default cipher list is:
- ECDHE-ECDSA-AES128-GCM-SHA256
- ECDHE-RSA-AES128-GCM-SHA256
- ECDHE-ECDSA-AES256-GCM-SHA384
- ECDHE-RSA-AES256-GCM-SHA384 | +| `ecdhCurves` | _string array_ | false | ECDHCurves specifies the set of supported ECDH curves.
In non-FIPS Envoy Proxy builds the default curves are:
- X25519
- P-256
In builds using BoringSSL FIPS the default curve is:
- P-256 | +| `signatureAlgorithms` | _string array_ | false | SignatureAlgorithms specifies which signature algorithms the listener should
support. | +| `alpnProtocols` | _[ALPNProtocol](#alpnprotocol) array_ | false | ALPNProtocols supplies the list of ALPN protocols that should be
exposed by the listener. By default h2 and http/1.1 are enabled.
Supported values are:
- http/1.0
- http/1.1
- h2 | + + +#### BackendTrafficPolicy + + + +BackendTrafficPolicy allows the user to configure the behavior of the connection +between the Envoy Proxy listener and the backend service. + +_Appears in:_ +- [BackendTrafficPolicyList](#backendtrafficpolicylist) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `apiVersion` | _string_ | |`gateway.envoyproxy.io/v1alpha1` +| `kind` | _string_ | |`BackendTrafficPolicy` +| `metadata` | _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#objectmeta-v1-meta)_ | true | Refer to Kubernetes API documentation for fields of `metadata`. | +| `spec` | _[BackendTrafficPolicySpec](#backendtrafficpolicyspec)_ | true | spec defines the desired state of BackendTrafficPolicy. | +| `status` | _[PolicyStatus](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.PolicyStatus)_ | true | status defines the current status of BackendTrafficPolicy. | + + + + +#### BackendTrafficPolicyList + + + +BackendTrafficPolicyList contains a list of BackendTrafficPolicy resources. + + + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `apiVersion` | _string_ | |`gateway.envoyproxy.io/v1alpha1` +| `kind` | _string_ | |`BackendTrafficPolicyList` +| `metadata` | _[ListMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#listmeta-v1-meta)_ | true | Refer to Kubernetes API documentation for fields of `metadata`. | +| `items` | _[BackendTrafficPolicy](#backendtrafficpolicy) array_ | true | | + + +#### BackendTrafficPolicySpec + + + +BackendTrafficPolicySpec defines the desired state of BackendTrafficPolicy. + +_Appears in:_ +- [BackendTrafficPolicy](#backendtrafficpolicy) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `targetRef` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName)_ | true | TargetRef is the name of the resource this policy is being attached to.
This policy and the TargetRef MUST be in the same namespace for this
Policy to have effect

Deprecated: use targetRefs/targetSelectors instead | +| `targetRefs` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName) array_ | true | TargetRefs are the names of the Gateway resources this policy
is being attached to. | +| `targetSelectors` | _[TargetSelector](#targetselector) array_ | true | TargetSelectors allow targeting resources for this policy based on labels | +| `rateLimit` | _[RateLimitSpec](#ratelimitspec)_ | false | RateLimit allows the user to limit the number of incoming requests
to a predefined value based on attributes within the traffic flow. | +| `loadBalancer` | _[LoadBalancer](#loadbalancer)_ | false | LoadBalancer policy to apply when routing traffic from the gateway to
the backend endpoints | +| `proxyProtocol` | _[ProxyProtocol](#proxyprotocol)_ | false | ProxyProtocol enables the Proxy Protocol when communicating with the backend. | +| `tcpKeepalive` | _[TCPKeepalive](#tcpkeepalive)_ | false | TcpKeepalive settings associated with the upstream client connection.
Disabled by default. | +| `healthCheck` | _[HealthCheck](#healthcheck)_ | false | HealthCheck allows gateway to perform active health checking on backends. | +| `faultInjection` | _[FaultInjection](#faultinjection)_ | false | FaultInjection defines the fault injection policy to be applied. This configuration can be used to
inject delays and abort requests to mimic failure scenarios such as service failures and overloads | +| `circuitBreaker` | _[CircuitBreaker](#circuitbreaker)_ | false | Circuit Breaker settings for the upstream connections and requests.
If not set, circuit breakers will be enabled with the default thresholds | +| `retry` | _[Retry](#retry)_ | false | Retry provides more advanced usage, allowing users to customize the number of retries, retry fallback strategy, and retry triggering conditions.
If not set, retry will be disabled. | +| `useClientProtocol` | _boolean_ | false | UseClientProtocol configures Envoy to prefer sending requests to backends using
the same HTTP protocol that the incoming request used. Defaults to false, which means
that Envoy will use the protocol indicated by the attached BackendRef. | +| `timeout` | _[Timeout](#timeout)_ | false | Timeout settings for the backend connections. | +| `connection` | _[BackendConnection](#backendconnection)_ | false | Connection includes backend connection settings. | + + +#### BasicAuth + + + +BasicAuth defines the configuration for the HTTP Basic Authentication. + +_Appears in:_ +- [SecurityPolicySpec](#securitypolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `users` | _[SecretObjectReference](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.SecretObjectReference)_ | true | The Kubernetes secret which contains the username-password pairs in
htpasswd format, used to verify user credentials in the "Authorization"
header.

This is an Opaque secret. The username-password pairs should be stored in
the key ".htpasswd". As the key name indicates, the value needs to be the
htpasswd format, for example: "user1:{SHA}hashed_user1_password".
Right now, only SHA hash algorithm is supported.
Reference to https://httpd.apache.org/docs/2.4/programs/htpasswd.html
for more details.

Note: The secret must be in the same namespace as the SecurityPolicy. | + + +#### BootstrapType + +_Underlying type:_ _string_ + +BootstrapType defines the types of bootstrap supported by Envoy Gateway. + +_Appears in:_ +- [ProxyBootstrap](#proxybootstrap) + +| Value | Description | +| ----- | ----------- | +| `Merge` | Merge merges the provided bootstrap with the default one. The provided bootstrap can add or override a value
within a map, or add a new value to a list.
Please note that the provided bootstrap can't override a value within a list.
| +| `Replace` | Replace replaces the default bootstrap with the provided one.
| + + +#### CIDR + +_Underlying type:_ _string_ + +CIDR defines a CIDR Address range. +A CIDR can be an IPv4 address range such as "192.168.1.0/24" or an IPv6 address range such as "2001:0db8:11a3:09d7::/64". + +_Appears in:_ +- [Principal](#principal) + + + +#### CORS + + + +CORS defines the configuration for Cross-Origin Resource Sharing (CORS). + +_Appears in:_ +- [SecurityPolicySpec](#securitypolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `allowOrigins` | _[Origin](#origin) array_ | true | AllowOrigins defines the origins that are allowed to make requests. | +| `allowMethods` | _string array_ | true | AllowMethods defines the methods that are allowed to make requests. | +| `allowHeaders` | _string array_ | true | AllowHeaders defines the headers that are allowed to be sent with requests. | +| `exposeHeaders` | _string array_ | true | ExposeHeaders defines the headers that can be exposed in the responses. | +| `maxAge` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | true | MaxAge defines how long the results of a preflight request can be cached. | +| `allowCredentials` | _boolean_ | true | AllowCredentials indicates whether a request can include user credentials
like cookies, authentication headers, or TLS client certificates. | + + +#### CircuitBreaker + + + +CircuitBreaker defines the Circuit Breaker configuration. + +_Appears in:_ +- [BackendTrafficPolicySpec](#backendtrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `maxConnections` | _integer_ | false | The maximum number of connections that Envoy will establish to the referenced backend defined within a xRoute rule. | +| `maxPendingRequests` | _integer_ | false | The maximum number of pending requests that Envoy will queue to the referenced backend defined within a xRoute rule. | +| `maxParallelRequests` | _integer_ | false | The maximum number of parallel requests that Envoy will make to the referenced backend defined within a xRoute rule. | +| `maxParallelRetries` | _integer_ | false | The maximum number of parallel retries that Envoy will make to the referenced backend defined within a xRoute rule. | +| `maxRequestsPerConnection` | _integer_ | false | The maximum number of requests that Envoy will make over a single connection to the referenced backend defined within a xRoute rule.
Default: unlimited. | + + +#### ClaimToHeader + + + +ClaimToHeader defines a configuration to convert JWT claims into HTTP headers + +_Appears in:_ +- [JWTProvider](#jwtprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `header` | _string_ | true | Header defines the name of the HTTP request header that the JWT Claim will be saved into. | +| `claim` | _string_ | true | Claim is the JWT Claim that should be saved into the header : it can be a nested claim of type
(eg. "claim.nested.key", "sub"). The nested claim name must use dot "."
to separate the JSON name path. | + + +#### ClientConnection + + + +ClientConnection allows users to configure connection-level settings of client + +_Appears in:_ +- [ClientTrafficPolicySpec](#clienttrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `connectionLimit` | _[ConnectionLimit](#connectionlimit)_ | false | ConnectionLimit defines limits related to connections | +| `bufferLimit` | _[Quantity](#quantity)_ | false | BufferLimit provides configuration for the maximum buffer size in bytes for each incoming connection.
For example, 20Mi, 1Gi, 256Ki etc.
Note that when the suffix is not provided, the value is interpreted as bytes.
Default: 32768 bytes. | + + +#### ClientIPDetectionSettings + + + +ClientIPDetectionSettings provides configuration for determining the original client IP address for requests. + +_Appears in:_ +- [ClientTrafficPolicySpec](#clienttrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `xForwardedFor` | _[XForwardedForSettings](#xforwardedforsettings)_ | false | XForwardedForSettings provides configuration for using X-Forwarded-For headers for determining the client IP address. | +| `customHeader` | _[CustomHeaderExtensionSettings](#customheaderextensionsettings)_ | false | CustomHeader provides configuration for determining the client IP address for a request based on
a trusted custom HTTP header. This uses the custom_header original IP detection extension.
Refer to https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/http/original_ip_detection/custom_header/v3/custom_header.proto
for more details. | + + +#### ClientTLSSettings + + + + + +_Appears in:_ +- [ClientTrafficPolicySpec](#clienttrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `clientValidation` | _[ClientValidationContext](#clientvalidationcontext)_ | false | ClientValidation specifies the configuration to validate the client
initiating the TLS connection to the Gateway listener. | +| `minVersion` | _[TLSVersion](#tlsversion)_ | false | Min specifies the minimal TLS protocol version to allow.
The default is TLS 1.2 if this is not specified. | +| `maxVersion` | _[TLSVersion](#tlsversion)_ | false | Max specifies the maximal TLS protocol version to allow
The default is TLS 1.3 if this is not specified. | +| `ciphers` | _string array_ | false | Ciphers specifies the set of cipher suites supported when
negotiating TLS 1.0 - 1.2. This setting has no effect for TLS 1.3.
In non-FIPS Envoy Proxy builds the default cipher 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
In builds using BoringSSL FIPS the default cipher list is:
- ECDHE-ECDSA-AES128-GCM-SHA256
- ECDHE-RSA-AES128-GCM-SHA256
- ECDHE-ECDSA-AES256-GCM-SHA384
- ECDHE-RSA-AES256-GCM-SHA384 | +| `ecdhCurves` | _string array_ | false | ECDHCurves specifies the set of supported ECDH curves.
In non-FIPS Envoy Proxy builds the default curves are:
- X25519
- P-256
In builds using BoringSSL FIPS the default curve is:
- P-256 | +| `signatureAlgorithms` | _string array_ | false | SignatureAlgorithms specifies which signature algorithms the listener should
support. | +| `alpnProtocols` | _[ALPNProtocol](#alpnprotocol) array_ | false | ALPNProtocols supplies the list of ALPN protocols that should be
exposed by the listener. By default h2 and http/1.1 are enabled.
Supported values are:
- http/1.0
- http/1.1
- h2 | + + +#### ClientTimeout + + + + + +_Appears in:_ +- [ClientTrafficPolicySpec](#clienttrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `tcp` | _[TCPClientTimeout](#tcpclienttimeout)_ | false | Timeout settings for TCP. | +| `http` | _[HTTPClientTimeout](#httpclienttimeout)_ | false | Timeout settings for HTTP. | + + +#### ClientTrafficPolicy + + + +ClientTrafficPolicy allows the user to configure the behavior of the connection +between the downstream client and Envoy Proxy listener. + +_Appears in:_ +- [ClientTrafficPolicyList](#clienttrafficpolicylist) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `apiVersion` | _string_ | |`gateway.envoyproxy.io/v1alpha1` +| `kind` | _string_ | |`ClientTrafficPolicy` +| `metadata` | _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#objectmeta-v1-meta)_ | true | Refer to Kubernetes API documentation for fields of `metadata`. | +| `spec` | _[ClientTrafficPolicySpec](#clienttrafficpolicyspec)_ | true | Spec defines the desired state of ClientTrafficPolicy. | +| `status` | _[PolicyStatus](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.PolicyStatus)_ | true | Status defines the current status of ClientTrafficPolicy. | + + +#### ClientTrafficPolicyList + + + +ClientTrafficPolicyList contains a list of ClientTrafficPolicy resources. + + + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `apiVersion` | _string_ | |`gateway.envoyproxy.io/v1alpha1` +| `kind` | _string_ | |`ClientTrafficPolicyList` +| `metadata` | _[ListMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#listmeta-v1-meta)_ | true | Refer to Kubernetes API documentation for fields of `metadata`. | +| `items` | _[ClientTrafficPolicy](#clienttrafficpolicy) array_ | true | | + + +#### ClientTrafficPolicySpec + + + +ClientTrafficPolicySpec defines the desired state of ClientTrafficPolicy. + +_Appears in:_ +- [ClientTrafficPolicy](#clienttrafficpolicy) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `targetRef` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName)_ | true | TargetRef is the name of the resource this policy is being attached to.
This policy and the TargetRef MUST be in the same namespace for this
Policy to have effect

Deprecated: use targetRefs/targetSelectors instead | +| `targetRefs` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName) array_ | true | TargetRefs are the names of the Gateway resources this policy
is being attached to. | +| `targetSelectors` | _[TargetSelector](#targetselector) array_ | true | TargetSelectors allow targeting resources for this policy based on labels | +| `tcpKeepalive` | _[TCPKeepalive](#tcpkeepalive)_ | false | TcpKeepalive settings associated with the downstream client connection.
If defined, sets SO_KEEPALIVE on the listener socket to enable TCP Keepalives.
Disabled by default. | +| `enableProxyProtocol` | _boolean_ | false | EnableProxyProtocol interprets the ProxyProtocol header and adds the
Client Address into the X-Forwarded-For header.
Note Proxy Protocol must be present when this field is set, else the connection
is closed. | +| `clientIPDetection` | _[ClientIPDetectionSettings](#clientipdetectionsettings)_ | false | ClientIPDetectionSettings provides configuration for determining the original client IP address for requests. | +| `tls` | _[ClientTLSSettings](#clienttlssettings)_ | false | TLS settings configure TLS termination settings with the downstream client. | +| `path` | _[PathSettings](#pathsettings)_ | false | Path enables managing how the incoming path set by clients can be normalized. | +| `headers` | _[HeaderSettings](#headersettings)_ | false | HeaderSettings provides configuration for header management. | +| `timeout` | _[ClientTimeout](#clienttimeout)_ | false | Timeout settings for the client connections. | +| `connection` | _[ClientConnection](#clientconnection)_ | false | Connection includes client connection settings. | +| `http1` | _[HTTP1Settings](#http1settings)_ | false | HTTP1 provides HTTP/1 configuration on the listener. | +| `http2` | _[HTTP2Settings](#http2settings)_ | false | HTTP2 provides HTTP/2 configuration on the listener. | +| `http3` | _[HTTP3Settings](#http3settings)_ | false | HTTP3 provides HTTP/3 configuration on the listener. | +| `healthCheck` | _[HealthCheckSettings](#healthchecksettings)_ | false | HealthCheck provides configuration for determining whether the HTTP/HTTPS listener is healthy. | + + +#### ClientValidationContext + + + +ClientValidationContext holds configuration that can be used to validate the client initiating the TLS connection +to the Gateway. +By default, no client specific configuration is validated. + +_Appears in:_ +- [ClientTLSSettings](#clienttlssettings) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `optional` | _boolean_ | false | Optional set to true accepts connections even when a client doesn't present a certificate.
Defaults to false, which rejects connections without a valid client certificate. | +| `caCertificateRefs` | _[SecretObjectReference](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.SecretObjectReference) array_ | false | CACertificateRefs contains one or more references to
Kubernetes objects that contain TLS certificates of
the Certificate Authorities that can be used
as a trust anchor to validate the certificates presented by the client.

A single reference to a Kubernetes ConfigMap or a Kubernetes Secret,
with the CA certificate in a key named `ca.crt` is currently supported.

References to a resource in different namespace are invalid UNLESS there
is a ReferenceGrant in the target namespace that allows the certificate
to be attached. | + + +#### Compression + + + +Compression defines the config of enabling compression. +This can help reduce the bandwidth at the expense of higher CPU. + +_Appears in:_ +- [BackendTrafficPolicySpec](#backendtrafficpolicyspec) +- [ProxyPrometheusProvider](#proxyprometheusprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[CompressorType](#compressortype)_ | true | CompressorType defines the compressor type to use for compression. | +| `gzip` | _[GzipCompressor](#gzipcompressor)_ | false | The configuration for GZIP compressor. | + + +#### CompressorType + +_Underlying type:_ _string_ + +CompressorType defines the types of compressor library supported by Envoy Gateway. + +_Appears in:_ +- [Compression](#compression) + + + +#### ConnectionLimit + + + + + +_Appears in:_ +- [ClientConnection](#clientconnection) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `value` | _integer_ | true | Value of the maximum concurrent connections limit.
When the limit is reached, incoming connections will be closed after the CloseDelay duration.
Default: unlimited. | +| `closeDelay` | _[Duration](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Duration)_ | false | CloseDelay defines the delay to use before closing connections that are rejected
once the limit value is reached.
Default: none. | + + +#### ConsistentHash + + + +ConsistentHash defines the configuration related to the consistent hash +load balancer policy. + +_Appears in:_ +- [LoadBalancer](#loadbalancer) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[ConsistentHashType](#consistenthashtype)_ | true | ConsistentHashType defines the type of input to hash on. Valid Type values are
"SourceIP",
"Header",
"Cookie". | +| `header` | _[Header](#header)_ | false | Header configures the header hash policy when the consistent hash type is set to Header. | +| `cookie` | _[Cookie](#cookie)_ | false | Cookie configures the cookie hash policy when the consistent hash type is set to Cookie. | +| `tableSize` | _integer_ | false | The table size for consistent hashing, must be prime number limited to 5000011. | + + +#### ConsistentHashType + +_Underlying type:_ _string_ + +ConsistentHashType defines the type of input to hash on. + +_Appears in:_ +- [ConsistentHash](#consistenthash) + +| Value | Description | +| ----- | ----------- | +| `SourceIP` | SourceIPConsistentHashType hashes based on the source IP address.
| +| `Header` | HeaderConsistentHashType hashes based on a request header.
| +| `Cookie` | CookieConsistentHashType hashes based on a cookie.
| + + +#### Cookie + + + +Cookie defines the cookie hashing configuration for consistent hash based +load balancing. + +_Appears in:_ +- [ConsistentHash](#consistenthash) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `name` | _string_ | true | Name of the cookie to hash.
If this cookie does not exist in the request, Envoy will generate a cookie and set
the TTL on the response back to the client based on Layer 4
attributes of the backend endpoint, to ensure that these future requests
go to the same backend endpoint. Make sure to set the TTL field for this case. | +| `ttl` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | false | TTL of the generated cookie if the cookie is not present. This value sets the
Max-Age attribute value. | +| `attributes` | _object (keys:string, values:string)_ | false | Additional Attributes to set for the generated cookie. | + + +#### CustomHeaderExtensionSettings + + + +CustomHeaderExtensionSettings provides configuration for determining the client IP address for a request based on +a trusted custom HTTP header. This uses the the custom_header original IP detection extension. +Refer to https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/http/original_ip_detection/custom_header/v3/custom_header.proto +for more details. + +_Appears in:_ +- [ClientIPDetectionSettings](#clientipdetectionsettings) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `name` | _string_ | true | Name of the header containing the original downstream remote address, if present. | +| `failClosed` | _boolean_ | false | FailClosed is a switch used to control the flow of traffic when client IP detection
fails. If set to true, the listener will respond with 403 Forbidden when the client
IP address cannot be determined. | + + +#### CustomTag + + + + + +_Appears in:_ +- [ProxyTracing](#proxytracing) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[CustomTagType](#customtagtype)_ | true | Type defines the type of custom tag. | +| `literal` | _[LiteralCustomTag](#literalcustomtag)_ | true | Literal adds hard-coded value to each span.
It's required when the type is "Literal". | +| `environment` | _[EnvironmentCustomTag](#environmentcustomtag)_ | true | Environment adds value from environment variable to each span.
It's required when the type is "Environment". | +| `requestHeader` | _[RequestHeaderCustomTag](#requestheadercustomtag)_ | true | RequestHeader adds value from request header to each span.
It's required when the type is "RequestHeader". | + + +#### CustomTagType + +_Underlying type:_ _string_ + + + +_Appears in:_ +- [CustomTag](#customtag) + +| Value | Description | +| ----- | ----------- | +| `Literal` | CustomTagTypeLiteral adds hard-coded value to each span.
| +| `Environment` | CustomTagTypeEnvironment adds value from environment variable to each span.
| +| `RequestHeader` | CustomTagTypeRequestHeader adds value from request header to each span.
| + + +#### EnvironmentCustomTag + + + +EnvironmentCustomTag adds value from environment variable to each span. + +_Appears in:_ +- [CustomTag](#customtag) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `name` | _string_ | true | Name defines the name of the environment variable which to extract the value from. | +| `defaultValue` | _string_ | false | DefaultValue defines the default value to use if the environment variable is not set. | + + +#### EnvoyExtensionPolicy + + + +EnvoyExtensionPolicy allows the user to configure various envoy extensibility options for the Gateway. + +_Appears in:_ +- [EnvoyExtensionPolicyList](#envoyextensionpolicylist) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `apiVersion` | _string_ | |`gateway.envoyproxy.io/v1alpha1` +| `kind` | _string_ | |`EnvoyExtensionPolicy` +| `metadata` | _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#objectmeta-v1-meta)_ | true | Refer to Kubernetes API documentation for fields of `metadata`. | +| `spec` | _[EnvoyExtensionPolicySpec](#envoyextensionpolicyspec)_ | true | Spec defines the desired state of EnvoyExtensionPolicy. | +| `status` | _[PolicyStatus](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.PolicyStatus)_ | true | Status defines the current status of EnvoyExtensionPolicy. | + + +#### EnvoyExtensionPolicyList + + + +EnvoyExtensionPolicyList contains a list of EnvoyExtensionPolicy resources. + + + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `apiVersion` | _string_ | |`gateway.envoyproxy.io/v1alpha1` +| `kind` | _string_ | |`EnvoyExtensionPolicyList` +| `metadata` | _[ListMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#listmeta-v1-meta)_ | true | Refer to Kubernetes API documentation for fields of `metadata`. | +| `items` | _[EnvoyExtensionPolicy](#envoyextensionpolicy) array_ | true | | + + +#### EnvoyExtensionPolicySpec + + + +EnvoyExtensionPolicySpec defines the desired state of EnvoyExtensionPolicy. + +_Appears in:_ +- [EnvoyExtensionPolicy](#envoyextensionpolicy) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `targetRef` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName)_ | true | TargetRef is the name of the resource this policy is being attached to.
This policy and the TargetRef MUST be in the same namespace for this
Policy to have effect

Deprecated: use targetRefs/targetSelectors instead | +| `targetRefs` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName) array_ | true | TargetRefs are the names of the Gateway resources this policy
is being attached to. | +| `targetSelectors` | _[TargetSelector](#targetselector) array_ | true | TargetSelectors allow targeting resources for this policy based on labels | +| `wasm` | _[Wasm](#wasm) array_ | false | Wasm is a list of Wasm extensions to be loaded by the Gateway.
Order matters, as the extensions will be loaded in the order they are
defined in this list. | +| `extProc` | _[ExtProc](#extproc) array_ | false | ExtProc is an ordered list of external processing filters
that should added to the envoy filter chain | + + +#### EnvoyFilter + +_Underlying type:_ _string_ + +EnvoyFilter defines the type of Envoy HTTP filter. + +_Appears in:_ +- [FilterPosition](#filterposition) + +| Value | Description | +| ----- | ----------- | +| `envoy.filters.http.fault` | EnvoyFilterFault defines the Envoy HTTP fault filter.
| +| `envoy.filters.http.cors` | EnvoyFilterCORS defines the Envoy HTTP CORS filter.
| +| `envoy.filters.http.ext_authz` | EnvoyFilterExtAuthz defines the Envoy HTTP external authorization filter.
| +| `envoy.filters.http.basic_authn` | EnvoyFilterBasicAuthn defines the Envoy HTTP basic authentication filter.
| +| `envoy.filters.http.oauth2` | EnvoyFilterOAuth2 defines the Envoy HTTP OAuth2 filter.
| +| `envoy.filters.http.jwt_authn` | EnvoyFilterJWTAuthn defines the Envoy HTTP JWT authentication filter.
| +| `envoy.filters.http.ext_proc` | EnvoyFilterExtProc defines the Envoy HTTP external process filter.
| +| `envoy.filters.http.wasm` | EnvoyFilterWasm defines the Envoy HTTP WebAssembly filter.
| +| `envoy.filters.http.local_ratelimit` | EnvoyFilterLocalRateLimit defines the Envoy HTTP local rate limit filter.
| +| `envoy.filters.http.ratelimit` | EnvoyFilterRateLimit defines the Envoy HTTP rate limit filter.
| +| `envoy.filters.http.rbac` | EnvoyFilterRBAC defines the Envoy RBAC filter.
| +| `envoy.filters.http.router` | EnvoyFilterRouter defines the Envoy HTTP router filter.
| + + +#### EnvoyGateway + + + +EnvoyGateway is the schema for the envoygateways API. + + + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `apiVersion` | _string_ | |`gateway.envoyproxy.io/v1alpha1` +| `kind` | _string_ | |`EnvoyGateway` +| `gateway` | _[Gateway](#gateway)_ | false | Gateway defines desired Gateway API specific configuration. If unset,
default configuration parameters will apply. | +| `provider` | _[EnvoyGatewayProvider](#envoygatewayprovider)_ | false | Provider defines the desired provider and provider-specific configuration.
If unspecified, the Kubernetes provider is used with default configuration
parameters. | +| `logging` | _[EnvoyGatewayLogging](#envoygatewaylogging)_ | false | Logging defines logging parameters for Envoy Gateway. | +| `admin` | _[EnvoyGatewayAdmin](#envoygatewayadmin)_ | false | Admin defines the desired admin related abilities.
If unspecified, the Admin is used with default configuration
parameters. | +| `telemetry` | _[EnvoyGatewayTelemetry](#envoygatewaytelemetry)_ | false | Telemetry defines the desired control plane telemetry related abilities.
If unspecified, the telemetry is used with default configuration. | +| `rateLimit` | _[RateLimit](#ratelimit)_ | false | RateLimit defines the configuration associated with the Rate Limit service
deployed by Envoy Gateway required to implement the Global Rate limiting
functionality. The specific rate limit service used here is the reference
implementation in Envoy. For more details visit https://github.com/envoyproxy/ratelimit.
This configuration is unneeded for "Local" rate limiting. | +| `extensionManager` | _[ExtensionManager](#extensionmanager)_ | false | ExtensionManager defines an extension manager to register for the Envoy Gateway Control Plane. | +| `extensionApis` | _[ExtensionAPISettings](#extensionapisettings)_ | false | ExtensionAPIs defines the settings related to specific Gateway API Extensions
implemented by Envoy Gateway | + + +#### EnvoyGatewayAdmin + + + +EnvoyGatewayAdmin defines the Envoy Gateway Admin configuration. + +_Appears in:_ +- [EnvoyGateway](#envoygateway) +- [EnvoyGatewaySpec](#envoygatewayspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `address` | _[EnvoyGatewayAdminAddress](#envoygatewayadminaddress)_ | false | Address defines the address of Envoy Gateway Admin Server. | +| `enableDumpConfig` | _boolean_ | false | EnableDumpConfig defines if enable dump config in Envoy Gateway logs. | +| `enablePprof` | _boolean_ | false | EnablePprof defines if enable pprof in Envoy Gateway Admin Server. | + + +#### EnvoyGatewayAdminAddress + + + +EnvoyGatewayAdminAddress defines the Envoy Gateway Admin Address configuration. + +_Appears in:_ +- [EnvoyGatewayAdmin](#envoygatewayadmin) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `port` | _integer_ | false | Port defines the port the admin server is exposed on. | +| `host` | _string_ | false | Host defines the admin server hostname. | + + +#### EnvoyGatewayCustomProvider + + + +EnvoyGatewayCustomProvider defines configuration for the Custom provider. + +_Appears in:_ +- [EnvoyGatewayProvider](#envoygatewayprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `resource` | _[EnvoyGatewayResourceProvider](#envoygatewayresourceprovider)_ | true | Resource defines the desired resource provider.
This provider is used to specify the provider to be used
to retrieve the resource configurations such as Gateway API
resources | +| `infrastructure` | _[EnvoyGatewayInfrastructureProvider](#envoygatewayinfrastructureprovider)_ | true | Infrastructure defines the desired infrastructure provider.
This provider is used to specify the provider to be used
to provide an environment to deploy the out resources like
the Envoy Proxy data plane. | + + +#### EnvoyGatewayFileResourceProvider + + + +EnvoyGatewayFileResourceProvider defines configuration for the File Resource provider. + +_Appears in:_ +- [EnvoyGatewayResourceProvider](#envoygatewayresourceprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `paths` | _string array_ | true | Paths are the paths to a directory or file containing the resource configuration.
Recursive sub directories are not currently supported. | + + +#### EnvoyGatewayHostInfrastructureProvider + + + +EnvoyGatewayHostInfrastructureProvider defines configuration for the Host Infrastructure provider. + +_Appears in:_ +- [EnvoyGatewayInfrastructureProvider](#envoygatewayinfrastructureprovider) + + + +#### EnvoyGatewayInfrastructureProvider + + + +EnvoyGatewayInfrastructureProvider defines configuration for the Custom Infrastructure provider. + +_Appears in:_ +- [EnvoyGatewayCustomProvider](#envoygatewaycustomprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[InfrastructureProviderType](#infrastructureprovidertype)_ | true | Type is the type of infrastructure providers to use. Supported types are "Host". | +| `host` | _[EnvoyGatewayHostInfrastructureProvider](#envoygatewayhostinfrastructureprovider)_ | false | Host defines the configuration of the Host provider. Host provides runtime
deployment of the data plane as a child process on the host environment. | + + +#### EnvoyGatewayKubernetesProvider + + + +EnvoyGatewayKubernetesProvider defines configuration for the Kubernetes provider. + +_Appears in:_ +- [EnvoyGatewayProvider](#envoygatewayprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `rateLimitDeployment` | _[KubernetesDeploymentSpec](#kubernetesdeploymentspec)_ | false | RateLimitDeployment defines the desired state of the Envoy ratelimit deployment resource.
If unspecified, default settings for the managed Envoy ratelimit deployment resource
are applied. | +| `watch` | _[KubernetesWatchMode](#kuberneteswatchmode)_ | false | Watch holds configuration of which input resources should be watched and reconciled. | +| `deploy` | _[KubernetesDeployMode](#kubernetesdeploymode)_ | false | Deploy holds configuration of how output managed resources such as the Envoy Proxy data plane
should be deployed | +| `overwriteControlPlaneCerts` | _boolean_ | false | OverwriteControlPlaneCerts updates the secrets containing the control plane certs, when set. | +| `leaderElection` | _[LeaderElection](#leaderelection)_ | false | LeaderElection specifies the configuration for leader election.
If it's not set up, leader election will be active by default, using Kubernetes' standard settings. | +| `shutdownManager` | _[ShutdownManager](#shutdownmanager)_ | false | ShutdownManager defines the configuration for the shutdown manager. | + + +#### EnvoyGatewayLogComponent + +_Underlying type:_ _string_ + +EnvoyGatewayLogComponent defines a component that supports a configured logging level. + +_Appears in:_ +- [EnvoyGatewayLogging](#envoygatewaylogging) + +| Value | Description | +| ----- | ----------- | +| `default` | LogComponentGatewayDefault defines the "default"-wide logging component. When specified,
all other logging components are ignored.
| +| `provider` | LogComponentProviderRunner defines the "provider" runner component.
| +| `gateway-api` | LogComponentGatewayAPIRunner defines the "gateway-api" runner component.
| +| `xds-translator` | LogComponentXdsTranslatorRunner defines the "xds-translator" runner component.
| +| `xds-server` | LogComponentXdsServerRunner defines the "xds-server" runner component.
| +| `infrastructure` | LogComponentInfrastructureRunner defines the "infrastructure" runner component.
| +| `global-ratelimit` | LogComponentGlobalRateLimitRunner defines the "global-ratelimit" runner component.
| + + +#### EnvoyGatewayLogging + + + +EnvoyGatewayLogging defines logging for Envoy Gateway. + +_Appears in:_ +- [EnvoyGateway](#envoygateway) +- [EnvoyGatewaySpec](#envoygatewayspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `level` | _object (keys:[EnvoyGatewayLogComponent](#envoygatewaylogcomponent), values:[LogLevel](#loglevel))_ | true | Level is the logging level. If unspecified, defaults to "info".
EnvoyGatewayLogComponent options: default/provider/gateway-api/xds-translator/xds-server/infrastructure/global-ratelimit.
LogLevel options: debug/info/error/warn. | + + +#### EnvoyGatewayMetricSink + + + +EnvoyGatewayMetricSink defines control plane +metric sinks where metrics are sent to. + +_Appears in:_ +- [EnvoyGatewayMetrics](#envoygatewaymetrics) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[MetricSinkType](#metricsinktype)_ | true | Type defines the metric sink type.
EG control plane currently supports OpenTelemetry. | +| `openTelemetry` | _[EnvoyGatewayOpenTelemetrySink](#envoygatewayopentelemetrysink)_ | true | OpenTelemetry defines the configuration for OpenTelemetry sink.
It's required if the sink type is OpenTelemetry. | + + +#### EnvoyGatewayMetrics + + + +EnvoyGatewayMetrics defines control plane push/pull metrics configurations. + +_Appears in:_ +- [EnvoyGatewayTelemetry](#envoygatewaytelemetry) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `sinks` | _[EnvoyGatewayMetricSink](#envoygatewaymetricsink) array_ | true | Sinks defines the metric sinks where metrics are sent to. | +| `prometheus` | _[EnvoyGatewayPrometheusProvider](#envoygatewayprometheusprovider)_ | true | Prometheus defines the configuration for prometheus endpoint. | + + +#### EnvoyGatewayOpenTelemetrySink + + + + + +_Appears in:_ +- [EnvoyGatewayMetricSink](#envoygatewaymetricsink) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `host` | _string_ | true | Host define the sink service hostname. | +| `protocol` | _string_ | true | Protocol define the sink service protocol. | +| `port` | _integer_ | false | Port defines the port the sink service is exposed on. | +| `exportInterval` | _[Duration](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Duration)_ | true | ExportInterval configures the intervening time between exports for a
Sink. This option overrides any value set for the
OTEL_METRIC_EXPORT_INTERVAL environment variable.
If ExportInterval is less than or equal to zero, 60 seconds
is used as the default. | +| `exportTimeout` | _[Duration](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Duration)_ | true | ExportTimeout configures the time a Sink waits for an export to
complete before canceling it. This option overrides any value set for the
OTEL_METRIC_EXPORT_TIMEOUT environment variable.
If ExportTimeout is less than or equal to zero, 30 seconds
is used as the default. | + + +#### EnvoyGatewayPrometheusProvider + + + +EnvoyGatewayPrometheusProvider will expose prometheus endpoint in pull mode. + +_Appears in:_ +- [EnvoyGatewayMetrics](#envoygatewaymetrics) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `disable` | _boolean_ | true | Disable defines if disables the prometheus metrics in pull mode. | + + +#### EnvoyGatewayProvider + + + +EnvoyGatewayProvider defines the desired configuration of a provider. + +_Appears in:_ +- [EnvoyGateway](#envoygateway) +- [EnvoyGatewaySpec](#envoygatewayspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[ProviderType](#providertype)_ | true | Type is the type of provider to use. Supported types are "Kubernetes". | +| `kubernetes` | _[EnvoyGatewayKubernetesProvider](#envoygatewaykubernetesprovider)_ | false | Kubernetes defines the configuration of the Kubernetes provider. Kubernetes
provides runtime configuration via the Kubernetes API. | +| `custom` | _[EnvoyGatewayCustomProvider](#envoygatewaycustomprovider)_ | false | Custom defines the configuration for the Custom provider. This provider
allows you to define a specific resource provider and a infrastructure
provider. | + + +#### EnvoyGatewayResourceProvider + + + +EnvoyGatewayResourceProvider defines configuration for the Custom Resource provider. + +_Appears in:_ +- [EnvoyGatewayCustomProvider](#envoygatewaycustomprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[ResourceProviderType](#resourceprovidertype)_ | true | Type is the type of resource provider to use. Supported types are "File". | +| `file` | _[EnvoyGatewayFileResourceProvider](#envoygatewayfileresourceprovider)_ | false | File defines the configuration of the File provider. File provides runtime
configuration defined by one or more files. | + + +#### EnvoyGatewaySpec + + + +EnvoyGatewaySpec defines the desired state of Envoy Gateway. + +_Appears in:_ +- [EnvoyGateway](#envoygateway) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `gateway` | _[Gateway](#gateway)_ | false | Gateway defines desired Gateway API specific configuration. If unset,
default configuration parameters will apply. | +| `provider` | _[EnvoyGatewayProvider](#envoygatewayprovider)_ | false | Provider defines the desired provider and provider-specific configuration.
If unspecified, the Kubernetes provider is used with default configuration
parameters. | +| `logging` | _[EnvoyGatewayLogging](#envoygatewaylogging)_ | false | Logging defines logging parameters for Envoy Gateway. | +| `admin` | _[EnvoyGatewayAdmin](#envoygatewayadmin)_ | false | Admin defines the desired admin related abilities.
If unspecified, the Admin is used with default configuration
parameters. | +| `telemetry` | _[EnvoyGatewayTelemetry](#envoygatewaytelemetry)_ | false | Telemetry defines the desired control plane telemetry related abilities.
If unspecified, the telemetry is used with default configuration. | +| `rateLimit` | _[RateLimit](#ratelimit)_ | false | RateLimit defines the configuration associated with the Rate Limit service
deployed by Envoy Gateway required to implement the Global Rate limiting
functionality. The specific rate limit service used here is the reference
implementation in Envoy. For more details visit https://github.com/envoyproxy/ratelimit.
This configuration is unneeded for "Local" rate limiting. | +| `extensionManager` | _[ExtensionManager](#extensionmanager)_ | false | ExtensionManager defines an extension manager to register for the Envoy Gateway Control Plane. | +| `extensionApis` | _[ExtensionAPISettings](#extensionapisettings)_ | false | ExtensionAPIs defines the settings related to specific Gateway API Extensions
implemented by Envoy Gateway | + + +#### EnvoyGatewayTelemetry + + + +EnvoyGatewayTelemetry defines telemetry configurations for envoy gateway control plane. +Control plane will focus on metrics observability telemetry and tracing telemetry later. + +_Appears in:_ +- [EnvoyGateway](#envoygateway) +- [EnvoyGatewaySpec](#envoygatewayspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `metrics` | _[EnvoyGatewayMetrics](#envoygatewaymetrics)_ | true | Metrics defines metrics configuration for envoy gateway. | + + +#### EnvoyJSONPatchConfig + + + +EnvoyJSONPatchConfig defines the configuration for patching a Envoy xDS Resource +using JSONPatch semantic + +_Appears in:_ +- [EnvoyPatchPolicySpec](#envoypatchpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[EnvoyResourceType](#envoyresourcetype)_ | true | Type is the typed URL of the Envoy xDS Resource | +| `name` | _string_ | true | Name is the name of the resource | +| `operation` | _[JSONPatchOperation](#jsonpatchoperation)_ | true | Patch defines the JSON Patch Operation | + + +#### EnvoyPatchPolicy + + + +EnvoyPatchPolicy allows the user to modify the generated Envoy xDS +resources by Envoy Gateway using this patch API + +_Appears in:_ +- [EnvoyPatchPolicyList](#envoypatchpolicylist) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `apiVersion` | _string_ | |`gateway.envoyproxy.io/v1alpha1` +| `kind` | _string_ | |`EnvoyPatchPolicy` +| `metadata` | _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#objectmeta-v1-meta)_ | true | Refer to Kubernetes API documentation for fields of `metadata`. | +| `spec` | _[EnvoyPatchPolicySpec](#envoypatchpolicyspec)_ | true | Spec defines the desired state of EnvoyPatchPolicy. | +| `status` | _[PolicyStatus](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.PolicyStatus)_ | true | Status defines the current status of EnvoyPatchPolicy. | + + +#### EnvoyPatchPolicyList + + + +EnvoyPatchPolicyList contains a list of EnvoyPatchPolicy resources. + + + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `apiVersion` | _string_ | |`gateway.envoyproxy.io/v1alpha1` +| `kind` | _string_ | |`EnvoyPatchPolicyList` +| `metadata` | _[ListMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#listmeta-v1-meta)_ | true | Refer to Kubernetes API documentation for fields of `metadata`. | +| `items` | _[EnvoyPatchPolicy](#envoypatchpolicy) array_ | true | | + + +#### EnvoyPatchPolicySpec + + + +EnvoyPatchPolicySpec defines the desired state of EnvoyPatchPolicy. + +_Appears in:_ +- [EnvoyPatchPolicy](#envoypatchpolicy) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[EnvoyPatchType](#envoypatchtype)_ | true | Type decides the type of patch.
Valid EnvoyPatchType values are "JSONPatch". | +| `jsonPatches` | _[EnvoyJSONPatchConfig](#envoyjsonpatchconfig) array_ | false | JSONPatch defines the JSONPatch configuration. | +| `targetRef` | _[LocalPolicyTargetReference](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReference)_ | true | TargetRef is the name of the Gateway API resource this policy
is being attached to.
By default, attaching to Gateway is supported and
when mergeGateways is enabled it should attach to GatewayClass.
This Policy and the TargetRef MUST be in the same namespace
for this Policy to have effect and be applied to the Gateway
TargetRef | +| `priority` | _integer_ | true | Priority of the EnvoyPatchPolicy.
If multiple EnvoyPatchPolicies are applied to the same
TargetRef, they will be applied in the ascending order of
the priority i.e. int32.min has the highest priority and
int32.max has the lowest priority.
Defaults to 0. | + + +#### EnvoyPatchType + +_Underlying type:_ _string_ + +EnvoyPatchType specifies the types of Envoy patching mechanisms. + +_Appears in:_ +- [EnvoyPatchPolicySpec](#envoypatchpolicyspec) + +| Value | Description | +| ----- | ----------- | +| `JSONPatch` | JSONPatchEnvoyPatchType allows the user to patch the generated xDS resources using JSONPatch semantics.
For more details on the semantics, please refer to https://datatracker.ietf.org/doc/html/rfc6902
| + + +#### EnvoyProxy + + + +EnvoyProxy is the schema for the envoyproxies API. + + + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `apiVersion` | _string_ | |`gateway.envoyproxy.io/v1alpha1` +| `kind` | _string_ | |`EnvoyProxy` +| `metadata` | _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#objectmeta-v1-meta)_ | true | Refer to Kubernetes API documentation for fields of `metadata`. | +| `spec` | _[EnvoyProxySpec](#envoyproxyspec)_ | true | EnvoyProxySpec defines the desired state of EnvoyProxy. | +| `status` | _[EnvoyProxyStatus](#envoyproxystatus)_ | true | EnvoyProxyStatus defines the actual state of EnvoyProxy. | + + +#### EnvoyProxyKubernetesProvider + + + +EnvoyProxyKubernetesProvider defines configuration for the Kubernetes resource +provider. + +_Appears in:_ +- [EnvoyProxyProvider](#envoyproxyprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `envoyDeployment` | _[KubernetesDeploymentSpec](#kubernetesdeploymentspec)_ | false | EnvoyDeployment defines the desired state of the Envoy deployment resource.
If unspecified, default settings for the managed Envoy deployment resource
are applied. | +| `envoyDaemonSet` | _[KubernetesDaemonSetSpec](#kubernetesdaemonsetspec)_ | false | EnvoyDaemonSet defines the desired state of the Envoy daemonset resource.
Disabled by default, a deployment resource is used instead to provision the Envoy Proxy fleet | +| `envoyService` | _[KubernetesServiceSpec](#kubernetesservicespec)_ | false | EnvoyService defines the desired state of the Envoy service resource.
If unspecified, default settings for the managed Envoy service resource
are applied. | +| `envoyHpa` | _[KubernetesHorizontalPodAutoscalerSpec](#kuberneteshorizontalpodautoscalerspec)_ | false | EnvoyHpa defines the Horizontal Pod Autoscaler settings for Envoy Proxy Deployment.
Once the HPA is being set, Replicas field from EnvoyDeployment will be ignored. | +| `useListenerPortAsContainerPort` | _boolean_ | false | UseListenerPortAsContainerPort disables the port shifting feature in the Envoy Proxy.
When set to false (default value), if the service port is a privileged port (1-1023), add a constant to the value converting it into an ephemeral port.
This allows the container to bind to the port without needing a CAP_NET_BIND_SERVICE capability. | +| `envoyPDB` | _[KubernetesPodDisruptionBudgetSpec](#kubernetespoddisruptionbudgetspec)_ | false | EnvoyPDB allows to control the pod disruption budget of an Envoy Proxy. | + + +#### EnvoyProxyProvider + + + +EnvoyProxyProvider defines the desired state of a resource provider. + +_Appears in:_ +- [EnvoyProxySpec](#envoyproxyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[ProviderType](#providertype)_ | true | Type is the type of resource provider to use. A resource provider provides
infrastructure resources for running the data plane, e.g. Envoy proxy, and
optional auxiliary control planes. Supported types are "Kubernetes". | +| `kubernetes` | _[EnvoyProxyKubernetesProvider](#envoyproxykubernetesprovider)_ | false | Kubernetes defines the desired state of the Kubernetes resource provider.
Kubernetes provides infrastructure resources for running the data plane,
e.g. Envoy proxy. If unspecified and type is "Kubernetes", default settings
for managed Kubernetes resources are applied. | + + +#### EnvoyProxySpec + + + +EnvoyProxySpec defines the desired state of EnvoyProxy. + +_Appears in:_ +- [EnvoyProxy](#envoyproxy) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `provider` | _[EnvoyProxyProvider](#envoyproxyprovider)_ | false | Provider defines the desired resource provider and provider-specific configuration.
If unspecified, the "Kubernetes" resource provider is used with default configuration
parameters. | +| `logging` | _[ProxyLogging](#proxylogging)_ | true | Logging defines logging parameters for managed proxies. | +| `telemetry` | _[ProxyTelemetry](#proxytelemetry)_ | false | Telemetry defines telemetry parameters for managed proxies. | +| `bootstrap` | _[ProxyBootstrap](#proxybootstrap)_ | false | Bootstrap defines the Envoy Bootstrap as a YAML string.
Visit https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/bootstrap/v3/bootstrap.proto#envoy-v3-api-msg-config-bootstrap-v3-bootstrap
to learn more about the syntax.
If set, this is the Bootstrap configuration used for the managed Envoy Proxy fleet instead of the default Bootstrap configuration
set by Envoy Gateway.
Some fields within the Bootstrap that are required to communicate with the xDS Server (Envoy Gateway) and receive xDS resources
from it are not configurable and will result in the `EnvoyProxy` resource being rejected.
Backward compatibility across minor versions is not guaranteed.
We strongly recommend using `egctl x translate` to generate a `EnvoyProxy` resource with the `Bootstrap` field set to the default
Bootstrap configuration used. You can edit this configuration, and rerun `egctl x translate` to ensure there are no validation errors. | +| `concurrency` | _integer_ | false | Concurrency defines the number of worker threads to run. If unset, it defaults to
the number of cpuset threads on the platform. | +| `routingType` | _[RoutingType](#routingtype)_ | false | RoutingType can be set to "Service" to use the Service Cluster IP for routing to the backend,
or it can be set to "Endpoint" to use Endpoint routing. The default is "Endpoint". | +| `extraArgs` | _string array_ | false | ExtraArgs defines additional command line options that are provided to Envoy.
More info: https://www.envoyproxy.io/docs/envoy/latest/operations/cli#command-line-options
Note: some command line options are used internally(e.g. --log-level) so they cannot be provided here. | +| `mergeGateways` | _boolean_ | false | MergeGateways defines if Gateway resources should be merged onto the same Envoy Proxy Infrastructure.
Setting this field to true would merge all Gateway Listeners under the parent Gateway Class.
This means that the port, protocol and hostname tuple must be unique for every listener.
If a duplicate listener is detected, the newer listener (based on timestamp) will be rejected and its status will be updated with a "Accepted=False" condition. | +| `shutdown` | _[ShutdownConfig](#shutdownconfig)_ | false | Shutdown defines configuration for graceful envoy shutdown process. | +| `filterOrder` | _[FilterPosition](#filterposition) array_ | false | FilterOrder defines the order of filters in the Envoy proxy's HTTP filter chain.
The FilterPosition in the list will be applied in the order they are defined.
If unspecified, the default filter order is applied.
Default filter order is:

- envoy.filters.http.fault

- envoy.filters.http.cors

- envoy.filters.http.ext_authz

- envoy.filters.http.basic_authn

- envoy.filters.http.oauth2

- envoy.filters.http.jwt_authn

- envoy.filters.http.ext_proc

- envoy.filters.http.wasm

- envoy.filters.http.rbac

- envoy.filters.http.local_ratelimit

- envoy.filters.http.ratelimit

- envoy.filters.http.router | +| `backendTLS` | _[BackendTLSConfig](#backendtlsconfig)_ | false | BackendTLS is the TLS configuration for the Envoy proxy to use when connecting to backends.
These settings are applied on backends for which TLS policies are specified. | + + +#### EnvoyProxyStatus + + + +EnvoyProxyStatus defines the observed state of EnvoyProxy. This type is not implemented +until https://github.com/envoyproxy/gateway/issues/1007 is fixed. + +_Appears in:_ +- [EnvoyProxy](#envoyproxy) + + + +#### EnvoyResourceType + +_Underlying type:_ _string_ + +EnvoyResourceType specifies the type URL of the Envoy resource. + +_Appears in:_ +- [EnvoyJSONPatchConfig](#envoyjsonpatchconfig) + +| Value | Description | +| ----- | ----------- | +| `type.googleapis.com/envoy.config.listener.v3.Listener` | ListenerEnvoyResourceType defines the Type URL of the Listener resource
| +| `type.googleapis.com/envoy.config.route.v3.RouteConfiguration` | RouteConfigurationEnvoyResourceType defines the Type URL of the RouteConfiguration resource
| +| `type.googleapis.com/envoy.config.cluster.v3.Cluster` | ClusterEnvoyResourceType defines the Type URL of the Cluster resource
| +| `type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment` | ClusterLoadAssignmentEnvoyResourceType defines the Type URL of the ClusterLoadAssignment resource
| + + +#### ExtAuth + + + +ExtAuth defines the configuration for External Authorization. + +_Appears in:_ +- [SecurityPolicySpec](#securitypolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `grpc` | _[GRPCExtAuthService](#grpcextauthservice)_ | true | GRPC defines the gRPC External Authorization service.
Either GRPCService or HTTPService must be specified,
and only one of them can be provided. | +| `http` | _[HTTPExtAuthService](#httpextauthservice)_ | true | HTTP defines the HTTP External Authorization service.
Either GRPCService or HTTPService must be specified,
and only one of them can be provided. | +| `headersToExtAuth` | _string array_ | false | HeadersToExtAuth defines the client request headers that will be included
in the request to the external authorization service.
Note: If not specified, the default behavior for gRPC and HTTP external
authorization services is different due to backward compatibility reasons.
All headers will be included in the check request to a gRPC authorization server.
Only the following headers will be included in the check request to an HTTP
authorization server: Host, Method, Path, Content-Length, and Authorization.
And these headers will always be included to the check request to an HTTP
authorization server by default, no matter whether they are specified
in HeadersToExtAuth or not. | +| `failOpen` | _boolean_ | false | FailOpen is a switch used to control the behavior when a response from the External Authorization service cannot be obtained.
If FailOpen is set to true, the system allows the traffic to pass through.
Otherwise, if it is set to false or not set (defaulting to false),
the system blocks the traffic and returns a HTTP 5xx error, reflecting a fail-closed approach.
This setting determines whether to prioritize accessibility over strict security in case of authorization service failure. | + + +#### ExtProc + + + +ExtProc defines the configuration for External Processing filter. + +_Appears in:_ +- [EnvoyExtensionPolicySpec](#envoyextensionpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `backendRefs` | _[BackendRef](#backendref) array_ | true | BackendRefs defines the configuration of the external processing service | +| `messageTimeout` | _[Duration](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Duration)_ | false | MessageTimeout is the timeout for a response to be returned from the external processor
Default: 200ms | +| `failOpen` | _boolean_ | false | FailOpen defines if requests or responses that cannot be processed due to connectivity to the
external processor are terminated or passed-through.
Default: false | +| `processingMode` | _[ExtProcProcessingMode](#extprocprocessingmode)_ | false | ProcessingMode defines how request and response body is processed
Default: header and body are not sent to the external processor | + + +#### ExtProcBodyProcessingMode + +_Underlying type:_ _string_ + + + +_Appears in:_ +- [ProcessingModeOptions](#processingmodeoptions) + +| Value | Description | +| ----- | ----------- | +| `Streamed` | StreamedExtProcBodyProcessingMode will stream the body to the server in pieces as they arrive at the proxy.
| +| `Buffered` | BufferedExtProcBodyProcessingMode will buffer the message body in memory and send the entire body at once. If the body exceeds the configured buffer limit, then the downstream system will receive an error.
| +| `BufferedPartial` | BufferedPartialExtBodyHeaderProcessingMode will buffer the message body in memory and send the entire body in one chunk. If the body exceeds the configured buffer limit, then the body contents up to the buffer limit will be sent.
| + + +#### ExtProcProcessingMode + + + +ExtProcProcessingMode defines if and how headers and bodies are sent to the service. +https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/ext_proc/v3/processing_mode.proto#envoy-v3-api-msg-extensions-filters-http-ext-proc-v3-processingmode + +_Appears in:_ +- [ExtProc](#extproc) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `request` | _[ProcessingModeOptions](#processingmodeoptions)_ | false | Defines processing mode for requests. If present, request headers are sent. Request body is processed according
to the specified mode. | +| `response` | _[ProcessingModeOptions](#processingmodeoptions)_ | false | Defines processing mode for responses. If present, response headers are sent. Response body is processed according
to the specified mode. | + + +#### ExtensionAPISettings + + + +ExtensionAPISettings defines the settings specific to Gateway API Extensions. + +_Appears in:_ +- [EnvoyGateway](#envoygateway) +- [EnvoyGatewaySpec](#envoygatewayspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `enableEnvoyPatchPolicy` | _boolean_ | true | EnableEnvoyPatchPolicy enables Envoy Gateway to
reconcile and implement the EnvoyPatchPolicy resources. | +| `enableBackend` | _boolean_ | true | EnableBackend enables Envoy Gateway to
reconcile and implement the Backend resources. | + + +#### ExtensionHooks + + + +ExtensionHooks defines extension hooks across all supported runners + +_Appears in:_ +- [ExtensionManager](#extensionmanager) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `xdsTranslator` | _[XDSTranslatorHooks](#xdstranslatorhooks)_ | true | XDSTranslator defines all the supported extension hooks for the xds-translator runner | + + +#### ExtensionManager + + + +ExtensionManager defines the configuration for registering an extension manager to +the Envoy Gateway control plane. + +_Appears in:_ +- [EnvoyGateway](#envoygateway) +- [EnvoyGatewaySpec](#envoygatewayspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `resources` | _[GroupVersionKind](#groupversionkind) array_ | false | Resources defines the set of K8s resources the extension will handle as route
filter resources | +| `policyResources` | _[GroupVersionKind](#groupversionkind) array_ | false | PolicyResources defines the set of K8S resources the extension server will handle
as directly attached GatewayAPI policies | +| `hooks` | _[ExtensionHooks](#extensionhooks)_ | true | Hooks defines the set of hooks the extension supports | +| `service` | _[ExtensionService](#extensionservice)_ | true | Service defines the configuration of the extension service that the Envoy
Gateway Control Plane will call through extension hooks. | + + +#### ExtensionService + + + +ExtensionService defines the configuration for connecting to a registered extension service. + +_Appears in:_ +- [ExtensionManager](#extensionmanager) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `fqdn` | _[FQDNEndpoint](#fqdnendpoint)_ | false | FQDN defines a FQDN endpoint | +| `ip` | _[IPEndpoint](#ipendpoint)_ | false | IP defines an IP endpoint. Currently, only IPv4 Addresses are supported. | +| `unix` | _[UnixSocket](#unixsocket)_ | false | Unix defines the unix domain socket endpoint | +| `host` | _string_ | false | Host define the extension service hostname.
Deprecated: use the appropriate transport attribute instead (FQDN,IP,Unix) | +| `port` | _integer_ | false | Port defines the port the extension service is exposed on.
Deprecated: use the appropriate transport attribute instead (FQDN,IP,Unix) | +| `tls` | _[ExtensionTLS](#extensiontls)_ | false | TLS defines TLS configuration for communication between Envoy Gateway and
the extension service. | + + +#### ExtensionTLS + + + +ExtensionTLS defines the TLS configuration when connecting to an extension service + +_Appears in:_ +- [ExtensionService](#extensionservice) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `certificateRef` | _[SecretObjectReference](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.SecretObjectReference)_ | true | CertificateRef contains a references to objects (Kubernetes objects or otherwise) that
contains a TLS certificate and private keys. These certificates are used to
establish a TLS handshake to the extension server.

CertificateRef can only reference a Kubernetes Secret at this time. | + + +#### FQDNEndpoint + + + +FQDNEndpoint describes TCP/UDP socket address, corresponding to Envoy's Socket Address +https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/address.proto#config-core-v3-socketaddress + +_Appears in:_ +- [BackendEndpoint](#backendendpoint) +- [ExtensionService](#extensionservice) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `hostname` | _string_ | true | Hostname defines the FQDN hostname of the backend endpoint. | +| `port` | _integer_ | true | Port defines the port of the backend endpoint. | + + +#### FaultInjection + + + +FaultInjection defines the fault injection policy to be applied. This configuration can be used to +inject delays and abort requests to mimic failure scenarios such as service failures and overloads + +_Appears in:_ +- [BackendTrafficPolicySpec](#backendtrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `delay` | _[FaultInjectionDelay](#faultinjectiondelay)_ | false | If specified, a delay will be injected into the request. | +| `abort` | _[FaultInjectionAbort](#faultinjectionabort)_ | false | If specified, the request will be aborted if it meets the configuration criteria. | + + +#### FaultInjectionAbort + + + +FaultInjectionAbort defines the abort fault injection configuration + +_Appears in:_ +- [FaultInjection](#faultinjection) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `httpStatus` | _integer_ | false | StatusCode specifies the HTTP status code to be returned | +| `grpcStatus` | _integer_ | false | GrpcStatus specifies the GRPC status code to be returned | +| `percentage` | _float_ | false | Percentage specifies the percentage of requests to be aborted. Default 100%, if set 0, no requests will be aborted. Accuracy to 0.0001%. | + + +#### FaultInjectionDelay + + + +FaultInjectionDelay defines the delay fault injection configuration + +_Appears in:_ +- [FaultInjection](#faultinjection) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `fixedDelay` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | true | FixedDelay specifies the fixed delay duration | +| `percentage` | _float_ | false | Percentage specifies the percentage of requests to be delayed. Default 100%, if set 0, no requests will be delayed. Accuracy to 0.0001%. | + + +#### FileEnvoyProxyAccessLog + + + + + +_Appears in:_ +- [ProxyAccessLogSink](#proxyaccesslogsink) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `path` | _string_ | true | Path defines the file path used to expose envoy access log(e.g. /dev/stdout). | + + +#### FilterPosition + + + +FilterPosition defines the position of an Envoy HTTP filter in the filter chain. + +_Appears in:_ +- [EnvoyProxySpec](#envoyproxyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `name` | _[EnvoyFilter](#envoyfilter)_ | true | Name of the filter. | +| `before` | _[EnvoyFilter](#envoyfilter)_ | true | Before defines the filter that should come before the filter.
Only one of Before or After must be set. | +| `after` | _[EnvoyFilter](#envoyfilter)_ | true | After defines the filter that should come after the filter.
Only one of Before or After must be set. | + + +#### GRPCExtAuthService + + + +GRPCExtAuthService defines the gRPC External Authorization service +The authorization request message is defined in +https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/auth/v3/external_auth.proto + +_Appears in:_ +- [ExtAuth](#extauth) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `backendRef` | _[BackendObjectReference](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.BackendObjectReference)_ | true | BackendRef references a Kubernetes object that represents the
backend server to which the authorization request will be sent.
Only Service kind is supported for now.

Deprecated: Use BackendRefs instead. | +| `backendRefs` | _[BackendRef](#backendref) array_ | false | BackendRefs references a Kubernetes object that represents the
backend server to which the authorization request will be sent.
Only Service kind is supported for now. | + + +#### Gateway + + + +Gateway defines the desired Gateway API configuration of Envoy Gateway. + +_Appears in:_ +- [EnvoyGateway](#envoygateway) +- [EnvoyGatewaySpec](#envoygatewayspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `controllerName` | _string_ | false | ControllerName defines the name of the Gateway API controller. If unspecified,
defaults to "gateway.envoyproxy.io/gatewayclass-controller". See the following
for additional details:
https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.GatewayClass | + + +#### GlobalRateLimit + + + +GlobalRateLimit defines global rate limit configuration. + +_Appears in:_ +- [RateLimitSpec](#ratelimitspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `rules` | _[RateLimitRule](#ratelimitrule) array_ | true | Rules are a list of RateLimit selectors and limits. Each rule and its
associated limit is applied in a mutually exclusive way. If a request
matches multiple rules, each of their associated limits get applied, so a
single request might increase the rate limit counters for multiple rules
if selected. The rate limit service will return a logical OR of the individual
rate limit decisions of all matching rules. For example, if a request
matches two rules, one rate limited and one not, the final decision will be
to rate limit the request. | + + +#### GroupVersionKind + + + +GroupVersionKind unambiguously identifies a Kind. +It can be converted to k8s.io/apimachinery/pkg/runtime/schema.GroupVersionKind + +_Appears in:_ +- [ExtensionManager](#extensionmanager) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `group` | _string_ | true | | +| `version` | _string_ | true | | +| `kind` | _string_ | true | | + + +#### GzipCompressor + + + +GzipCompressor defines the config for the Gzip compressor. +The default values can be found here: +https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/compression/gzip/compressor/v3/gzip.proto#extension-envoy-compression-gzip-compressor + +_Appears in:_ +- [Compression](#compression) + + + +#### HTTP10Settings + + + +HTTP10Settings provides HTTP/1.0 configuration on the listener. + +_Appears in:_ +- [HTTP1Settings](#http1settings) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `useDefaultHost` | _boolean_ | false | UseDefaultHost defines if the HTTP/1.0 request is missing the Host header,
then the hostname associated with the listener should be injected into the
request.
If this is not set and an HTTP/1.0 request arrives without a host, then
it will be rejected. | + + +#### HTTP1Settings + + + +HTTP1Settings provides HTTP/1 configuration on the listener. + +_Appears in:_ +- [ClientTrafficPolicySpec](#clienttrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `enableTrailers` | _boolean_ | false | EnableTrailers defines if HTTP/1 trailers should be proxied by Envoy. | +| `preserveHeaderCase` | _boolean_ | false | PreserveHeaderCase defines if Envoy should preserve the letter case of headers.
By default, Envoy will lowercase all the headers. | +| `http10` | _[HTTP10Settings](#http10settings)_ | false | HTTP10 turns on support for HTTP/1.0 and HTTP/0.9 requests. | + + +#### HTTP2Settings + + + +HTTP2Settings provides HTTP/2 configuration on the listener. + +_Appears in:_ +- [ClientTrafficPolicySpec](#clienttrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `initialStreamWindowSize` | _[Quantity](#quantity)_ | false | InitialStreamWindowSize sets the initial window size for HTTP/2 streams.
If not set, the default value is 64 KiB(64*1024). | +| `initialConnectionWindowSize` | _[Quantity](#quantity)_ | false | InitialConnectionWindowSize sets the initial window size for HTTP/2 connections.
If not set, the default value is 1 MiB. | +| `maxConcurrentStreams` | _integer_ | false | MaxConcurrentStreams sets the maximum number of concurrent streams allowed per connection.
If not set, the default value is 100. | + + +#### HTTP3Settings + + + +HTTP3Settings provides HTTP/3 configuration on the listener. + +_Appears in:_ +- [ClientTrafficPolicySpec](#clienttrafficpolicyspec) + + + +#### HTTPActiveHealthChecker + + + +HTTPActiveHealthChecker defines the settings of http health check. + +_Appears in:_ +- [ActiveHealthCheck](#activehealthcheck) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `path` | _string_ | true | Path defines the HTTP path that will be requested during health checking. | +| `method` | _string_ | false | Method defines the HTTP method used for health checking.
Defaults to GET | +| `expectedStatuses` | _[HTTPStatus](#httpstatus) array_ | false | ExpectedStatuses defines a list of HTTP response statuses considered healthy.
Defaults to 200 only | +| `expectedResponse` | _[ActiveHealthCheckPayload](#activehealthcheckpayload)_ | false | ExpectedResponse defines a list of HTTP expected responses to match. | + + +#### HTTPClientTimeout + + + + + +_Appears in:_ +- [ClientTimeout](#clienttimeout) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `requestReceivedTimeout` | _[Duration](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Duration)_ | false | RequestReceivedTimeout is the duration envoy waits for the complete request reception. This timer starts upon request
initiation and stops when either the last byte of the request is sent upstream or when the response begins. | +| `idleTimeout` | _[Duration](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Duration)_ | false | IdleTimeout for an HTTP connection. Idle time is defined as a period in which there are no active requests in the connection.
Default: 1 hour. | + + +#### HTTPExtAuthService + + + +HTTPExtAuthService defines the HTTP External Authorization service + +_Appears in:_ +- [ExtAuth](#extauth) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `backendRef` | _[BackendObjectReference](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.BackendObjectReference)_ | true | BackendRef references a Kubernetes object that represents the
backend server to which the authorization request will be sent.
Only Service kind is supported for now.

Deprecated: Use BackendRefs instead. | +| `backendRefs` | _[BackendRef](#backendref) array_ | false | BackendRefs references a Kubernetes object that represents the
backend server to which the authorization request will be sent.
Only Service kind is supported for now. | +| `path` | _string_ | true | Path is the path of the HTTP External Authorization service.
If path is specified, the authorization request will be sent to that path,
or else the authorization request will be sent to the root path. | +| `headersToBackend` | _string array_ | false | HeadersToBackend are the authorization response headers that will be added
to the original client request before sending it to the backend server.
Note that coexisting headers will be overridden.
If not specified, no authorization response headers will be added to the
original client request. | + + +#### HTTPStatus + +_Underlying type:_ _integer_ + +HTTPStatus defines the http status code. + +_Appears in:_ +- [HTTPActiveHealthChecker](#httpactivehealthchecker) +- [RetryOn](#retryon) + + + +#### HTTPTimeout + + + + + +_Appears in:_ +- [Timeout](#timeout) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `connectionIdleTimeout` | _[Duration](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Duration)_ | false | The idle timeout for an HTTP connection. Idle time is defined as a period in which there are no active requests in the connection.
Default: 1 hour. | +| `maxConnectionDuration` | _[Duration](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Duration)_ | false | The maximum duration of an HTTP connection.
Default: unlimited. | + + +#### HTTPWasmCodeSource + + + +HTTPWasmCodeSource defines the HTTP URL containing the Wasm code. + +_Appears in:_ +- [WasmCodeSource](#wasmcodesource) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `url` | _string_ | true | URL is the URL containing the Wasm code. | +| `sha256` | _string_ | false | SHA256 checksum that will be used to verify the Wasm code.

If not specified, Envoy Gateway will not verify the downloaded Wasm code.
kubebuilder:validation:Pattern=`^[a-f0-9]{64}$` | + + +#### Header + + + +Header defines the header hashing configuration for consistent hash based +load balancing. + +_Appears in:_ +- [ConsistentHash](#consistenthash) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `name` | _string_ | true | Name of the header to hash. | + + + + +#### HeaderMatchType + +_Underlying type:_ _string_ + +HeaderMatchType specifies the semantics of how HTTP header values should be compared. +Valid HeaderMatchType values are "Exact", "RegularExpression", and "Distinct". + +_Appears in:_ +- [HeaderMatch](#headermatch) + +| Value | Description | +| ----- | ----------- | +| `Exact` | HeaderMatchExact matches the exact value of the Value field against the value of
the specified HTTP Header.
| +| `RegularExpression` | HeaderMatchRegularExpression matches a regular expression against the value of the
specified HTTP Header. The regex string must adhere to the syntax documented in
https://github.com/google/re2/wiki/Syntax.
| +| `Distinct` | HeaderMatchDistinct matches any and all possible unique values encountered in the
specified HTTP Header. Note that each unique value will receive its own rate limit
bucket.
Note: This is only supported for Global Rate Limits.
| + + +#### HeaderSettings + + + +HeaderSettings provides configuration options for headers on the listener. + +_Appears in:_ +- [ClientTrafficPolicySpec](#clienttrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `enableEnvoyHeaders` | _boolean_ | false | EnableEnvoyHeaders configures Envoy Proxy to add the "X-Envoy-" headers to requests
and responses. | +| `disableRateLimitHeaders` | _boolean_ | false | DisableRateLimitHeaders configures Envoy Proxy to omit the "X-RateLimit-" response headers
when rate limiting is enabled. | +| `xForwardedClientCert` | _[XForwardedClientCert](#xforwardedclientcert)_ | false | XForwardedClientCert configures how Envoy Proxy handle the x-forwarded-client-cert (XFCC) HTTP header.

x-forwarded-client-cert (XFCC) is an HTTP header used to forward the certificate
information of part or all of the clients or proxies that a request has flowed through,
on its way from the client to the server.

Envoy proxy may choose to sanitize/append/forward the XFCC header before proxying the request.

If not set, the default behavior is sanitizing the XFCC header. | +| `withUnderscoresAction` | _[WithUnderscoresAction](#withunderscoresaction)_ | false | WithUnderscoresAction configures the action to take when an HTTP header with underscores
is encountered. The default action is to reject the request. | +| `preserveXRequestID` | _boolean_ | false | PreserveXRequestID configures Envoy to keep the X-Request-ID header if passed for a request that is edge
(Edge request is the request from external clients to front Envoy) and not reset it, which is the current Envoy behaviour.
It defaults to false. | + + +#### HealthCheck + + + +HealthCheck configuration to decide which endpoints +are healthy and can be used for routing. + +_Appears in:_ +- [BackendTrafficPolicySpec](#backendtrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `active` | _[ActiveHealthCheck](#activehealthcheck)_ | false | Active health check configuration | +| `passive` | _[PassiveHealthCheck](#passivehealthcheck)_ | false | Passive passive check configuration | + + +#### HealthCheckSettings + + + +HealthCheckSettings provides HealthCheck configuration on the HTTP/HTTPS listener. + +_Appears in:_ +- [ClientTrafficPolicySpec](#clienttrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `path` | _string_ | true | Path specifies the HTTP path to match on for health check requests. | + + +#### IPEndpoint + + + +IPEndpoint describes TCP/UDP socket address, corresponding to Envoy's Socket Address +https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/address.proto#config-core-v3-socketaddress + +_Appears in:_ +- [BackendEndpoint](#backendendpoint) +- [ExtensionService](#extensionservice) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `address` | _string_ | true | Address defines the IP address of the backend endpoint. | +| `port` | _integer_ | true | Port defines the port of the backend endpoint. | + + +#### ImagePullPolicy + +_Underlying type:_ _string_ + +ImagePullPolicy defines the policy to use when pulling an OIC image. + +_Appears in:_ +- [WasmCodeSource](#wasmcodesource) + +| Value | Description | +| ----- | ----------- | +| `IfNotPresent` | ImagePullPolicyIfNotPresent will only pull the image if it does not already exist in the EG cache.
| +| `Always` | ImagePullPolicyAlways will pull the image when the EnvoyExtension resource version changes.
Note: EG does not update the Wasm module every time an Envoy proxy requests the Wasm module.
| + + +#### ImageWasmCodeSource + + + +ImageWasmCodeSource defines the OCI image containing the Wasm code. + +_Appears in:_ +- [WasmCodeSource](#wasmcodesource) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `url` | _string_ | true | URL is the URL of the OCI image.
URL can be in the format of `registry/image:tag` or `registry/image@sha256:digest`. | +| `sha256` | _string_ | false | SHA256 checksum that will be used to verify the OCI image.

It must match the digest of the OCI image.

If not specified, Envoy Gateway will not verify the downloaded OCI image.
kubebuilder:validation:Pattern=`^[a-f0-9]{64}$` | +| `pullSecretRef` | _[SecretObjectReference](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.SecretObjectReference)_ | false | PullSecretRef is a reference to the secret containing the credentials to pull the image.
Only support Kubernetes Secret resource from the same namespace. | + + +#### InfrastructureProviderType + +_Underlying type:_ _string_ + +InfrastructureProviderType defines the types of custom infrastructure providers supported by Envoy Gateway. + +_Appears in:_ +- [EnvoyGatewayInfrastructureProvider](#envoygatewayinfrastructureprovider) + +| Value | Description | +| ----- | ----------- | +| `Host` | InfrastructureProviderTypeHost defines the "Host" provider.
| + + +#### JSONPatchOperation + + + +JSONPatchOperation defines the JSON Patch Operation as defined in +https://datatracker.ietf.org/doc/html/rfc6902 + +_Appears in:_ +- [EnvoyJSONPatchConfig](#envoyjsonpatchconfig) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `op` | _[JSONPatchOperationType](#jsonpatchoperationtype)_ | true | Op is the type of operation to perform | +| `path` | _string_ | true | Path is the location of the target document/field where the operation will be performed
Refer to https://datatracker.ietf.org/doc/html/rfc6901 for more details. | +| `from` | _string_ | false | From is the source location of the value to be copied or moved. Only valid
for move or copy operations
Refer to https://datatracker.ietf.org/doc/html/rfc6901 for more details. | +| `value` | _[JSON](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#json-v1-apiextensions-k8s-io)_ | false | Value is the new value of the path location. The value is only used by
the `add` and `replace` operations. | + + +#### JSONPatchOperationType + +_Underlying type:_ _string_ + +JSONPatchOperationType specifies the JSON Patch operations that can be performed. + +_Appears in:_ +- [JSONPatchOperation](#jsonpatchoperation) + + + +#### JWT + + + +JWT defines the configuration for JSON Web Token (JWT) authentication. + +_Appears in:_ +- [SecurityPolicySpec](#securitypolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `optional` | _boolean_ | true | Optional determines whether a missing JWT is acceptable, defaulting to false if not specified.
Note: Even if optional is set to true, JWT authentication will still fail if an invalid JWT is presented. | +| `providers` | _[JWTProvider](#jwtprovider) array_ | true | Providers defines the JSON Web Token (JWT) authentication provider type.
When multiple JWT providers are specified, the JWT is considered valid if
any of the providers successfully validate the JWT. For additional details,
see https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/jwt_authn_filter.html. | + + +#### JWTExtractor + + + +JWTExtractor defines a custom JWT token extraction from HTTP request. +If specified, Envoy will extract the JWT token from the listed extractors (headers, cookies, or params) and validate each of them. +If any value extracted is found to be an invalid JWT, a 401 error will be returned. + +_Appears in:_ +- [JWTProvider](#jwtprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `headers` | _[JWTHeaderExtractor](#jwtheaderextractor) array_ | false | Headers represents a list of HTTP request headers to extract the JWT token from. | +| `cookies` | _string array_ | false | Cookies represents a list of cookie names to extract the JWT token from. | +| `params` | _string array_ | false | Params represents a list of query parameters to extract the JWT token from. | + + +#### JWTHeaderExtractor + + + +JWTHeaderExtractor defines an HTTP header location to extract JWT token + +_Appears in:_ +- [JWTExtractor](#jwtextractor) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `name` | _string_ | true | Name is the HTTP header name to retrieve the token | +| `valuePrefix` | _string_ | false | ValuePrefix is the prefix that should be stripped before extracting the token.
The format would be used by Envoy like "{ValuePrefix}".
For example, "Authorization: Bearer ", then the ValuePrefix="Bearer " with a space at the end. | + + +#### JWTProvider + + + +JWTProvider defines how a JSON Web Token (JWT) can be verified. + +_Appears in:_ +- [JWT](#jwt) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `name` | _string_ | true | Name defines a unique name for the JWT provider. A name can have a variety of forms,
including RFC1123 subdomains, RFC 1123 labels, or RFC 1035 labels. | +| `issuer` | _string_ | false | Issuer is the principal that issued the JWT and takes the form of a URL or email address.
For additional details, see https://tools.ietf.org/html/rfc7519#section-4.1.1 for
URL format and https://rfc-editor.org/rfc/rfc5322.html for email format. If not provided,
the JWT issuer is not checked. | +| `audiences` | _string array_ | false | Audiences is a list of JWT audiences allowed access. For additional details, see
https://tools.ietf.org/html/rfc7519#section-4.1.3. If not provided, JWT audiences
are not checked. | +| `remoteJWKS` | _[RemoteJWKS](#remotejwks)_ | true | RemoteJWKS defines how to fetch and cache JSON Web Key Sets (JWKS) from a remote
HTTP/HTTPS endpoint. | +| `claimToHeaders` | _[ClaimToHeader](#claimtoheader) array_ | false | ClaimToHeaders is a list of JWT claims that must be extracted into HTTP request headers
For examples, following config:
The claim must be of type; string, int, double, bool. Array type claims are not supported | +| `recomputeRoute` | _boolean_ | false | RecomputeRoute clears the route cache and recalculates the routing decision.
This field must be enabled if the headers generated from the claim are used for
route matching decisions. If the recomputation selects a new route, features targeting
the new matched route will be applied. | +| `extractFrom` | _[JWTExtractor](#jwtextractor)_ | false | ExtractFrom defines different ways to extract the JWT token from HTTP request.
If empty, it defaults to extract JWT token from the Authorization HTTP request header using Bearer schema
or access_token from query parameters. | + + +#### KubernetesContainerSpec + + + +KubernetesContainerSpec defines the desired state of the Kubernetes container resource. + +_Appears in:_ +- [KubernetesDaemonSetSpec](#kubernetesdaemonsetspec) +- [KubernetesDeploymentSpec](#kubernetesdeploymentspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `env` | _[EnvVar](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#envvar-v1-core) array_ | false | List of environment variables to set in the container. | +| `resources` | _[ResourceRequirements](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#resourcerequirements-v1-core)_ | false | Resources required by this container.
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ | +| `securityContext` | _[SecurityContext](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#securitycontext-v1-core)_ | false | SecurityContext defines the security options the container should be run with.
If set, the fields of SecurityContext override the equivalent fields of PodSecurityContext.
More info: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ | +| `image` | _string_ | false | Image specifies the EnvoyProxy container image to be used, instead of the default image. | +| `volumeMounts` | _[VolumeMount](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#volumemount-v1-core) array_ | false | VolumeMounts are volumes to mount into the container's filesystem.
Cannot be updated. | + + +#### KubernetesDaemonSetSpec + + + +KubernetesDaemonsetSpec defines the desired state of the Kubernetes daemonset resource. + +_Appears in:_ +- [EnvoyProxyKubernetesProvider](#envoyproxykubernetesprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `patch` | _[KubernetesPatchSpec](#kubernetespatchspec)_ | false | Patch defines how to perform the patch operation to daemonset | +| `strategy` | _[DaemonSetUpdateStrategy](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#daemonsetupdatestrategy-v1-apps)_ | false | The daemonset strategy to use to replace existing pods with new ones. | +| `pod` | _[KubernetesPodSpec](#kubernetespodspec)_ | false | Pod defines the desired specification of pod. | +| `container` | _[KubernetesContainerSpec](#kubernetescontainerspec)_ | false | Container defines the desired specification of main container. | +| `name` | _string_ | false | Name of the daemonSet.
When unset, this defaults to an autogenerated name. | + + +#### KubernetesDeployMode + + + +KubernetesDeployMode holds configuration for how to deploy managed resources such as the Envoy Proxy +data plane fleet. + +_Appears in:_ +- [EnvoyGatewayKubernetesProvider](#envoygatewaykubernetesprovider) + + + +#### KubernetesDeploymentSpec + + + +KubernetesDeploymentSpec defines the desired state of the Kubernetes deployment resource. + +_Appears in:_ +- [EnvoyGatewayKubernetesProvider](#envoygatewaykubernetesprovider) +- [EnvoyProxyKubernetesProvider](#envoyproxykubernetesprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `patch` | _[KubernetesPatchSpec](#kubernetespatchspec)_ | false | Patch defines how to perform the patch operation to deployment | +| `replicas` | _integer_ | false | Replicas is the number of desired pods. Defaults to 1. | +| `strategy` | _[DeploymentStrategy](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#deploymentstrategy-v1-apps)_ | false | The deployment strategy to use to replace existing pods with new ones. | +| `pod` | _[KubernetesPodSpec](#kubernetespodspec)_ | false | Pod defines the desired specification of pod. | +| `container` | _[KubernetesContainerSpec](#kubernetescontainerspec)_ | false | Container defines the desired specification of main container. | +| `initContainers` | _[Container](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#container-v1-core) array_ | false | List of initialization containers belonging to the pod.
More info: https://kubernetes.io/docs/concepts/workloads/pods/init-containers/ | +| `name` | _string_ | false | Name of the deployment.
When unset, this defaults to an autogenerated name. | + + +#### KubernetesHorizontalPodAutoscalerSpec + + + +KubernetesHorizontalPodAutoscalerSpec defines Kubernetes Horizontal Pod Autoscaler settings of Envoy Proxy Deployment. +When HPA is enabled, it is recommended that the value in `KubernetesDeploymentSpec.replicas` be removed, otherwise +Envoy Gateway will revert back to this value every time reconciliation occurs. +See k8s.io.autoscaling.v2.HorizontalPodAutoScalerSpec. + +_Appears in:_ +- [EnvoyProxyKubernetesProvider](#envoyproxykubernetesprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `minReplicas` | _integer_ | false | minReplicas is the lower limit for the number of replicas to which the autoscaler
can scale down. It defaults to 1 replica. | +| `maxReplicas` | _integer_ | true | maxReplicas is the upper limit for the number of replicas to which the autoscaler can scale up.
It cannot be less that minReplicas. | +| `metrics` | _[MetricSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#metricspec-v2-autoscaling) array_ | false | metrics contains the specifications for which to use to calculate the
desired replica count (the maximum replica count across all metrics will
be used).
If left empty, it defaults to being based on CPU utilization with average on 80% usage. | +| `behavior` | _[HorizontalPodAutoscalerBehavior](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#horizontalpodautoscalerbehavior-v2-autoscaling)_ | false | behavior configures the scaling behavior of the target
in both Up and Down directions (scaleUp and scaleDown fields respectively).
If not set, the default HPAScalingRules for scale up and scale down are used.
See k8s.io.autoscaling.v2.HorizontalPodAutoScalerBehavior. | + + +#### KubernetesPatchSpec + + + +KubernetesPatchSpec defines how to perform the patch operation + +_Appears in:_ +- [KubernetesDaemonSetSpec](#kubernetesdaemonsetspec) +- [KubernetesDeploymentSpec](#kubernetesdeploymentspec) +- [KubernetesServiceSpec](#kubernetesservicespec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[MergeType](#mergetype)_ | false | Type is the type of merge operation to perform

By default, StrategicMerge is used as the patch type. | +| `value` | _[JSON](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#json-v1-apiextensions-k8s-io)_ | true | Object contains the raw configuration for merged object | + + +#### KubernetesPodDisruptionBudgetSpec + + + +KubernetesPodDisruptionBudgetSpec defines Kubernetes PodDisruptionBudget settings of Envoy Proxy Deployment. + +_Appears in:_ +- [EnvoyProxyKubernetesProvider](#envoyproxykubernetesprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `minAvailable` | _integer_ | false | MinAvailable specifies the minimum number of pods that must be available at all times during voluntary disruptions,
such as node drains or updates. This setting ensures that your envoy proxy maintains a certain level of availability
and resilience during maintenance operations. | + + +#### KubernetesPodSpec + + + +KubernetesPodSpec defines the desired state of the Kubernetes pod resource. + +_Appears in:_ +- [KubernetesDaemonSetSpec](#kubernetesdaemonsetspec) +- [KubernetesDeploymentSpec](#kubernetesdeploymentspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `annotations` | _object (keys:string, values:string)_ | false | Annotations are the annotations that should be appended to the pods.
By default, no pod annotations are appended. | +| `labels` | _object (keys:string, values:string)_ | false | Labels are the additional labels that should be tagged to the pods.
By default, no additional pod labels are tagged. | +| `securityContext` | _[PodSecurityContext](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#podsecuritycontext-v1-core)_ | false | SecurityContext holds pod-level security attributes and common container settings.
Optional: Defaults to empty. See type description for default values of each field. | +| `affinity` | _[Affinity](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#affinity-v1-core)_ | false | If specified, the pod's scheduling constraints. | +| `tolerations` | _[Toleration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#toleration-v1-core) array_ | false | If specified, the pod's tolerations. | +| `volumes` | _[Volume](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#volume-v1-core) array_ | false | Volumes that can be mounted by containers belonging to the pod.
More info: https://kubernetes.io/docs/concepts/storage/volumes | +| `imagePullSecrets` | _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#localobjectreference-v1-core) array_ | false | ImagePullSecrets is an optional list of references to secrets
in the same namespace to use for pulling any of the images used by this PodSpec.
If specified, these secrets will be passed to individual puller implementations for them to use.
More info: https://kubernetes.io/docs/concepts/containers/images#specifying-imagepullsecrets-on-a-pod | +| `nodeSelector` | _object (keys:string, values:string)_ | false | NodeSelector is a selector which must be true for the pod to fit on a node.
Selector which must match a node's labels for the pod to be scheduled on that node.
More info: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/ | +| `topologySpreadConstraints` | _[TopologySpreadConstraint](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#topologyspreadconstraint-v1-core) array_ | false | TopologySpreadConstraints describes how a group of pods ought to spread across topology
domains. Scheduler will schedule pods in a way which abides by the constraints.
All topologySpreadConstraints are ANDed. | + + +#### KubernetesServiceSpec + + + +KubernetesServiceSpec defines the desired state of the Kubernetes service resource. + +_Appears in:_ +- [EnvoyProxyKubernetesProvider](#envoyproxykubernetesprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `annotations` | _object (keys:string, values:string)_ | false | Annotations that should be appended to the service.
By default, no annotations are appended. | +| `type` | _[ServiceType](#servicetype)_ | false | Type determines how the Service is exposed. Defaults to LoadBalancer.
Valid options are ClusterIP, LoadBalancer and NodePort.
"LoadBalancer" means a service will be exposed via an external load balancer (if the cloud provider supports it).
"ClusterIP" means a service will only be accessible inside the cluster, via the cluster IP.
"NodePort" means a service will be exposed on a static Port on all Nodes of the cluster. | +| `loadBalancerClass` | _string_ | false | LoadBalancerClass, when specified, allows for choosing the LoadBalancer provider
implementation if more than one are available or is otherwise expected to be specified | +| `allocateLoadBalancerNodePorts` | _boolean_ | false | AllocateLoadBalancerNodePorts defines if NodePorts will be automatically allocated for
services with type LoadBalancer. Default is "true". It may be set to "false" if the cluster
load-balancer does not rely on NodePorts. If the caller requests specific NodePorts (by specifying a
value), those requests will be respected, regardless of this field. This field may only be set for
services with type LoadBalancer and will be cleared if the type is changed to any other type. | +| `loadBalancerSourceRanges` | _string array_ | false | LoadBalancerSourceRanges defines a list of allowed IP addresses which will be configured as
firewall rules on the platform providers load balancer. This is not guaranteed to be working as
it happens outside of kubernetes and has to be supported and handled by the platform provider.
This field may only be set for services with type LoadBalancer and will be cleared if the type
is changed to any other type. | +| `loadBalancerIP` | _string_ | false | LoadBalancerIP defines the IP Address of the underlying load balancer service. This field
may be ignored if the load balancer provider does not support this feature.
This field has been deprecated in Kubernetes, but it is still used for setting the IP Address in some cloud
providers such as GCP. | +| `externalTrafficPolicy` | _[ServiceExternalTrafficPolicy](#serviceexternaltrafficpolicy)_ | false | ExternalTrafficPolicy determines the externalTrafficPolicy for the Envoy Service. Valid options
are Local and Cluster. Default is "Local". "Local" means traffic will only go to pods on the node
receiving the traffic. "Cluster" means connections are loadbalanced to all pods in the cluster. | +| `patch` | _[KubernetesPatchSpec](#kubernetespatchspec)_ | false | Patch defines how to perform the patch operation to the service | +| `name` | _string_ | false | Name of the service.
When unset, this defaults to an autogenerated name. | + + +#### KubernetesWatchMode + + + +KubernetesWatchMode holds the configuration for which input resources to watch and reconcile. + +_Appears in:_ +- [EnvoyGatewayKubernetesProvider](#envoygatewaykubernetesprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[KubernetesWatchModeType](#kuberneteswatchmodetype)_ | true | Type indicates what watch mode to use. KubernetesWatchModeTypeNamespaces and
KubernetesWatchModeTypeNamespaceSelector are currently supported
By default, when this field is unset or empty, Envoy Gateway will watch for input namespaced resources
from all namespaces. | +| `namespaces` | _string array_ | true | Namespaces holds the list of namespaces that Envoy Gateway will watch for namespaced scoped
resources such as Gateway, HTTPRoute and Service.
Note that Envoy Gateway will continue to reconcile relevant cluster scoped resources such as
GatewayClass that it is linked to. Precisely one of Namespaces and NamespaceSelector must be set. | +| `namespaceSelector` | _[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#labelselector-v1-meta)_ | true | NamespaceSelector holds the label selector used to dynamically select namespaces.
Envoy Gateway will watch for namespaces matching the specified label selector.
Precisely one of Namespaces and NamespaceSelector must be set. | + + +#### KubernetesWatchModeType + +_Underlying type:_ _string_ + +KubernetesWatchModeType defines the type of KubernetesWatchMode + +_Appears in:_ +- [KubernetesWatchMode](#kuberneteswatchmode) + + + +#### LeaderElection + + + +LeaderElection defines the desired leader election settings. + +_Appears in:_ +- [EnvoyGatewayKubernetesProvider](#envoygatewaykubernetesprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `leaseDuration` | _[Duration](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Duration)_ | true | LeaseDuration defines the time non-leader contenders will wait before attempting to claim leadership.
It's based on the timestamp of the last acknowledged signal. The default setting is 15 seconds. | +| `renewDeadline` | _[Duration](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Duration)_ | true | RenewDeadline represents the time frame within which the current leader will attempt to renew its leadership
status before relinquishing its position. The default setting is 10 seconds. | +| `retryPeriod` | _[Duration](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Duration)_ | true | RetryPeriod denotes the interval at which LeaderElector clients should perform action retries.
The default setting is 2 seconds. | +| `disable` | _boolean_ | true | Disable provides the option to turn off leader election, which is enabled by default. | + + +#### LiteralCustomTag + + + +LiteralCustomTag adds hard-coded value to each span. + +_Appears in:_ +- [CustomTag](#customtag) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `value` | _string_ | true | Value defines the hard-coded value to add to each span. | + + +#### LoadBalancer + + + +LoadBalancer defines the load balancer policy to be applied. + +_Appears in:_ +- [BackendTrafficPolicySpec](#backendtrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[LoadBalancerType](#loadbalancertype)_ | true | Type decides the type of Load Balancer policy.
Valid LoadBalancerType values are
"ConsistentHash",
"LeastRequest",
"Random",
"RoundRobin". | +| `consistentHash` | _[ConsistentHash](#consistenthash)_ | false | ConsistentHash defines the configuration when the load balancer type is
set to ConsistentHash | +| `slowStart` | _[SlowStart](#slowstart)_ | false | SlowStart defines the configuration related to the slow start load balancer policy.
If set, during slow start window, traffic sent to the newly added hosts will gradually increase.
Currently this is only supported for RoundRobin and LeastRequest load balancers | + + +#### LoadBalancerType + +_Underlying type:_ _string_ + +LoadBalancerType specifies the types of LoadBalancer. + +_Appears in:_ +- [LoadBalancer](#loadbalancer) + +| Value | Description | +| ----- | ----------- | +| `ConsistentHash` | ConsistentHashLoadBalancerType load balancer policy.
| +| `LeastRequest` | LeastRequestLoadBalancerType load balancer policy.
| +| `Random` | RandomLoadBalancerType load balancer policy.
| +| `RoundRobin` | RoundRobinLoadBalancerType load balancer policy.
| + + +#### LocalRateLimit + + + +LocalRateLimit defines local rate limit configuration. + +_Appears in:_ +- [RateLimitSpec](#ratelimitspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `rules` | _[RateLimitRule](#ratelimitrule) array_ | false | Rules are a list of RateLimit selectors and limits. If a request matches
multiple rules, the strictest limit is applied. For example, if a request
matches two rules, one with 10rps and one with 20rps, the final limit will
be based on the rule with 10rps. | + + +#### LogLevel + +_Underlying type:_ _string_ + +LogLevel defines a log level for Envoy Gateway and EnvoyProxy system logs. + +_Appears in:_ +- [EnvoyGatewayLogging](#envoygatewaylogging) +- [ProxyLogging](#proxylogging) + +| Value | Description | +| ----- | ----------- | +| `debug` | LogLevelDebug defines the "debug" logging level.
| +| `info` | LogLevelInfo defines the "Info" logging level.
| +| `warn` | LogLevelWarn defines the "Warn" logging level.
| +| `error` | LogLevelError defines the "Error" logging level.
| + + + + +#### MetricSinkType + +_Underlying type:_ _string_ + + + +_Appears in:_ +- [EnvoyGatewayMetricSink](#envoygatewaymetricsink) +- [ProxyMetricSink](#proxymetricsink) + +| Value | Description | +| ----- | ----------- | +| `OpenTelemetry` | | + + +#### OIDC + + + +OIDC defines the configuration for the OpenID Connect (OIDC) authentication. + +_Appears in:_ +- [SecurityPolicySpec](#securitypolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `provider` | _[OIDCProvider](#oidcprovider)_ | true | The OIDC Provider configuration. | +| `clientID` | _string_ | true | The client ID to be used in the OIDC
[Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). | +| `clientSecret` | _[SecretObjectReference](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.SecretObjectReference)_ | true | The Kubernetes secret which contains the OIDC client secret to be used in the
[Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest).

This is an Opaque secret. The client secret should be stored in the key
"client-secret". | +| `cookieNames` | _[OIDCCookieNames](#oidccookienames)_ | false | The optional cookie name overrides to be used for Bearer and IdToken cookies in the
[Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest).
If not specified, uses a randomly generated suffix | +| `scopes` | _string array_ | false | The OIDC scopes to be used in the
[Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest).
The "openid" scope is always added to the list of scopes if not already
specified. | +| `resources` | _string array_ | false | The OIDC resources to be used in the
[Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). | +| `redirectURL` | _string_ | true | The redirect URL to be used in the OIDC
[Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest).
If not specified, uses the default redirect URI "%REQ(x-forwarded-proto)%://%REQ(:authority)%/oauth2/callback" | +| `logoutPath` | _string_ | true | The path to log a user out, clearing their credential cookies.

If not specified, uses a default logout path "/logout" | +| `forwardAccessToken` | _boolean_ | false | ForwardAccessToken indicates whether the Envoy should forward the access token
via the Authorization header Bearer scheme to the upstream.
If not specified, defaults to false. | +| `defaultTokenTTL` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | false | DefaultTokenTTL is the default lifetime of the id token and access token.
Please note that Envoy will always use the expiry time from the response
of the authorization server if it is provided. This field is only used when
the expiry time is not provided by the authorization.

If not specified, defaults to 0. In this case, the "expires_in" field in
the authorization response must be set by the authorization server, or the
OAuth flow will fail. | +| `refreshToken` | _boolean_ | false | RefreshToken indicates whether the Envoy should automatically refresh the
id token and access token when they expire.
When set to true, the Envoy will use the refresh token to get a new id token
and access token when they expire.

If not specified, defaults to false. | +| `defaultRefreshTokenTTL` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | false | DefaultRefreshTokenTTL is the default lifetime of the refresh token.
This field is only used when the exp (expiration time) claim is omitted in
the refresh token or the refresh token is not JWT.

If not specified, defaults to 604800s (one week).
Note: this field is only applicable when the "refreshToken" field is set to true. | + + +#### OIDCCookieNames + + + +OIDCCookieNames defines the names of cookies to use in the Envoy OIDC filter. + +_Appears in:_ +- [OIDC](#oidc) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `accessToken` | _string_ | false | The name of the cookie used to store the AccessToken in the
[Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest).
If not specified, defaults to "AccessToken-(randomly generated uid)" | +| `idToken` | _string_ | false | The name of the cookie used to store the IdToken in the
[Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest).
If not specified, defaults to "IdToken-(randomly generated uid)" | + + +#### OIDCProvider + + + +OIDCProvider defines the OIDC Provider configuration. + +_Appears in:_ +- [OIDC](#oidc) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `issuer` | _string_ | true | The OIDC Provider's [issuer identifier](https://openid.net/specs/openid-connect-discovery-1_0.html#IssuerDiscovery).
Issuer MUST be a URI RFC 3986 [RFC3986] with a scheme component that MUST
be https, a host component, and optionally, port and path components and
no query or fragment components. | +| `authorizationEndpoint` | _string_ | false | The OIDC Provider's [authorization endpoint](https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint).
If not provided, EG will try to discover it from the provider's [Well-Known Configuration Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse). | +| `tokenEndpoint` | _string_ | false | The OIDC Provider's [token endpoint](https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint).
If not provided, EG will try to discover it from the provider's [Well-Known Configuration Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse). | + + +#### OpenTelemetryEnvoyProxyAccessLog + + + +OpenTelemetryEnvoyProxyAccessLog defines the OpenTelemetry access log sink. + +_Appears in:_ +- [ProxyAccessLogSink](#proxyaccesslogsink) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `host` | _string_ | false | Host define the extension service hostname.
Deprecated: Use BackendRefs instead. | +| `port` | _integer_ | false | Port defines the port the extension service is exposed on.
Deprecated: Use BackendRefs instead. | +| `backendRefs` | _[BackendRef](#backendref) array_ | false | BackendRefs references a Kubernetes object that represents the
backend server to which the access log will be sent.
Only Service kind is supported for now. | +| `resources` | _object (keys:string, values:string)_ | false | Resources is a set of labels that describe the source of a log entry, including envoy node info.
It's recommended to follow [semantic conventions](https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/). | + + +#### Origin + +_Underlying type:_ _string_ + +Origin is defined by the scheme (protocol), hostname (domain), and port of +the URL used to access it. The hostname can be "precise" which is just the +domain name or "wildcard" which is a domain name prefixed with a single +wildcard label such as "*.example.com". +In addition to that a single wildcard (with or without scheme) can be +configured to match any origin. + + +For example, the following are valid origins: +- https://foo.example.com +- https://*.example.com +- http://foo.example.com:8080 +- http://*.example.com:8080 +- https://* + +_Appears in:_ +- [CORS](#cors) + + + +#### PassiveHealthCheck + + + +PassiveHealthCheck defines the configuration for passive health checks in the context of Envoy's Outlier Detection, +see https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/outlier + +_Appears in:_ +- [HealthCheck](#healthcheck) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `splitExternalLocalOriginErrors` | _boolean_ | false | SplitExternalLocalOriginErrors enables splitting of errors between external and local origin. | +| `interval` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | false | Interval defines the time between passive health checks. | +| `consecutiveLocalOriginFailures` | _integer_ | false | ConsecutiveLocalOriginFailures sets the number of consecutive local origin failures triggering ejection.
Parameter takes effect only when split_external_local_origin_errors is set to true. | +| `consecutiveGatewayErrors` | _integer_ | false | ConsecutiveGatewayErrors sets the number of consecutive gateway errors triggering ejection. | +| `consecutive5XxErrors` | _integer_ | false | Consecutive5xxErrors sets the number of consecutive 5xx errors triggering ejection. | +| `baseEjectionTime` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | false | BaseEjectionTime defines the base duration for which a host will be ejected on consecutive failures. | +| `maxEjectionPercent` | _integer_ | false | MaxEjectionPercent sets the maximum percentage of hosts in a cluster that can be ejected. | + + +#### PathEscapedSlashAction + +_Underlying type:_ _string_ + +PathEscapedSlashAction determines the action for requests that contain %2F, %2f, %5C, or %5c +sequences in the URI path. + +_Appears in:_ +- [PathSettings](#pathsettings) + +| Value | Description | +| ----- | ----------- | +| `KeepUnchanged` | KeepUnchangedAction keeps escaped slashes as they arrive without changes
| +| `RejectRequest` | RejectRequestAction rejects client requests containing escaped slashes
with a 400 status. gRPC requests will be rejected with the INTERNAL (13)
error code.
The "httpN.downstream_rq_failed_path_normalization" counter is incremented
for each rejected request.
| +| `UnescapeAndRedirect` | UnescapeAndRedirect unescapes %2F and %5C sequences and redirects to the new path
if these sequences were present.
Redirect occurs after path normalization and merge slashes transformations if
they were configured. gRPC requests will be rejected with the INTERNAL (13)
error code.
This option minimizes possibility of path confusion exploits by forcing request
with unescaped slashes to traverse all parties: downstream client, intermediate
proxies, Envoy and upstream server.
The “httpN.downstream_rq_redirected_with_normalized_path” counter is incremented
for each redirected request.
| +| `UnescapeAndForward` | UnescapeAndForward unescapes %2F and %5C sequences and forwards the request.
Note: this option should not be enabled if intermediaries perform path based access
control as it may lead to path confusion vulnerabilities.
| + + +#### PathSettings + + + +PathSettings provides settings that managing how the incoming path set by clients is handled. + +_Appears in:_ +- [ClientTrafficPolicySpec](#clienttrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `escapedSlashesAction` | _[PathEscapedSlashAction](#pathescapedslashaction)_ | false | EscapedSlashesAction determines how %2f, %2F, %5c, or %5C sequences in the path URI
should be handled.
The default is UnescapeAndRedirect. | +| `disableMergeSlashes` | _boolean_ | false | DisableMergeSlashes allows disabling the default configuration of merging adjacent
slashes in the path.
Note that slash merging is not part of the HTTP spec and is provided for convenience. | + + +#### PerRetryPolicy + + + + + +_Appears in:_ +- [Retry](#retry) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `timeout` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | false | Timeout is the timeout per retry attempt. | +| `backOff` | _[BackOffPolicy](#backoffpolicy)_ | false | Backoff is the backoff policy to be applied per retry attempt. gateway uses a fully jittered exponential
back-off algorithm for retries. For additional details,
see https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/router_filter#config-http-filters-router-x-envoy-max-retries | + + +#### PolicyTargetReferences + + + + + +_Appears in:_ +- [BackendTrafficPolicySpec](#backendtrafficpolicyspec) +- [ClientTrafficPolicySpec](#clienttrafficpolicyspec) +- [EnvoyExtensionPolicySpec](#envoyextensionpolicyspec) +- [SecurityPolicySpec](#securitypolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `targetRef` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName)_ | true | TargetRef is the name of the resource this policy is being attached to.
This policy and the TargetRef MUST be in the same namespace for this
Policy to have effect

Deprecated: use targetRefs/targetSelectors instead | +| `targetRefs` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName) array_ | true | TargetRefs are the names of the Gateway resources this policy
is being attached to. | +| `targetSelectors` | _[TargetSelector](#targetselector) array_ | true | TargetSelectors allow targeting resources for this policy based on labels | + + +#### Principal + + + +Principal specifies the client identity of a request. +A client identity can be a client IP, a JWT claim, username from the Authorization header, +or any other identity that can be extracted from a custom header. +Currently, only the client IP is supported. + +_Appears in:_ +- [AuthorizationRule](#authorizationrule) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `clientCIDRs` | _[CIDR](#cidr) array_ | true | ClientCIDRs are the IP CIDR ranges of the client.
Valid examples are "192.168.1.0/24" or "2001:db8::/64"

The client IP is inferred from the X-Forwarded-For header, a custom header,
or the proxy protocol.
You can use the `ClientIPDetection` or the `EnableProxyProtocol` field in
the `ClientTrafficPolicy` to configure how the client IP is detected. | + + +#### ProcessingModeOptions + + + +ProcessingModeOptions defines if headers or body should be processed by the external service + +_Appears in:_ +- [ExtProcProcessingMode](#extprocprocessingmode) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `body` | _[ExtProcBodyProcessingMode](#extprocbodyprocessingmode)_ | false | Defines body processing mode | + + +#### ProviderType + +_Underlying type:_ _string_ + +ProviderType defines the types of providers supported by Envoy Gateway. + +_Appears in:_ +- [EnvoyGatewayProvider](#envoygatewayprovider) +- [EnvoyProxyProvider](#envoyproxyprovider) + +| Value | Description | +| ----- | ----------- | +| `Kubernetes` | ProviderTypeKubernetes defines the "Kubernetes" provider.
| +| `File` | ProviderTypeFile defines the "File" provider. This type is not implemented
until https://github.com/envoyproxy/gateway/issues/1001 is fixed.
| + + +#### ProxyAccessLog + + + + + +_Appears in:_ +- [ProxyTelemetry](#proxytelemetry) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `disable` | _boolean_ | true | Disable disables access logging for managed proxies if set to true. | +| `settings` | _[ProxyAccessLogSetting](#proxyaccesslogsetting) array_ | false | Settings defines accesslog settings for managed proxies.
If unspecified, will send default format to stdout. | + + +#### ProxyAccessLogFormat + + + +ProxyAccessLogFormat defines the format of accesslog. +By default accesslogs are written to standard output. + +_Appears in:_ +- [ProxyAccessLogSetting](#proxyaccesslogsetting) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[ProxyAccessLogFormatType](#proxyaccesslogformattype)_ | true | Type defines the type of accesslog format. | +| `text` | _string_ | false | Text defines the text accesslog format, following Envoy accesslog formatting,
It's required when the format type is "Text".
Envoy [command operators](https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#command-operators) may be used in the format.
The [format string documentation](https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#config-access-log-format-strings) provides more information. | +| `json` | _object (keys:string, values:string)_ | false | JSON is additional attributes that describe the specific event occurrence.
Structured format for the envoy access logs. Envoy [command operators](https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#command-operators)
can be used as values for fields within the Struct.
It's required when the format type is "JSON". | + + +#### ProxyAccessLogFormatType + +_Underlying type:_ _string_ + + + +_Appears in:_ +- [ProxyAccessLogFormat](#proxyaccesslogformat) + +| Value | Description | +| ----- | ----------- | +| `Text` | ProxyAccessLogFormatTypeText defines the text accesslog format.
| +| `JSON` | ProxyAccessLogFormatTypeJSON defines the JSON accesslog format.
| + + +#### ProxyAccessLogSetting + + + + + +_Appears in:_ +- [ProxyAccessLog](#proxyaccesslog) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `format` | _[ProxyAccessLogFormat](#proxyaccesslogformat)_ | false | Format defines the format of accesslog.
This will be ignored if sink type is ALS. | +| `sinks` | _[ProxyAccessLogSink](#proxyaccesslogsink) array_ | true | Sinks defines the sinks of accesslog. | + + +#### ProxyAccessLogSink + + + +ProxyAccessLogSink defines the sink of accesslog. + +_Appears in:_ +- [ProxyAccessLogSetting](#proxyaccesslogsetting) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[ProxyAccessLogSinkType](#proxyaccesslogsinktype)_ | true | Type defines the type of accesslog sink. | +| `als` | _[ALSEnvoyProxyAccessLog](#alsenvoyproxyaccesslog)_ | false | ALS defines the gRPC Access Log Service (ALS) sink. | +| `file` | _[FileEnvoyProxyAccessLog](#fileenvoyproxyaccesslog)_ | false | File defines the file accesslog sink. | +| `openTelemetry` | _[OpenTelemetryEnvoyProxyAccessLog](#opentelemetryenvoyproxyaccesslog)_ | false | OpenTelemetry defines the OpenTelemetry accesslog sink. | + + +#### ProxyAccessLogSinkType + +_Underlying type:_ _string_ + + + +_Appears in:_ +- [ProxyAccessLogSink](#proxyaccesslogsink) + +| Value | Description | +| ----- | ----------- | +| `ALS` | ProxyAccessLogSinkTypeALS defines the gRPC Access Log Service (ALS) sink.
The service must implement the Envoy gRPC Access Log Service streaming API:
https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/accesslog/v3/als.proto
| +| `File` | ProxyAccessLogSinkTypeFile defines the file accesslog sink.
| +| `OpenTelemetry` | ProxyAccessLogSinkTypeOpenTelemetry defines the OpenTelemetry accesslog sink.
When the provider is Kubernetes, EnvoyGateway always sends `k8s.namespace.name`
and `k8s.pod.name` as additional attributes.
| + + +#### ProxyBootstrap + + + +ProxyBootstrap defines Envoy Bootstrap configuration. + +_Appears in:_ +- [EnvoyProxySpec](#envoyproxyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[BootstrapType](#bootstraptype)_ | false | Type is the type of the bootstrap configuration, it should be either Replace or Merge.
If unspecified, it defaults to Replace. | +| `value` | _string_ | true | Value is a YAML string of the bootstrap. | + + +#### ProxyLogComponent + +_Underlying type:_ _string_ + +ProxyLogComponent defines a component that supports a configured logging level. + +_Appears in:_ +- [ProxyLogging](#proxylogging) + +| Value | Description | +| ----- | ----------- | +| `default` | LogComponentDefault defines the default logging component.
See more details: https://www.envoyproxy.io/docs/envoy/latest/operations/cli#cmdoption-l
| +| `upstream` | LogComponentUpstream defines the "upstream" logging component.
| +| `http` | LogComponentHTTP defines the "http" logging component.
| +| `connection` | LogComponentConnection defines the "connection" logging component.
| +| `admin` | LogComponentAdmin defines the "admin" logging component.
| +| `client` | LogComponentClient defines the "client" logging component.
| +| `filter` | LogComponentFilter defines the "filter" logging component.
| +| `main` | LogComponentMain defines the "main" logging component.
| +| `router` | LogComponentRouter defines the "router" logging component.
| +| `runtime` | LogComponentRuntime defines the "runtime" logging component.
| + + +#### ProxyLogging + + + +ProxyLogging defines logging parameters for managed proxies. + +_Appears in:_ +- [EnvoyProxySpec](#envoyproxyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `level` | _object (keys:[ProxyLogComponent](#proxylogcomponent), values:[LogLevel](#loglevel))_ | true | Level is a map of logging level per component, where the component is the key
and the log level is the value. If unspecified, defaults to "default: warn". | + + +#### ProxyMetricSink + + + +ProxyMetricSink defines the sink of metrics. +Default metrics sink is OpenTelemetry. + +_Appears in:_ +- [ProxyMetrics](#proxymetrics) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[MetricSinkType](#metricsinktype)_ | true | Type defines the metric sink type.
EG currently only supports OpenTelemetry. | +| `openTelemetry` | _[ProxyOpenTelemetrySink](#proxyopentelemetrysink)_ | false | OpenTelemetry defines the configuration for OpenTelemetry sink.
It's required if the sink type is OpenTelemetry. | + + +#### ProxyMetrics + + + + + +_Appears in:_ +- [ProxyTelemetry](#proxytelemetry) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `prometheus` | _[ProxyPrometheusProvider](#proxyprometheusprovider)_ | true | Prometheus defines the configuration for Admin endpoint `/stats/prometheus`. | +| `sinks` | _[ProxyMetricSink](#proxymetricsink) array_ | true | Sinks defines the metric sinks where metrics are sent to. | +| `matches` | _[StringMatch](#stringmatch) array_ | true | Matches defines configuration for selecting specific metrics instead of generating all metrics stats
that are enabled by default. This helps reduce CPU and memory overhead in Envoy, but eliminating some stats
may after critical functionality. Here are the stats that we strongly recommend not disabling:
`cluster_manager.warming_clusters`, `cluster..membership_total`,`cluster..membership_healthy`,
`cluster..membership_degraded`,reference https://github.com/envoyproxy/envoy/issues/9856,
https://github.com/envoyproxy/envoy/issues/14610 | +| `enableVirtualHostStats` | _boolean_ | true | EnableVirtualHostStats enables envoy stat metrics for virtual hosts. | +| `enablePerEndpointStats` | _boolean_ | true | EnablePerEndpointStats enables per endpoint envoy stats metrics.
Please use with caution. | + + +#### ProxyOpenTelemetrySink + + + +ProxyOpenTelemetrySink defines the configuration for OpenTelemetry sink. + +_Appears in:_ +- [ProxyMetricSink](#proxymetricsink) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `host` | _string_ | false | Host define the service hostname.
Deprecated: Use BackendRefs instead. | +| `port` | _integer_ | false | Port defines the port the service is exposed on.
Deprecated: Use BackendRefs instead. | +| `backendRefs` | _[BackendRef](#backendref) array_ | false | BackendRefs references a Kubernetes object that represents the
backend server to which the metric will be sent.
Only Service kind is supported for now. | + + +#### ProxyPrometheusProvider + + + + + +_Appears in:_ +- [ProxyMetrics](#proxymetrics) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `disable` | _boolean_ | true | Disable the Prometheus endpoint. | +| `compression` | _[Compression](#compression)_ | false | Configure the compression on Prometheus endpoint. Compression is useful in situations when bandwidth is scarce and large payloads can be effectively compressed at the expense of higher CPU load. | + + +#### ProxyProtocol + + + +ProxyProtocol defines the configuration related to the proxy protocol +when communicating with the backend. + +_Appears in:_ +- [BackendTrafficPolicySpec](#backendtrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `version` | _[ProxyProtocolVersion](#proxyprotocolversion)_ | true | Version of ProxyProtol
Valid ProxyProtocolVersion values are
"V1"
"V2" | + + +#### ProxyProtocolVersion + +_Underlying type:_ _string_ + +ProxyProtocolVersion defines the version of the Proxy Protocol to use. + +_Appears in:_ +- [ProxyProtocol](#proxyprotocol) + +| Value | Description | +| ----- | ----------- | +| `V1` | ProxyProtocolVersionV1 is the PROXY protocol version 1 (human readable format).
| +| `V2` | ProxyProtocolVersionV2 is the PROXY protocol version 2 (binary format).
| + + +#### ProxyTelemetry + + + + + +_Appears in:_ +- [EnvoyProxySpec](#envoyproxyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `accessLog` | _[ProxyAccessLog](#proxyaccesslog)_ | false | AccessLogs defines accesslog parameters for managed proxies.
If unspecified, will send default format to stdout. | +| `tracing` | _[ProxyTracing](#proxytracing)_ | false | Tracing defines tracing configuration for managed proxies.
If unspecified, will not send tracing data. | +| `metrics` | _[ProxyMetrics](#proxymetrics)_ | true | Metrics defines metrics configuration for managed proxies. | + + +#### ProxyTracing + + + + + +_Appears in:_ +- [ProxyTelemetry](#proxytelemetry) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `samplingRate` | _integer_ | false | SamplingRate controls the rate at which traffic will be
selected for tracing if no prior sampling decision has been made.
Defaults to 100, valid values [0-100]. 100 indicates 100% sampling. | +| `customTags` | _object (keys:string, values:[CustomTag](#customtag))_ | true | CustomTags defines the custom tags to add to each span.
If provider is kubernetes, pod name and namespace are added by default. | +| `provider` | _[TracingProvider](#tracingprovider)_ | true | Provider defines the tracing provider. | + + +#### RateLimit + + + +RateLimit defines the configuration associated with the Rate Limit Service +used for Global Rate Limiting. + +_Appears in:_ +- [EnvoyGateway](#envoygateway) +- [EnvoyGatewaySpec](#envoygatewayspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `backend` | _[RateLimitDatabaseBackend](#ratelimitdatabasebackend)_ | true | Backend holds the configuration associated with the
database backend used by the rate limit service to store
state associated with global ratelimiting. | +| `timeout` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | false | Timeout specifies the timeout period for the proxy to access the ratelimit server
If not set, timeout is 20ms. | +| `failClosed` | _boolean_ | true | FailClosed is a switch used to control the flow of traffic
when the response from the ratelimit server cannot be obtained.
If FailClosed is false, let the traffic pass,
otherwise, don't let the traffic pass and return 500.
If not set, FailClosed is False. | +| `telemetry` | _[RateLimitTelemetry](#ratelimittelemetry)_ | false | Telemetry defines telemetry configuration for RateLimit. | + + +#### RateLimitDatabaseBackend + + + +RateLimitDatabaseBackend defines the configuration associated with +the database backend used by the rate limit service. + +_Appears in:_ +- [RateLimit](#ratelimit) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[RateLimitDatabaseBackendType](#ratelimitdatabasebackendtype)_ | true | Type is the type of database backend to use. Supported types are:
* Redis: Connects to a Redis database. | +| `redis` | _[RateLimitRedisSettings](#ratelimitredissettings)_ | false | Redis defines the settings needed to connect to a Redis database. | + + +#### RateLimitDatabaseBackendType + +_Underlying type:_ _string_ + +RateLimitDatabaseBackendType specifies the types of database backend +to be used by the rate limit service. + +_Appears in:_ +- [RateLimitDatabaseBackend](#ratelimitdatabasebackend) + +| Value | Description | +| ----- | ----------- | +| `Redis` | RedisBackendType uses a redis database for the rate limit service.
| + + +#### RateLimitMetrics + + + + + +_Appears in:_ +- [RateLimitTelemetry](#ratelimittelemetry) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `prometheus` | _[RateLimitMetricsPrometheusProvider](#ratelimitmetricsprometheusprovider)_ | true | Prometheus defines the configuration for prometheus endpoint. | + + +#### RateLimitMetricsPrometheusProvider + + + + + +_Appears in:_ +- [RateLimitMetrics](#ratelimitmetrics) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `disable` | _boolean_ | true | Disable the Prometheus endpoint. | + + +#### RateLimitRedisSettings + + + +RateLimitRedisSettings defines the configuration for connecting to redis database. + +_Appears in:_ +- [RateLimitDatabaseBackend](#ratelimitdatabasebackend) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `url` | _string_ | true | URL of the Redis Database. | +| `tls` | _[RedisTLSSettings](#redistlssettings)_ | false | TLS defines TLS configuration for connecting to redis database. | + + +#### RateLimitRule + + + +RateLimitRule defines the semantics for matching attributes +from the incoming requests, and setting limits for them. + +_Appears in:_ +- [GlobalRateLimit](#globalratelimit) +- [LocalRateLimit](#localratelimit) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `clientSelectors` | _[RateLimitSelectCondition](#ratelimitselectcondition) array_ | false | ClientSelectors holds the list of select conditions to select
specific clients using attributes from the traffic flow.
All individual select conditions must hold True for this rule
and its limit to be applied.

If no client selectors are specified, the rule applies to all traffic of
the targeted Route.

If the policy targets a Gateway, the rule applies to each Route of the Gateway.
Please note that each Route has its own rate limit counters. For example,
if a Gateway has two Routes, and the policy has a rule with limit 10rps,
each Route will have its own 10rps limit. | +| `limit` | _[RateLimitValue](#ratelimitvalue)_ | true | Limit holds the rate limit values.
This limit is applied for traffic flows when the selectors
compute to True, causing the request to be counted towards the limit.
The limit is enforced and the request is ratelimited, i.e. a response with
429 HTTP status code is sent back to the client when
the selected requests have reached the limit. | + + +#### RateLimitSelectCondition + + + +RateLimitSelectCondition specifies the attributes within the traffic flow that can +be used to select a subset of clients to be ratelimited. +All the individual conditions must hold True for the overall condition to hold True. + +_Appears in:_ +- [RateLimitRule](#ratelimitrule) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `headers` | _[HeaderMatch](#headermatch) array_ | false | Headers is a list of request headers to match. Multiple header values are ANDed together,
meaning, a request MUST match all the specified headers.
At least one of headers or sourceCIDR condition must be specified. | +| `sourceCIDR` | _[SourceMatch](#sourcematch)_ | false | SourceCIDR is the client IP Address range to match on.
At least one of headers or sourceCIDR condition must be specified. | + + +#### RateLimitSpec + + + +RateLimitSpec defines the desired state of RateLimitSpec. + +_Appears in:_ +- [BackendTrafficPolicySpec](#backendtrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[RateLimitType](#ratelimittype)_ | true | Type decides the scope for the RateLimits.
Valid RateLimitType values are "Global" or "Local". | +| `global` | _[GlobalRateLimit](#globalratelimit)_ | false | Global defines global rate limit configuration. | +| `local` | _[LocalRateLimit](#localratelimit)_ | false | Local defines local rate limit configuration. | + + +#### RateLimitTelemetry + + + + + +_Appears in:_ +- [RateLimit](#ratelimit) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `metrics` | _[RateLimitMetrics](#ratelimitmetrics)_ | true | Metrics defines metrics configuration for RateLimit. | +| `tracing` | _[RateLimitTracing](#ratelimittracing)_ | true | Tracing defines traces configuration for RateLimit. | + + +#### RateLimitTracing + + + + + +_Appears in:_ +- [RateLimitTelemetry](#ratelimittelemetry) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `samplingRate` | _integer_ | false | SamplingRate controls the rate at which traffic will be
selected for tracing if no prior sampling decision has been made.
Defaults to 100, valid values [0-100]. 100 indicates 100% sampling. | +| `provider` | _[RateLimitTracingProvider](#ratelimittracingprovider)_ | true | Provider defines the rateLimit tracing provider.
Only OpenTelemetry is supported currently. | + + +#### RateLimitTracingProvider + + + +RateLimitTracingProvider defines the tracing provider configuration of RateLimit + +_Appears in:_ +- [RateLimitTracing](#ratelimittracing) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[RateLimitTracingProviderType](#ratelimittracingprovidertype)_ | true | Type defines the tracing provider type.
Since to RateLimit Exporter currently using OpenTelemetry, only OpenTelemetry is supported | +| `url` | _string_ | true | URL is the endpoint of the trace collector that supports the OTLP protocol | + + + + +#### RateLimitType + +_Underlying type:_ _string_ + +RateLimitType specifies the types of RateLimiting. + +_Appears in:_ +- [RateLimitSpec](#ratelimitspec) + +| Value | Description | +| ----- | ----------- | +| `Global` | GlobalRateLimitType allows the rate limits to be applied across all Envoy
proxy instances.
| +| `Local` | LocalRateLimitType allows the rate limits to be applied on a per Envoy
proxy instance basis.
| + + +#### RateLimitUnit + +_Underlying type:_ _string_ + +RateLimitUnit specifies the intervals for setting rate limits. +Valid RateLimitUnit values are "Second", "Minute", "Hour", and "Day". + +_Appears in:_ +- [RateLimitValue](#ratelimitvalue) + +| Value | Description | +| ----- | ----------- | +| `Second` | RateLimitUnitSecond specifies the rate limit interval to be 1 second.
| +| `Minute` | RateLimitUnitMinute specifies the rate limit interval to be 1 minute.
| +| `Hour` | RateLimitUnitHour specifies the rate limit interval to be 1 hour.
| +| `Day` | RateLimitUnitDay specifies the rate limit interval to be 1 day.
| + + +#### RateLimitValue + + + +RateLimitValue defines the limits for rate limiting. + +_Appears in:_ +- [RateLimitRule](#ratelimitrule) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `requests` | _integer_ | true | | +| `unit` | _[RateLimitUnit](#ratelimitunit)_ | true | | + + +#### RedisTLSSettings + + + +RedisTLSSettings defines the TLS configuration for connecting to redis database. + +_Appears in:_ +- [RateLimitRedisSettings](#ratelimitredissettings) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `certificateRef` | _[SecretObjectReference](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.SecretObjectReference)_ | false | CertificateRef defines the client certificate reference for TLS connections.
Currently only a Kubernetes Secret of type TLS is supported. | + + +#### RemoteJWKS + + + +RemoteJWKS defines how to fetch and cache JSON Web Key Sets (JWKS) from a remote +HTTP/HTTPS endpoint. + +_Appears in:_ +- [JWTProvider](#jwtprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `uri` | _string_ | true | URI is the HTTPS URI to fetch the JWKS. Envoy's system trust bundle is used to
validate the server certificate. | + + +#### RequestHeaderCustomTag + + + +RequestHeaderCustomTag adds value from request header to each span. + +_Appears in:_ +- [CustomTag](#customtag) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `name` | _string_ | true | Name defines the name of the request header which to extract the value from. | +| `defaultValue` | _string_ | false | DefaultValue defines the default value to use if the request header is not set. | + + +#### ResourceProviderType + +_Underlying type:_ _string_ + +ResourceProviderType defines the types of custom resource providers supported by Envoy Gateway. + +_Appears in:_ +- [EnvoyGatewayResourceProvider](#envoygatewayresourceprovider) + +| Value | Description | +| ----- | ----------- | +| `File` | ResourceProviderTypeFile defines the "File" provider.
| + + +#### Retry + + + +Retry defines the retry strategy to be applied. + +_Appears in:_ +- [BackendTrafficPolicySpec](#backendtrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `numRetries` | _integer_ | false | NumRetries is the number of retries to be attempted. Defaults to 2. | +| `retryOn` | _[RetryOn](#retryon)_ | false | RetryOn specifies the retry trigger condition.

If not specified, the default is to retry on connect-failure,refused-stream,unavailable,cancelled,retriable-status-codes(503). | +| `perRetry` | _[PerRetryPolicy](#perretrypolicy)_ | false | PerRetry is the retry policy to be applied per retry attempt. | + + +#### RetryOn + + + + + +_Appears in:_ +- [Retry](#retry) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `triggers` | _[TriggerEnum](#triggerenum) array_ | false | Triggers specifies the retry trigger condition(Http/Grpc). | +| `httpStatusCodes` | _[HTTPStatus](#httpstatus) array_ | false | HttpStatusCodes specifies the http status codes to be retried.
The retriable-status-codes trigger must also be configured for these status codes to trigger a retry. | + + +#### RoutingType + +_Underlying type:_ _string_ + +RoutingType defines the type of routing of this Envoy proxy. + +_Appears in:_ +- [EnvoyProxySpec](#envoyproxyspec) + +| Value | Description | +| ----- | ----------- | +| `Service` | ServiceRoutingType is the RoutingType for Service Cluster IP routing.
| +| `Endpoint` | EndpointRoutingType is the RoutingType for Endpoint routing.
| + + +#### SecurityPolicy + + + +SecurityPolicy allows the user to configure various security settings for a +Gateway. + +_Appears in:_ +- [SecurityPolicyList](#securitypolicylist) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `apiVersion` | _string_ | |`gateway.envoyproxy.io/v1alpha1` +| `kind` | _string_ | |`SecurityPolicy` +| `metadata` | _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#objectmeta-v1-meta)_ | true | Refer to Kubernetes API documentation for fields of `metadata`. | +| `spec` | _[SecurityPolicySpec](#securitypolicyspec)_ | true | Spec defines the desired state of SecurityPolicy. | +| `status` | _[PolicyStatus](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.PolicyStatus)_ | true | Status defines the current status of SecurityPolicy. | + + +#### SecurityPolicyList + + + +SecurityPolicyList contains a list of SecurityPolicy resources. + + + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `apiVersion` | _string_ | |`gateway.envoyproxy.io/v1alpha1` +| `kind` | _string_ | |`SecurityPolicyList` +| `metadata` | _[ListMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#listmeta-v1-meta)_ | true | Refer to Kubernetes API documentation for fields of `metadata`. | +| `items` | _[SecurityPolicy](#securitypolicy) array_ | true | | + + +#### SecurityPolicySpec + + + +SecurityPolicySpec defines the desired state of SecurityPolicy. + +_Appears in:_ +- [SecurityPolicy](#securitypolicy) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `targetRef` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName)_ | true | TargetRef is the name of the resource this policy is being attached to.
This policy and the TargetRef MUST be in the same namespace for this
Policy to have effect

Deprecated: use targetRefs/targetSelectors instead | +| `targetRefs` | _[LocalPolicyTargetReferenceWithSectionName](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.LocalPolicyTargetReferenceWithSectionName) array_ | true | TargetRefs are the names of the Gateway resources this policy
is being attached to. | +| `targetSelectors` | _[TargetSelector](#targetselector) array_ | true | TargetSelectors allow targeting resources for this policy based on labels | +| `cors` | _[CORS](#cors)_ | false | CORS defines the configuration for Cross-Origin Resource Sharing (CORS). | +| `basicAuth` | _[BasicAuth](#basicauth)_ | false | BasicAuth defines the configuration for the HTTP Basic Authentication. | +| `jwt` | _[JWT](#jwt)_ | false | JWT defines the configuration for JSON Web Token (JWT) authentication. | +| `oidc` | _[OIDC](#oidc)_ | false | OIDC defines the configuration for the OpenID Connect (OIDC) authentication. | +| `extAuth` | _[ExtAuth](#extauth)_ | false | ExtAuth defines the configuration for External Authorization. | + + +#### ServiceExternalTrafficPolicy + +_Underlying type:_ _string_ + +ServiceExternalTrafficPolicy describes how nodes distribute service traffic they +receive on one of the Service's "externally-facing" addresses (NodePorts, ExternalIPs, +and LoadBalancer IPs. + +_Appears in:_ +- [KubernetesServiceSpec](#kubernetesservicespec) + +| Value | Description | +| ----- | ----------- | +| `Cluster` | ServiceExternalTrafficPolicyCluster routes traffic to all endpoints.
| +| `Local` | ServiceExternalTrafficPolicyLocal preserves the source IP of the traffic by
routing only to endpoints on the same node as the traffic was received on
(dropping the traffic if there are no local endpoints).
| + + +#### ServiceType + +_Underlying type:_ _string_ + +ServiceType string describes ingress methods for a service + +_Appears in:_ +- [KubernetesServiceSpec](#kubernetesservicespec) + +| Value | Description | +| ----- | ----------- | +| `ClusterIP` | ServiceTypeClusterIP means a service will only be accessible inside the
cluster, via the cluster IP.
| +| `LoadBalancer` | ServiceTypeLoadBalancer means a service will be exposed via an
external load balancer (if the cloud provider supports it).
| +| `NodePort` | ServiceTypeNodePort means a service will be exposed on each Kubernetes Node
at a static Port, common across all Nodes.
| + + +#### ShutdownConfig + + + +ShutdownConfig defines configuration for graceful envoy shutdown process. + +_Appears in:_ +- [EnvoyProxySpec](#envoyproxyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `drainTimeout` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | false | DrainTimeout defines the graceful drain timeout. This should be less than the pod's terminationGracePeriodSeconds.
If unspecified, defaults to 600 seconds. | +| `minDrainDuration` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | false | MinDrainDuration defines the minimum drain duration allowing time for endpoint deprogramming to complete.
If unspecified, defaults to 5 seconds. | + + +#### ShutdownManager + + + +ShutdownManager defines the configuration for the shutdown manager. + +_Appears in:_ +- [EnvoyGatewayKubernetesProvider](#envoygatewaykubernetesprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `image` | _string_ | true | Image specifies the ShutdownManager container image to be used, instead of the default image. | + + +#### SlowStart + + + +SlowStart defines the configuration related to the slow start load balancer policy. + +_Appears in:_ +- [LoadBalancer](#loadbalancer) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `window` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | true | Window defines the duration of the warm up period for newly added host.
During slow start window, traffic sent to the newly added hosts will gradually increase.
Currently only supports linear growth of traffic. For additional details,
see https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/cluster.proto#config-cluster-v3-cluster-slowstartconfig | + + + + +#### SourceMatchType + +_Underlying type:_ _string_ + + + +_Appears in:_ +- [SourceMatch](#sourcematch) + +| Value | Description | +| ----- | ----------- | +| `Exact` | SourceMatchExact All IP Addresses within the specified Source IP CIDR are treated as a single client selector
and share the same rate limit bucket.
| +| `Distinct` | SourceMatchDistinct Each IP Address within the specified Source IP CIDR is treated as a distinct client selector
and uses a separate rate limit bucket/counter.
Note: This is only supported for Global Rate Limits.
| + + +#### StringMatch + + + +StringMatch defines how to match any strings. +This is a general purpose match condition that can be used by other EG APIs +that need to match against a string. + +_Appears in:_ +- [ProxyMetrics](#proxymetrics) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[StringMatchType](#stringmatchtype)_ | false | Type specifies how to match against a string. | +| `value` | _string_ | true | Value specifies the string value that the match must have. | + + +#### StringMatchType + +_Underlying type:_ _string_ + +StringMatchType specifies the semantics of how a string value should be compared. +Valid MatchType values are "Exact", "Prefix", "Suffix", "RegularExpression". + +_Appears in:_ +- [StringMatch](#stringmatch) + +| Value | Description | +| ----- | ----------- | +| `Exact` | StringMatchExact :the input string must match exactly the match value.
| +| `Prefix` | StringMatchPrefix :the input string must start with the match value.
| +| `Suffix` | StringMatchSuffix :the input string must end with the match value.
| +| `RegularExpression` | StringMatchRegularExpression :The input string must match the regular expression
specified in the match value.
The regex string must adhere to the syntax documented in
https://github.com/google/re2/wiki/Syntax.
| + + +#### TCPActiveHealthChecker + + + +TCPActiveHealthChecker defines the settings of tcp health check. + +_Appears in:_ +- [ActiveHealthCheck](#activehealthcheck) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `send` | _[ActiveHealthCheckPayload](#activehealthcheckpayload)_ | false | Send defines the request payload. | +| `receive` | _[ActiveHealthCheckPayload](#activehealthcheckpayload)_ | false | Receive defines the expected response payload. | + + +#### TCPClientTimeout + + + +TCPClientTimeout only provides timeout configuration on the listener whose protocol is TCP or TLS. + +_Appears in:_ +- [ClientTimeout](#clienttimeout) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `idleTimeout` | _[Duration](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Duration)_ | false | IdleTimeout for a TCP connection. Idle time is defined as a period in which there are no
bytes sent or received on either the upstream or downstream connection.
Default: 1 hour. | + + +#### TCPKeepalive + + + +TCPKeepalive define the TCP Keepalive configuration. + +_Appears in:_ +- [BackendTrafficPolicySpec](#backendtrafficpolicyspec) +- [ClientTrafficPolicySpec](#clienttrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `probes` | _integer_ | false | The total number of unacknowledged probes to send before deciding
the connection is dead.
Defaults to 9. | +| `idleTime` | _[Duration](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Duration)_ | false | The duration a connection needs to be idle before keep-alive
probes start being sent.
The duration format is
Defaults to `7200s`. | +| `interval` | _[Duration](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Duration)_ | false | The duration between keep-alive probes.
Defaults to `75s`. | + + +#### TCPTimeout + + + + + +_Appears in:_ +- [Timeout](#timeout) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `connectTimeout` | _[Duration](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Duration)_ | false | The timeout for network connection establishment, including TCP and TLS handshakes.
Default: 10 seconds. | + + +#### TLSSettings + + + + + +_Appears in:_ +- [BackendTLSConfig](#backendtlsconfig) +- [ClientTLSSettings](#clienttlssettings) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `minVersion` | _[TLSVersion](#tlsversion)_ | false | Min specifies the minimal TLS protocol version to allow.
The default is TLS 1.2 if this is not specified. | +| `maxVersion` | _[TLSVersion](#tlsversion)_ | false | Max specifies the maximal TLS protocol version to allow
The default is TLS 1.3 if this is not specified. | +| `ciphers` | _string array_ | false | Ciphers specifies the set of cipher suites supported when
negotiating TLS 1.0 - 1.2. This setting has no effect for TLS 1.3.
In non-FIPS Envoy Proxy builds the default cipher 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
In builds using BoringSSL FIPS the default cipher list is:
- ECDHE-ECDSA-AES128-GCM-SHA256
- ECDHE-RSA-AES128-GCM-SHA256
- ECDHE-ECDSA-AES256-GCM-SHA384
- ECDHE-RSA-AES256-GCM-SHA384 | +| `ecdhCurves` | _string array_ | false | ECDHCurves specifies the set of supported ECDH curves.
In non-FIPS Envoy Proxy builds the default curves are:
- X25519
- P-256
In builds using BoringSSL FIPS the default curve is:
- P-256 | +| `signatureAlgorithms` | _string array_ | false | SignatureAlgorithms specifies which signature algorithms the listener should
support. | +| `alpnProtocols` | _[ALPNProtocol](#alpnprotocol) array_ | false | ALPNProtocols supplies the list of ALPN protocols that should be
exposed by the listener. By default h2 and http/1.1 are enabled.
Supported values are:
- http/1.0
- http/1.1
- h2 | + + +#### TLSVersion + +_Underlying type:_ _string_ + +TLSVersion specifies the TLS version + +_Appears in:_ +- [BackendTLSConfig](#backendtlsconfig) +- [ClientTLSSettings](#clienttlssettings) +- [TLSSettings](#tlssettings) + +| Value | Description | +| ----- | ----------- | +| `Auto` | TLSAuto allows Envoy to choose the optimal TLS Version
| +| `1.0` | TLS1.0 specifies TLS version 1.0
| +| `1.1` | TLS1.1 specifies TLS version 1.1
| +| `1.2` | TLSv1.2 specifies TLS version 1.2
| +| `1.3` | TLSv1.3 specifies TLS version 1.3
| + + +#### TargetSelector + + + + + +_Appears in:_ +- [BackendTrafficPolicySpec](#backendtrafficpolicyspec) +- [ClientTrafficPolicySpec](#clienttrafficpolicyspec) +- [EnvoyExtensionPolicySpec](#envoyextensionpolicyspec) +- [PolicyTargetReferences](#policytargetreferences) +- [SecurityPolicySpec](#securitypolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `group` | _[Group](#group)_ | true | Group is the group that this selector targets. Defaults to gateway.networking.k8s.io | +| `kind` | _[Kind](#kind)_ | true | Kind is the resource kind that this selector targets. | +| `matchLabels` | _object (keys:string, values:string)_ | true | MatchLabels are the set of label selectors for identifying the targeted resource | + + +#### Timeout + + + +Timeout defines configuration for timeouts related to connections. + +_Appears in:_ +- [BackendTrafficPolicySpec](#backendtrafficpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `tcp` | _[TCPTimeout](#tcptimeout)_ | false | Timeout settings for TCP. | +| `http` | _[HTTPTimeout](#httptimeout)_ | false | Timeout settings for HTTP. | + + +#### TracingProvider + + + +TracingProvider defines the tracing provider configuration. + +_Appears in:_ +- [ProxyTracing](#proxytracing) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[TracingProviderType](#tracingprovidertype)_ | true | Type defines the tracing provider type. | +| `host` | _string_ | false | Host define the provider service hostname.
Deprecated: Use BackendRefs instead. | +| `port` | _integer_ | false | Port defines the port the provider service is exposed on.
Deprecated: Use BackendRefs instead. | +| `backendRefs` | _[BackendRef](#backendref) array_ | false | BackendRefs references a Kubernetes object that represents the
backend server to which the trace will be sent.
Only Service kind is supported for now. | +| `zipkin` | _[ZipkinTracingProvider](#zipkintracingprovider)_ | false | Zipkin defines the Zipkin tracing provider configuration | + + +#### TracingProviderType + +_Underlying type:_ _string_ + + + +_Appears in:_ +- [TracingProvider](#tracingprovider) + +| Value | Description | +| ----- | ----------- | +| `OpenTelemetry` | | +| `OpenTelemetry` | | +| `Zipkin` | | + + +#### TriggerEnum + +_Underlying type:_ _string_ + +TriggerEnum specifies the conditions that trigger retries. + +_Appears in:_ +- [RetryOn](#retryon) + +| Value | Description | +| ----- | ----------- | +| `5xx` | The upstream server responds with any 5xx response code, or does not respond at all (disconnect/reset/read timeout).
Includes connect-failure and refused-stream.
| +| `gateway-error` | The response is a gateway error (502,503 or 504).
| +| `reset` | The upstream server does not respond at all (disconnect/reset/read timeout.)
| +| `connect-failure` | Connection failure to the upstream server (connect timeout, etc.). (Included in *5xx*)
| +| `retriable-4xx` | The upstream server responds with a retriable 4xx response code.
Currently, the only response code in this category is 409.
| +| `refused-stream` | The upstream server resets the stream with a REFUSED_STREAM error code.
| +| `retriable-status-codes` | The upstream server responds with any response code matching one defined in the RetriableStatusCodes.
| +| `cancelled` | The gRPC status code in the response headers is “cancelled”.
| +| `deadline-exceeded` | The gRPC status code in the response headers is “deadline-exceeded”.
| +| `internal` | The gRPC status code in the response headers is “internal”.
| +| `resource-exhausted` | The gRPC status code in the response headers is “resource-exhausted”.
| +| `unavailable` | The gRPC status code in the response headers is “unavailable”.
| + + +#### UnixSocket + + + +UnixSocket describes TCP/UDP unix domain socket address, corresponding to Envoy's Pipe +https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/address.proto#config-core-v3-pipe + +_Appears in:_ +- [BackendEndpoint](#backendendpoint) +- [ExtensionService](#extensionservice) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `path` | _string_ | true | Path defines the unix domain socket path of the backend endpoint. | + + +#### Wasm + + + +Wasm defines a Wasm extension. + + +Note: at the moment, Envoy Gateway does not support configuring Wasm runtime. +v8 is used as the VM runtime for the Wasm extensions. + +_Appears in:_ +- [EnvoyExtensionPolicySpec](#envoyextensionpolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `name` | _string_ | false | Name is a unique name for this Wasm extension. It is used to identify the
Wasm extension if multiple extensions are handled by the same vm_id and root_id.
It's also used for logging/debugging.
If not specified, EG will generate a unique name for the Wasm extension. | +| `rootID` | _string_ | true | RootID is a unique ID for a set of extensions in a VM which will share a
RootContext and Contexts if applicable (e.g., an Wasm HttpFilter and an Wasm AccessLog).
If left blank, all extensions with a blank root_id with the same vm_id will share Context(s).

Note: RootID must match the root_id parameter used to register the Context in the Wasm code. | +| `code` | _[WasmCodeSource](#wasmcodesource)_ | true | Code is the Wasm code for the extension. | +| `config` | _[JSON](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#json-v1-apiextensions-k8s-io)_ | false | Config is the configuration for the Wasm extension.
This configuration will be passed as a JSON string to the Wasm extension. | +| `failOpen` | _boolean_ | false | FailOpen is a switch used to control the behavior when a fatal error occurs
during the initialization or the execution of the Wasm extension.
If FailOpen is set to true, the system bypasses the Wasm extension and
allows the traffic to pass through. Otherwise, if it is set to false or
not set (defaulting to false), the system blocks the traffic and returns
an HTTP 5xx error. | + + +#### WasmCodeSource + + + +WasmCodeSource defines the source of the Wasm code. + +_Appears in:_ +- [Wasm](#wasm) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | _[WasmCodeSourceType](#wasmcodesourcetype)_ | true | Type is the type of the source of the Wasm code.
Valid WasmCodeSourceType values are "HTTP" or "Image". | +| `http` | _[HTTPWasmCodeSource](#httpwasmcodesource)_ | false | HTTP is the HTTP URL containing the Wasm code.

Note that the HTTP server must be accessible from the Envoy proxy. | +| `image` | _[ImageWasmCodeSource](#imagewasmcodesource)_ | false | Image is the OCI image containing the Wasm code.

Note that the image must be accessible from the Envoy Gateway. | +| `pullPolicy` | _[ImagePullPolicy](#imagepullpolicy)_ | false | PullPolicy is the policy to use when pulling the Wasm module by either the HTTP or Image source.
This field is only applicable when the SHA256 field is not set.

If not specified, the default policy is IfNotPresent except for OCI images whose tag is latest.

Note: EG does not update the Wasm module every time an Envoy proxy requests
the Wasm module even if the pull policy is set to Always.
It only updates the Wasm module when the EnvoyExtension resource version changes. | + + +#### WasmCodeSourceType + +_Underlying type:_ _string_ + +WasmCodeSourceType specifies the types of sources for the Wasm code. + +_Appears in:_ +- [WasmCodeSource](#wasmcodesource) + +| Value | Description | +| ----- | ----------- | +| `HTTP` | HTTPWasmCodeSourceType allows the user to specify the Wasm code in an HTTP URL.
| +| `Image` | ImageWasmCodeSourceType allows the user to specify the Wasm code in an OCI image.
| + + +#### WithUnderscoresAction + +_Underlying type:_ _string_ + +WithUnderscoresAction configures the action to take when an HTTP header with underscores +is encountered. + +_Appears in:_ +- [HeaderSettings](#headersettings) + +| Value | Description | +| ----- | ----------- | +| `Allow` | WithUnderscoresActionAllow allows headers with underscores to be passed through.
| +| `RejectRequest` | WithUnderscoresActionRejectRequest rejects the client request. HTTP/1 requests are rejected with
the 400 status. HTTP/2 requests end with the stream reset.
| +| `DropHeader` | WithUnderscoresActionDropHeader drops the client header with name containing underscores. The header
is dropped before the filter chain is invoked and as such filters will not see
dropped headers.
| + + +#### XDSTranslatorHook + +_Underlying type:_ _string_ + +XDSTranslatorHook defines the types of hooks that an Envoy Gateway extension may support +for the xds-translator + +_Appears in:_ +- [XDSTranslatorHooks](#xdstranslatorhooks) + +| Value | Description | +| ----- | ----------- | +| `VirtualHost` | | +| `Route` | | +| `HTTPListener` | | +| `Translation` | | + + +#### XDSTranslatorHooks + + + +XDSTranslatorHooks contains all the pre and post hooks for the xds-translator runner. + +_Appears in:_ +- [ExtensionHooks](#extensionhooks) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `pre` | _[XDSTranslatorHook](#xdstranslatorhook) array_ | true | | +| `post` | _[XDSTranslatorHook](#xdstranslatorhook) array_ | true | | + + +#### XFCCCertData + +_Underlying type:_ _string_ + +XFCCCertData specifies the fields in the client certificate to be forwarded in the XFCC header. + +_Appears in:_ +- [XForwardedClientCert](#xforwardedclientcert) + +| Value | Description | +| ----- | ----------- | +| `Subject` | XFCCCertDataSubject is the Subject field of the current client certificate.
| +| `Cert` | XFCCCertDataCert is the entire client certificate in URL encoded PEM format.
| +| `Chain` | XFCCCertDataChain is the entire client certificate chain (including the leaf certificate) in URL encoded PEM format.
| +| `DNS` | XFCCCertDataDNS is the DNS type Subject Alternative Name field of the current client certificate.
| +| `URI` | XFCCCertDataURI is the URI type Subject Alternative Name field of the current client certificate.
| + + +#### XFCCForwardMode + +_Underlying type:_ _string_ + +XFCCForwardMode defines how XFCC header is handled by Envoy Proxy. + +_Appears in:_ +- [XForwardedClientCert](#xforwardedclientcert) + +| Value | Description | +| ----- | ----------- | +| `Sanitize` | XFCCForwardModeSanitize removes the XFCC header from the request. This is the default mode.
| +| `ForwardOnly` | XFCCForwardModeForwardOnly forwards the XFCC header in the request if the client connection is mTLS.
| +| `AppendForward` | XFCCForwardModeAppendForward appends the client certificate information to the request’s XFCC header and forward it if the client connection is mTLS.
| +| `SanitizeSet` | XFCCForwardModeSanitizeSet resets the XFCC header with the client certificate information and forward it if the client connection is mTLS.
The existing certificate information in the XFCC header is removed.
| +| `AlwaysForwardOnly` | XFCCForwardModeAlwaysForwardOnly always forwards the XFCC header in the request, regardless of whether the client connection is mTLS.
| + + +#### XForwardedClientCert + + + +XForwardedClientCert configures how Envoy Proxy handle the x-forwarded-client-cert (XFCC) HTTP header. + +_Appears in:_ +- [HeaderSettings](#headersettings) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `mode` | _[XFCCForwardMode](#xfccforwardmode)_ | false | Mode defines how XFCC header is handled by Envoy Proxy.
If not set, the default mode is `Sanitize`. | +| `certDetailsToAdd` | _[XFCCCertData](#xfcccertdata) array_ | false | CertDetailsToAdd specifies the fields in the client certificate to be forwarded in the XFCC header.

Hash(the SHA 256 digest of the current client certificate) and By(the Subject Alternative Name)
are always included if the client certificate is forwarded.

This field is only applicable when the mode is set to `AppendForward` or
`SanitizeSet` and the client connection is mTLS. | + + +#### XForwardedForSettings + + + +XForwardedForSettings provides configuration for using X-Forwarded-For headers for determining the client IP address. + +_Appears in:_ +- [ClientIPDetectionSettings](#clientipdetectionsettings) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `numTrustedHops` | _integer_ | false | NumTrustedHops controls the number of additional ingress proxy hops from the right side of XFF HTTP
headers to trust when determining the origin client's IP address.
Refer to https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers#x-forwarded-for
for more details. | + + +#### ZipkinTracingProvider + + + +ZipkinTracingProvider defines the Zipkin tracing provider configuration. + +_Appears in:_ +- [TracingProvider](#tracingprovider) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `enable128BitTraceId` | _boolean_ | false | Enable128BitTraceID determines whether a 128bit trace id will be used
when creating a new trace instance. If set to false, a 64bit trace
id will be used. | +| `disableSharedSpanContext` | _boolean_ | false | DisableSharedSpanContext determines whether the default Envoy behaviour of
client and server spans sharing the same span context should be disabled. | + + diff --git a/site/content/en/v1.1/boilerplates/index.md b/site/content/en/v1.1/boilerplates/index.md new file mode 100644 index 00000000000..dda80adbcbf --- /dev/null +++ b/site/content/en/v1.1/boilerplates/index.md @@ -0,0 +1,5 @@ +--- +headless: true +--- + +This file tells Hugo that the files in this directory tree shouldn't be rendered as normal pages on the site. diff --git a/site/content/en/v1.1/boilerplates/o11y_prerequisites.md b/site/content/en/v1.1/boilerplates/o11y_prerequisites.md new file mode 100644 index 00000000000..58586502f72 --- /dev/null +++ b/site/content/en/v1.1/boilerplates/o11y_prerequisites.md @@ -0,0 +1,14 @@ +--- +--- + +Follow the steps from the [Quickstart](../tasks/quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +Envoy Gateway provides an add-ons Helm Chart, which includes all the needing components for observability. +By default, the [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/) is disabled. + +Install the add-ons Helm Chart: + +```shell +helm install eg-addons oci://docker.io/envoyproxy/gateway-addons-helm --version v0.0.0-latest --set opentelemetry-collector.enabled=true -n monitoring --create-namespace +``` diff --git a/site/content/en/v1.1/concepts/_index.md b/site/content/en/v1.1/concepts/_index.md new file mode 100644 index 00000000000..4d568bd4491 --- /dev/null +++ b/site/content/en/v1.1/concepts/_index.md @@ -0,0 +1,5 @@ +--- +title: "Concepts" +weight: 1 +description: Learn about key concepts when working with Envoy Gateway +--- diff --git a/site/content/en/v1.1/concepts/concepts_overview.md b/site/content/en/v1.1/concepts/concepts_overview.md new file mode 100644 index 00000000000..31838b520f2 --- /dev/null +++ b/site/content/en/v1.1/concepts/concepts_overview.md @@ -0,0 +1,54 @@ ++++ +title = "Envoy Gateway Resources" ++++ + +There are several resources that play a part in enabling you to meet your Kubernetes ingress traffic handling needs. This page provides a brief overview of the resources you’ll be working with. + +## Overview + +![](/img/envoy-gateway-resources-overview.png) + +There are several resources that play a part in enabling you to meet your Kubernetes ingress traffic handling needs. This page provides a brief overview of the resources you’ll be working with. + +# Overview + +## Kubernetes Gateway API Resources +- **GatewayClass:** Defines a class of Gateways with common configuration. +- **Gateway:** Specifies how traffic can enter the cluster. +- **Routes:** **HTTPRoute, GRPCRoute, TLSRoute, TCPRoute, UDPRoute:** Define routing rules for different types of traffic. +## Envoy Gateway (EG) API Resources +- **EnvoyProxy:** Represents the deployment and configuration of the Envoy proxy within a Kubernetes cluster, managing its lifecycle and settings. +- **EnvoyPatchPolicy, ClientTrafficPolicy, SecurityPolicy, BackendTrafficPolicy, EnvoyExtensionPolicy, BackendTLSPolicy:** Additional policies and configurations specific to Envoy Gateway. +- **Backend:** A resource that makes routing to cluster-external backends easier and makes access to external processes via Unix Domain Sockets possible. + +| Resource | API | Required | Purpose | References | Description | +| ----------------------------------------------------------------------- | ----------- | -------- | ------------------ | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [GatewayClass][1] | Gateway API | Yes | Gateway Config | Core | Defines a class of Gateways with common configuration. | +| [Gateway][2] | Gateway API | Yes | Gateway Config | GatewayClass | Specifies how traffic can enter the cluster. | +| [HTTPRoute][3] [GRPCRoute][4] [TLSRoute][5] [TCPRoute][6] [UDPRoute][7] | Gateway API | Yes | Routing | Gateway | Define routing rules for different types of traffic. **Note:**_For simplicity these resources are referenced collectively as Route in the References column_ | +| [Backend][8] | EG API | No | Routing | N/A | Used for routing to cluster-external backends using FQDN or IP. Can also be used when you want to extend Envoy with external processes accessed via Unix Domain Sockets. | +| [ClientTrafficPolicy][9] | EG API | No | Traffic Handling | Gateway | Specifies policies for handling client traffic, including rate limiting, retries, and other client-specific configurations. | +| [BackendTrafficPolicy][10] | EG API | No | Traffic Handling | Gateway Route | Specifies policies for traffic directed towards backend services, including load balancing, health checks, and failover strategies. **Note:**_Most specific configuration wins_ | +| [SecurityPolicy][11] | EG API | No | Security | Gateway Route | Defines security-related policies such as authentication, authorization, and encryption settings for traffic handled by Envoy Gateway. **Note:**_Most specific configuration wins_ | +| [BackendTLSPolicy][12] | Gateway API | No | Security | Service | Defines TLS settings for backend connections, including certificate management, TLS version settings, and other security configurations. This policy is applied to Kubernetes Services. | +| [EnvoyProxy][13] | EG API | No | Customize & Extend | GatewayClass Gateway | The EnvoyProxy resource represents the deployment and configuration of the Envoy proxy itself within a Kubernetes cluster, managing its lifecycle and settings. **Note:**_Most specific configuration wins_ | +| [EnvoyPatchPolicy][14] | EG API | No | Customize & Extend | GatewayClass Gateway | This policy defines custom patches to be applied to Envoy Gateway resources, allowing users to tailor the configuration to their specific needs. **Note:**_Most specific configuration wins_ | +| [EnvoyExtensionPolicy][15] | EG API | No | Customize & Extend | Gateway Route, Backend | Allows for the configuration of Envoy proxy extensions, enabling custom behavior and functionality. **Note:**_Most specific configuration wins_ | + + + +[1]: https://gateway-api.sigs.k8s.io/api-types/gatewayclass/ +[2]: https://gateway-api.sigs.k8s.io/api-types/gateway/ +[3]: https://gateway-api.sigs.k8s.io/api-types/httproute/ +[4]: https://gateway-api.sigs.k8s.io/api-types/grpcroute/ +[5]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.TLSRoute +[6]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.TCPRoute +[7]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.UDPRoute +[8]: ../tasks/traffic/backend +[9]: ../api/extension_types#clienttrafficpolicy +[10]: ../api/extension_types#backendtrafficpolicy +[11]: ../api/extension_types#securitypolicy +[12]: https://gateway-api.sigs.k8s.io/api-types/backendtlspolicy/ +[13]: ../api/extension_types#envoyproxy +[14]: ../api/extension_types#envoypatchpolicy +[15]: ../api/extension_types#envoyextensionpolicy \ No newline at end of file diff --git a/site/content/en/v1.1/install/_index.md b/site/content/en/v1.1/install/_index.md new file mode 100644 index 00000000000..b4c6f79c6fd --- /dev/null +++ b/site/content/en/v1.1/install/_index.md @@ -0,0 +1,5 @@ +--- +title: "Installation" +description: This section includes installation related contents of Envoy Gateway. +weight: 70 +--- diff --git a/site/content/en/v1.1/install/custom-cert.md b/site/content/en/v1.1/install/custom-cert.md new file mode 100644 index 00000000000..dd059c03520 --- /dev/null +++ b/site/content/en/v1.1/install/custom-cert.md @@ -0,0 +1,146 @@ +--- +title: Control Plane Authentication using custom certs +weight: -70 +--- + +Envoy Gateway establishes a secure TLS connection for control plane communication between Envoy Gateway pods and the Envoy Proxy fleet. The TLS Certificates used here are self signed and generated using a job that runs before envoy gateway is created, and these certs and mounted on to the envoy gateway and envoy proxy pods. + +This task will walk you through configuring custom certs for control plane auth. + +## Before you begin + +We use Cert-Manager to manage the certificates. You can install it by following the [official guide](https://cert-manager.io/docs/installation/kubernetes/). + +## Configure custom certs for control plane + +1. First you need to set up the CA issuer, in this task, we use the `selfsigned-issuer` as an example. + + *You should not use the self-signed issuer in production, you should use a real CA issuer.* + + ```shell + cat < + +## Maintainers + +| Name | Email | Url | +| ---- | ------ | --- | +| envoy-gateway-steering-committee | | | +| envoy-gateway-maintainers | | | + +## Source Code + +* + +## Requirements + +| Repository | Name | Version | +|------------|------|---------| +| https://fluent.github.io/helm-charts | fluent-bit | 0.30.4 | +| https://grafana.github.io/helm-charts | grafana | 8.0.0 | +| https://grafana.github.io/helm-charts | loki | 4.8.0 | +| https://grafana.github.io/helm-charts | tempo | 1.3.1 | +| https://open-telemetry.github.io/opentelemetry-helm-charts | opentelemetry-collector | 0.73.1 | +| https://prometheus-community.github.io/helm-charts | prometheus | 25.21.0 | + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| fluent-bit.config.filters | string | `"[FILTER]\n Name kubernetes\n Match kube.*\n Merge_Log On\n Keep_Log Off\n K8S-Logging.Parser On\n K8S-Logging.Exclude On\n\n[FILTER]\n Name grep\n Match kube.*\n Regex $kubernetes['container_name'] ^envoy$\n\n[FILTER]\n Name parser\n Match kube.*\n Key_Name log\n Parser envoy\n Reserve_Data True\n"` | | +| fluent-bit.config.inputs | string | `"[INPUT]\n Name tail\n Path /var/log/containers/*.log\n multiline.parser docker, cri\n Tag kube.*\n Mem_Buf_Limit 5MB\n Skip_Long_Lines On\n"` | | +| fluent-bit.config.outputs | string | `"[OUTPUT]\n Name loki\n Match kube.*\n Host loki.monitoring.svc.cluster.local\n Port 3100\n Labels job=fluentbit, app=$kubernetes['labels']['app'], k8s_namespace_name=$kubernetes['namespace_name'], k8s_pod_name=$kubernetes['pod_name'], k8s_container_name=$kubernetes['container_name']\n"` | | +| fluent-bit.config.service | string | `"[SERVICE]\n Daemon Off\n Flush {{ .Values.flush }}\n Log_Level {{ .Values.logLevel }}\n Parsers_File parsers.conf\n Parsers_File custom_parsers.conf\n HTTP_Server On\n HTTP_Listen 0.0.0.0\n HTTP_Port {{ .Values.metricsPort }}\n Health_Check On\n"` | | +| fluent-bit.enabled | bool | `true` | | +| fluent-bit.fullnameOverride | string | `"fluent-bit"` | | +| fluent-bit.image.repository | string | `"fluent/fluent-bit"` | | +| fluent-bit.podAnnotations."fluentbit.io/exclude" | string | `"true"` | | +| fluent-bit.podAnnotations."prometheus.io/path" | string | `"/api/v1/metrics/prometheus"` | | +| fluent-bit.podAnnotations."prometheus.io/port" | string | `"2020"` | | +| fluent-bit.podAnnotations."prometheus.io/scrape" | string | `"true"` | | +| fluent-bit.testFramework.enabled | bool | `false` | | +| grafana.adminPassword | string | `"admin"` | | +| grafana.dashboardProviders."dashboardproviders.yaml".apiVersion | int | `1` | | +| grafana.dashboardProviders."dashboardproviders.yaml".providers[0].disableDeletion | bool | `false` | | +| grafana.dashboardProviders."dashboardproviders.yaml".providers[0].editable | bool | `true` | | +| grafana.dashboardProviders."dashboardproviders.yaml".providers[0].folder | string | `"envoy-gateway"` | | +| grafana.dashboardProviders."dashboardproviders.yaml".providers[0].name | string | `"envoy-gateway"` | | +| grafana.dashboardProviders."dashboardproviders.yaml".providers[0].options.path | string | `"/var/lib/grafana/dashboards/envoy-gateway"` | | +| grafana.dashboardProviders."dashboardproviders.yaml".providers[0].orgId | int | `1` | | +| grafana.dashboardProviders."dashboardproviders.yaml".providers[0].type | string | `"file"` | | +| grafana.dashboardsConfigMaps.envoy-gateway | string | `"grafana-dashboards"` | | +| grafana.datasources."datasources.yaml".apiVersion | int | `1` | | +| grafana.datasources."datasources.yaml".datasources[0].name | string | `"Prometheus"` | | +| grafana.datasources."datasources.yaml".datasources[0].type | string | `"prometheus"` | | +| grafana.datasources."datasources.yaml".datasources[0].url | string | `"http://prometheus"` | | +| grafana.enabled | bool | `true` | | +| grafana.fullnameOverride | string | `"grafana"` | | +| grafana.service.type | string | `"LoadBalancer"` | | +| loki.backend.replicas | int | `0` | | +| loki.deploymentMode | string | `"SingleBinary"` | | +| loki.enabled | bool | `true` | | +| loki.fullnameOverride | string | `"loki"` | | +| loki.gateway.enabled | bool | `false` | | +| loki.loki.auth_enabled | bool | `false` | | +| loki.loki.commonConfig.replication_factor | int | `1` | | +| loki.loki.compactorAddress | string | `"loki"` | | +| loki.loki.memberlist | string | `"loki-memberlist"` | | +| loki.loki.rulerConfig.storage.type | string | `"local"` | | +| loki.loki.storage.type | string | `"filesystem"` | | +| loki.monitoring.lokiCanary.enabled | bool | `false` | | +| loki.monitoring.selfMonitoring.enabled | bool | `false` | | +| loki.monitoring.selfMonitoring.grafanaAgent.installOperator | bool | `false` | | +| loki.read.replicas | int | `0` | | +| loki.singleBinary.replicas | int | `1` | | +| loki.test.enabled | bool | `false` | | +| loki.write.replicas | int | `0` | | +| opentelemetry-collector.config.exporters.logging.verbosity | string | `"detailed"` | | +| opentelemetry-collector.config.exporters.loki.endpoint | string | `"http://loki.monitoring.svc:3100/loki/api/v1/push"` | | +| opentelemetry-collector.config.exporters.otlp.endpoint | string | `"tempo.monitoring.svc:4317"` | | +| opentelemetry-collector.config.exporters.otlp.tls.insecure | bool | `true` | | +| opentelemetry-collector.config.exporters.prometheus.endpoint | string | `"0.0.0.0:19001"` | | +| opentelemetry-collector.config.extensions.health_check | object | `{}` | | +| opentelemetry-collector.config.processors.attributes.actions[0].action | string | `"insert"` | | +| opentelemetry-collector.config.processors.attributes.actions[0].key | string | `"loki.attribute.labels"` | | +| opentelemetry-collector.config.processors.attributes.actions[0].value | string | `"k8s.pod.name, k8s.namespace.name"` | | +| opentelemetry-collector.config.receivers.otlp.protocols.grpc.endpoint | string | `"${env:MY_POD_IP}:4317"` | | +| opentelemetry-collector.config.receivers.otlp.protocols.http.endpoint | string | `"${env:MY_POD_IP}:4318"` | | +| opentelemetry-collector.config.receivers.zipkin.endpoint | string | `"${env:MY_POD_IP}:9411"` | | +| opentelemetry-collector.config.service.extensions[0] | string | `"health_check"` | | +| opentelemetry-collector.config.service.pipelines.logs.exporters[0] | string | `"loki"` | | +| opentelemetry-collector.config.service.pipelines.logs.processors[0] | string | `"attributes"` | | +| opentelemetry-collector.config.service.pipelines.logs.receivers[0] | string | `"otlp"` | | +| opentelemetry-collector.config.service.pipelines.metrics.exporters[0] | string | `"prometheus"` | | +| opentelemetry-collector.config.service.pipelines.metrics.receivers[0] | string | `"otlp"` | | +| opentelemetry-collector.config.service.pipelines.traces.exporters[0] | string | `"otlp"` | | +| opentelemetry-collector.config.service.pipelines.traces.receivers[0] | string | `"otlp"` | | +| opentelemetry-collector.config.service.pipelines.traces.receivers[1] | string | `"zipkin"` | | +| opentelemetry-collector.enabled | bool | `false` | | +| opentelemetry-collector.fullnameOverride | string | `"otel-collector"` | | +| opentelemetry-collector.mode | string | `"deployment"` | | +| prometheus.alertmanager.enabled | bool | `false` | | +| prometheus.enabled | bool | `true` | | +| prometheus.kube-state-metrics.enabled | bool | `false` | | +| prometheus.prometheus-node-exporter.enabled | bool | `false` | | +| prometheus.prometheus-pushgateway.enabled | bool | `false` | | +| prometheus.server.fullnameOverride | string | `"prometheus"` | | +| prometheus.server.global.scrape_interval | string | `"15s"` | | +| prometheus.server.image.repository | string | `"prom/prometheus"` | | +| prometheus.server.persistentVolume.enabled | bool | `false` | | +| prometheus.server.readinessProbeInitialDelay | int | `0` | | +| prometheus.server.securityContext | object | `{}` | | +| prometheus.server.service.type | string | `"LoadBalancer"` | | +| tempo.enabled | bool | `true` | | +| tempo.fullnameOverride | string | `"tempo"` | | +| tempo.service.type | string | `"LoadBalancer"` | | + diff --git a/site/content/en/v1.1/install/gateway-helm-api.md b/site/content/en/v1.1/install/gateway-helm-api.md new file mode 100644 index 00000000000..9f2046a537f --- /dev/null +++ b/site/content/en/v1.1/install/gateway-helm-api.md @@ -0,0 +1,66 @@ ++++ +title = "Gateway Helm Chart" ++++ + +![Version: v0.0.0-latest](https://img.shields.io/badge/Version-v0.0.0--latest-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: latest](https://img.shields.io/badge/AppVersion-latest-informational?style=flat-square) + +The Helm chart for Envoy Gateway + +**Homepage:** + +## Maintainers + +| Name | Email | Url | +| ---- | ------ | --- | +| envoy-gateway-steering-committee | | | +| envoy-gateway-maintainers | | | + +## Source Code + +* + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| certgen | object | `{"job":{"annotations":{},"resources":{},"ttlSecondsAfterFinished":30},"rbac":{"annotations":{},"labels":{}}}` | Certgen is used to generate the certificates required by EnvoyGateway. If you want to construct a custom certificate, you can generate a custom certificate through Cert-Manager before installing EnvoyGateway. Certgen will not overwrite the custom certificate. Please do not manually modify `values.yaml` to disable certgen, it may cause EnvoyGateway OIDC,OAuth2,etc. to not work as expected. | +| config.envoyGateway.gateway.controllerName | string | `"gateway.envoyproxy.io/gatewayclass-controller"` | | +| config.envoyGateway.logging.level.default | string | `"info"` | | +| config.envoyGateway.provider.type | string | `"Kubernetes"` | | +| createNamespace | bool | `false` | | +| deployment.envoyGateway.image.repository | string | `""` | | +| deployment.envoyGateway.image.tag | string | `""` | | +| deployment.envoyGateway.imagePullPolicy | string | `""` | | +| deployment.envoyGateway.imagePullSecrets | list | `[]` | | +| deployment.envoyGateway.resources.limits.cpu | string | `"500m"` | | +| deployment.envoyGateway.resources.limits.memory | string | `"1024Mi"` | | +| deployment.envoyGateway.resources.requests.cpu | string | `"100m"` | | +| deployment.envoyGateway.resources.requests.memory | string | `"256Mi"` | | +| deployment.pod.affinity | object | `{}` | | +| deployment.pod.annotations."prometheus.io/port" | string | `"19001"` | | +| deployment.pod.annotations."prometheus.io/scrape" | string | `"true"` | | +| deployment.pod.labels | object | `{}` | | +| deployment.pod.tolerations | list | `[]` | | +| deployment.pod.topologySpreadConstraints | list | `[]` | | +| deployment.ports[0].name | string | `"grpc"` | | +| deployment.ports[0].port | int | `18000` | | +| deployment.ports[0].targetPort | int | `18000` | | +| deployment.ports[1].name | string | `"ratelimit"` | | +| deployment.ports[1].port | int | `18001` | | +| deployment.ports[1].targetPort | int | `18001` | | +| deployment.ports[2].name | string | `"wasm"` | | +| deployment.ports[2].port | int | `18002` | | +| deployment.ports[2].targetPort | int | `18002` | | +| deployment.ports[3].name | string | `"metrics"` | | +| deployment.ports[3].port | int | `19001` | | +| deployment.ports[3].targetPort | int | `19001` | | +| deployment.replicas | int | `1` | | +| global.images.envoyGateway.image | string | `nil` | | +| global.images.envoyGateway.pullPolicy | string | `nil` | | +| global.images.envoyGateway.pullSecrets | list | `[]` | | +| global.images.ratelimit.image | string | `"docker.io/envoyproxy/ratelimit:master"` | | +| global.images.ratelimit.pullPolicy | string | `"IfNotPresent"` | | +| global.images.ratelimit.pullSecrets | list | `[]` | | +| kubernetesClusterDomain | string | `"cluster.local"` | | +| podDisruptionBudget.minAvailable | int | `0` | | + diff --git a/site/content/en/v1.1/install/install-egctl.md b/site/content/en/v1.1/install/install-egctl.md new file mode 100644 index 00000000000..cbd82385740 --- /dev/null +++ b/site/content/en/v1.1/install/install-egctl.md @@ -0,0 +1,72 @@ +--- +title: "Install egctl" +weight: -80 +--- + +{{% alert title="What is egctl?" color="primary" %}} + +`egctl` is a command line tool to provide additional functionality for Envoy Gateway users. + +{{% /alert %}} + + +This task shows how to install the egctl CLI. egctl can be installed either from source, or from pre-built binary releases. + +### From The Envoy Gateway Project + +The Envoy Gateway project provides two ways to fetch and install egctl. These are the official methods to get egctl releases. Installation through those methods can be found below the official methods. + +{{< tabpane text=true >}} +{{% tab header="From the Binary Releases" %}} + +Every [release](https://github.com/envoyproxy/gateway/releases) of egctl provides binary releases for a variety of OSes. These binary versions can be manually downloaded and installed. + +1. Download your [desired version](https://github.com/envoyproxy/gateway/releases) +2. Unpack it (tar -zxvf egctl_latest_linux_amd64.tar.gz) +3. Find the egctl binary in the unpacked directory, and move it to its desired destination (mv bin/linux/amd64/egctl /usr/local/bin/egctl) + +From there, you should be able to run: `egctl help`. + +{{% /tab %}} +{{% tab header="From Script" %}} + +`egctl` now has an installer script that will automatically grab the latest release version of egctl and install it locally. + +You can fetch that script, and then execute it locally. It's well documented so that you can read through it and understand what it is doing before you run it. + +```shell +curl -fsSL -o get-egctl.sh https://gateway.envoyproxy.io/get-egctl.sh + +chmod +x get-egctl.sh + +# get help info of the +bash get-egctl.sh --help + +# install the latest development version of egctl +bash VERSION=latest get-egctl.sh +``` + +Yes, you can just use the below command if you want to live on the edge. + +```shell +curl -fsSL https://gateway.envoyproxy.io/get-egctl.sh | VERSION=latest bash +``` + +{{% /tab %}} + +{{% tab header="From Homebrew" %}} + +You can also install egctl using homebrew: + +```shell +brew install egctl +``` + +{{% /tab %}} +{{< /tabpane >}} + +{{% alert title="Next Steps" color="warning" %}} + +You can refer to the [Use egctl task](../tasks/operations/egctl) for more details about egctl. + +{{% /alert %}} diff --git a/site/content/en/v1.1/install/install-helm.md b/site/content/en/v1.1/install/install-helm.md new file mode 100644 index 00000000000..b3468f642f9 --- /dev/null +++ b/site/content/en/v1.1/install/install-helm.md @@ -0,0 +1,144 @@ ++++ +title = "Install with Helm" +weight = -100 ++++ + +[Helm](https://helm.sh) is a package manager for Kubernetes that automates the release and management of software on Kubernetes. + +Envoy Gateway can be installed via a Helm chart with a few simple steps, depending on if you are deploying for the first time, upgrading Envoy Gateway from an existing installation, or migrating from Envoy Gateway. + +## Before you begin + +{{% alert title="Compatibility Matrix" color="warning" %}} +Refer to the [Version Compatibility Matrix](/news/releases/matrix) to learn more. +{{% /alert %}} + +The Envoy Gateway Helm chart is hosted by DockerHub. + +It is published at `oci://docker.io/envoyproxy/gateway-helm`. + +{{% alert title="Note" color="primary" %}} +We use `v0.0.0-latest` as the latest development version. + +You can visit [Envoy Gateway Helm Chart](https://hub.docker.com/r/envoyproxy/gateway-helm/tags) for more releases. +{{% /alert %}} + +## Install with Helm + +Envoy Gateway is typically deployed to Kubernetes from the command line. If you don't have Kubernetes, you should use `kind` to create one. + +{{% alert title="Developer Guide" color="primary" %}} +Refer to the [Developer Guide](../../contributions/develop) to learn more. +{{% /alert %}} + +Install the Gateway API CRDs and Envoy Gateway: + +```shell +helm install eg oci://docker.io/envoyproxy/gateway-helm --version v0.0.0-latest -n envoy-gateway-system --create-namespace +``` + +Wait for Envoy Gateway to become available: + +```shell +kubectl wait --timeout=5m -n envoy-gateway-system deployment/envoy-gateway --for=condition=Available +``` + +Install the GatewayClass, Gateway, HTTPRoute and example app: + +```shell +kubectl apply -f https://github.com/envoyproxy/gateway/releases/download/latest/quickstart.yaml -n default +``` + +**Note**: [`quickstart.yaml`] defines that Envoy Gateway will listen for +traffic on port 80 on its globally-routable IP address, to make it easy to use +browsers to test Envoy Gateway. When Envoy Gateway sees that its Listener is +using a privileged port (<1024), it will map this internally to an +unprivileged port, so that Envoy Gateway doesn't need additional privileges. +It's important to be aware of this mapping, since you may need to take it into +consideration when debugging. + +[`quickstart.yaml`]: https://github.com/envoyproxy/gateway/releases/download/latest/quickstart.yaml + +## Helm chart customizations + +Some of the quick ways of using the helm install command for envoy gateway installation are below. + +{{% alert title="Helm Chart Values" color="primary" %}} +If you want to know all the available fields inside the values.yaml file, please see the [Helm Chart Values](./gateway-helm-api). +{{% /alert %}} + +### Increase the replicas + +```shell +helm install eg oci://docker.io/envoyproxy/gateway-helm --version v0.0.0-latest -n envoy-gateway-system --create-namespace --set deployment.replicas=2 +``` + +### Change the kubernetesClusterDomain name + +If you have installed your cluster with different domain name you can use below command. + +```shell +helm install eg oci://docker.io/envoyproxy/gateway-helm --version v0.0.0-latest -n envoy-gateway-system --create-namespace --set kubernetesClusterDomain= +``` + +**Note**: Above are some of the ways we can directly use for customization of our installation. But if you are looking for more complex changes [values.yaml](https://helm.sh/docs/chart_template_guide/values_files/) comes to rescue. + +### Using values.yaml file for complex installation + +```yaml +deployment: + envoyGateway: + resources: + limits: + cpu: 700m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + ports: + - name: grpc + port: 18005 + targetPort: 18000 + - name: ratelimit + port: 18006 + targetPort: 18001 + +config: + envoyGateway: + logging: + level: + default: debug +``` + +Here we have made three changes to our values.yaml file. Increase the resources limit for cpu to `700m`, changed the port for grpc to `18005` and for ratelimit to `18006` and also updated the logging level to `debug`. + +You can use the below command to install the envoy gateway using values.yaml file. + +```shell +helm install eg oci://docker.io/envoyproxy/gateway-helm --version v0.0.0-latest -n envoy-gateway-system --create-namespace -f values.yaml +``` + +## Open Ports + +These are the ports used by Envoy Gateway and the managed Envoy Proxy. + +### Envoy Gateway + +| Envoy Gateway | Address | Port | Configurable | +|:----------------------:|:---------:|:------:| :------: | +| Xds EnvoyProxy Server | 0.0.0.0 | 18000 | No | +| Xds RateLimit Server | 0.0.0.0 | 18001 | No | +| Admin Server | 127.0.0.1 | 19000 | Yes | +| Metrics Server | 0.0.0.0 | 19001 | No | +| Health Check | 127.0.0.1 | 8081 | No | + +### EnvoyProxy + +| Envoy Proxy | Address | Port | +|:---------------------------------:|:-----------:| :-----: | +| Admin Server | 127.0.0.1 | 19000 | +| Heath Check | 0.0.0.0 | 19001 | + +{{% alert title="Next Steps" color="warning" %}} +Envoy Gateway should now be successfully installed and running. To experience more abilities of Envoy Gateway, refer to [Tasks](../tasks). +{{% /alert %}} diff --git a/site/content/en/v1.1/install/install-yaml.md b/site/content/en/v1.1/install/install-yaml.md new file mode 100644 index 00000000000..c2a4da45fcd --- /dev/null +++ b/site/content/en/v1.1/install/install-yaml.md @@ -0,0 +1,39 @@ ++++ +title = "Install with Kubernetes YAML" +weight = -99 ++++ + +This task walks you through installing Envoy Gateway in your Kubernetes cluster. + +The manual install process does not allow for as much control over configuration +as the [Helm install method](./install-helm), so if you need more control over your Envoy Gateway +installation, it is recommended that you use helm. + +## Before you begin + +Envoy Gateway is designed to run in Kubernetes for production. The most essential requirements are: + +* Kubernetes 1.25 or later +* The `kubectl` command-line tool + +{{% alert title="Compatibility Matrix" color="warning" %}} +Refer to the [Version Compatibility Matrix](/news/releases/matrix) to learn more. +{{% /alert %}} + +## Install with YAML + +Envoy Gateway is typically deployed to Kubernetes from the command line. If you don't have Kubernetes, you should use `kind` to create one. + +{{% alert title="Developer Guide" color="primary" %}} +Refer to the [Developer Guide](../../contributions/develop) to learn more. +{{% /alert %}} + +1. In your terminal, run the following command: + + ```shell + kubectl apply --server-side -f https://github.com/envoyproxy/gateway/releases/download/latest/install.yaml + ``` + +2. Next Steps + + Envoy Gateway should now be successfully installed and running, but in order to experience more abilities of Envoy Gateway, you can refer to [Tasks](/latest/tasks). diff --git a/site/content/en/v1.1/tasks/_index.md b/site/content/en/v1.1/tasks/_index.md new file mode 100644 index 00000000000..49e8595328b --- /dev/null +++ b/site/content/en/v1.1/tasks/_index.md @@ -0,0 +1,5 @@ +--- +title: "Tasks" +weight: 2 +description: Learn Envoy Gateway hands-on through tasks +--- diff --git a/site/content/en/v1.1/tasks/extensibility/_index.md b/site/content/en/v1.1/tasks/extensibility/_index.md new file mode 100644 index 00000000000..664c734aeca --- /dev/null +++ b/site/content/en/v1.1/tasks/extensibility/_index.md @@ -0,0 +1,5 @@ +--- +title: "Extensibility" +weight: 4 +description: This section includes Extensibility tasks. +--- diff --git a/site/content/en/v1.1/tasks/extensibility/build-wasm-image.md b/site/content/en/v1.1/tasks/extensibility/build-wasm-image.md new file mode 100644 index 00000000000..dfe983dd193 --- /dev/null +++ b/site/content/en/v1.1/tasks/extensibility/build-wasm-image.md @@ -0,0 +1,71 @@ +--- +title: "Build a Wasm image" +--- + +Envoy Gateway supports two types of Wasm extensions within the [EnvoyExtensionPolicy][] API: HTTP Wasm Extensions and Image Wasm Extensions. +Packaging a Wasm extension as an OCI image is beneficial because it simplifies versioning and distribution for users. +Additionally, users can leverage existing image toolchain to build and manage Wasm images. + +This document describes how to build OCI images which are consumable by Envoy Gateway. + +## Wasm Image Formats + +There are two types of images that are supported by Envoy Gateway. One is in the Docker format, and another is the standard +OCI specification compliant format. Please note that both of them are supported by any OCI registries. You can choose +either format depending on your preference, and both types of images are consumable by Envoy Gateway [EnvoyExtensionPolicy][] API. + +## Build Wasm Docker image + +We assume that you have a valid Wasm binary named `plugin.wasm`. Then you can build a Wasm Docker image with the Docker CLI. + +1. First, we prepare the following Dockerfile: + +``` +$ cat Dockerfile +FROM scratch + +COPY plugin.wasm ./ +``` + +**Note: you must have exactly one `COPY` instruction in the Dockerfile in order to end up having only one layer in produced images.** + +2. Then, build your image via `docker build` command + +``` +$ docker build . -t my-registry/mywasm:0.1.0 +``` + +3. Finally, push the image to your registry via `docker push` command + +``` +$ docker push my-registry/mywasm:0.1.0 +``` + +## Build Wasm OCI image + +We assume that you have a valid Wasm binary named `plugin.wasm`, and you have [buildah](https://buildah.io/) installed on your machine. +Then you can build a Wasm OCI image with the `buildah` CLI. + +1. First, we create a working container from `scratch` base image with `buildah from` command. + +``` +$ buildah --name mywasm from scratch +mywasm +``` + +2. Then copy the Wasm binary into that base image by `buildah copy` command to create the layer. + +``` +$ buildah copy mywasm plugin.wasm ./ +af82a227630327c24026d7c6d3057c3d5478b14426b74c547df011ca5f23d271 +``` + +**Note: you must execute `buildah copy` exactly once in order to end up having only one layer in produced images** + +4. Now, you can build an OCI image and push it to your registry via `buildah commit` command + +``` +$ buildah commit mywasm docker://my-remote-registry/mywasm:0.1.0 +``` + +[EnvoyExtensionPolicy]: ../../../api/extension_types#envoyextensionpolicy diff --git a/site/content/en/v1.1/tasks/extensibility/envoy-patch-policy.md b/site/content/en/v1.1/tasks/extensibility/envoy-patch-policy.md new file mode 100644 index 00000000000..ff819754d1f --- /dev/null +++ b/site/content/en/v1.1/tasks/extensibility/envoy-patch-policy.md @@ -0,0 +1,359 @@ +--- +title: "Envoy Patch Policy" +--- + +This task explains the usage of the [EnvoyPatchPolicy][] API. +__Note:__ This API is meant for users extremely familiar with Envoy [xDS][] semantics. +Also before considering this API for production use cases, please be aware that this API +is unstable and the outcome may change across versions. Use at your own risk. + +## Introduction + +The [EnvoyPatchPolicy][] API allows user to modify the output [xDS][] +configuration generated by Envoy Gateway intended for EnvoyProxy, +using [JSON Patch][] semantics. + +## Motivation + +This API was introduced to allow advanced users to be able to leverage Envoy Proxy functionality +not exposed by Envoy Gateway APIs today. + +## Quickstart + +### Prerequisites + +* Follow the steps from the [Quickstart](../../quickstart) task to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +### Enable EnvoyPatchPolicy + +* By default [EnvoyPatchPolicy][] is disabled. Lets enable it in the [EnvoyGateway][] startup configuration + +* The default installation of Envoy Gateway installs a default [EnvoyGateway][] configuration and attaches it +using a `ConfigMap`. In the next step, we will update this resource to enable EnvoyPatchPolicy. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +* After updating the `ConfigMap`, you will need to restart the `envoy-gateway` deployment so the configuration kicks in + +```shell +kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system +``` + +## Testing + +### Customize Response + +* Use EnvoyProxy's [Local Reply Modification][] feature to return a custom response back to the client when +the status code is `404` + +* Apply the configuration + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <// + name: default/eg/http + operation: + op: add + path: "/default_filter_chain/filters/0/typed_config/local_reply_config" + value: + mappers: + - filter: + status_code_filter: + comparison: + op: EQ + value: + default_value: 404 + runtime_key: key_b + status_code: 406 + body: + inline_string: "could not find what you are looking for" +EOF +``` + +{{% /tab %}} +{{% tab header="Apply from file" %}} +Save and apply the following resource to your cluster: + +```yaml +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyPatchPolicy +metadata: + name: custom-response-patch-policy + namespace: default +spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: eg + namespace: default + type: JSONPatch + jsonPatches: + - type: "type.googleapis.com/envoy.config.listener.v3.Listener" + # The listener name is of the form // + name: default/eg/http + operation: + op: add + path: "/default_filter_chain/filters/0/typed_config/local_reply_config" + value: + mappers: + - filter: + status_code_filter: + comparison: + op: EQ + value: + default_value: 404 + runtime_key: key_b + status_code: 406 + body: + inline_string: "could not find what you are looking for" +``` + +{{% /tab %}} +{{< /tabpane >}} + +When mergeGateways is enabled, there will be one Envoy deployment for all Gateways in the cluster. +Then the EnvoyPatchPolicy should target a specific GatewayClass. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <// + name: default/eg/http + operation: + op: add + path: "/default_filter_chain/filters/0/typed_config/local_reply_config" + value: + mappers: + - filter: + status_code_filter: + comparison: + op: EQ + value: + default_value: 404 + runtime_key: key_b + status_code: 406 + body: + inline_string: "could not find what you are looking for" +EOF +``` + +{{% /tab %}} +{{% tab header="Apply from file" %}} +Save and apply the following resource to your cluster: + +```yaml +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyPatchPolicy +metadata: + name: custom-response-patch-policy + namespace: default +spec: + targetRef: + group: gateway.networking.k8s.io + kind: GatewayClass + name: eg + namespace: default + type: JSONPatch + jsonPatches: + - type: "type.googleapis.com/envoy.config.listener.v3.Listener" + # The listener name is of the form // + name: default/eg/http + operation: + op: add + path: "/default_filter_chain/filters/0/typed_config/local_reply_config" + value: + mappers: + - filter: + status_code_filter: + comparison: + op: EQ + value: + default_value: 404 + runtime_key: key_b + status_code: 406 + body: + inline_string: "could not find what you are looking for" +``` + +{{% /tab %}} +{{< /tabpane >}} + +* Edit the HTTPRoute resource from the Quickstart to only match on paths with value `/get` + +```shell +kubectl patch httproute backend --type=json --patch ' + - op: add + path: /spec/rules/0/matches/0/path/value + value: /get + ' +``` + +* Test it out by specifying a path apart from `/get` + +``` +$ curl --header "Host: www.example.com" http://localhost:8888/find +Handling connection for 8888 +could not find what you are looking for +``` + +## Debugging + +### Runtime + +* The `Status` subresource should have information about the status of the resource. Make sure +`Accepted=True` and `Programmed=True` conditions are set to ensure that the policy has been +applied to Envoy Proxy. + +```yaml +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyPatchPolicy +metadata: + annotations: + kubectl.kubernetes.io/last-applied-configuration: | + {"apiVersion":"gateway.envoyproxy.io/v1alpha1","kind":"EnvoyPatchPolicy","metadata":{"annotations":{},"name":"custom-response-patch-policy","namespace":"default"},"spec":{"jsonPatches":[{"name":"default/eg/http","operation":{"op":"add","path":"/default_filter_chain/filters/0/typed_config/local_reply_config","value":{"mappers":[{"body":{"inline_string":"could not find what you are looking for"},"filter":{"status_code_filter":{"comparison":{"op":"EQ","value":{"default_value":404}}}}}]}},"type":"type.googleapis.com/envoy.config.listener.v3.Listener"}],"priority":0,"targetRef":{"group":"gateway.networking.k8s.io","kind":"Gateway","name":"eg","namespace":"default"},"type":"JSONPatch"}} + creationTimestamp: "2023-07-31T21:47:53Z" + generation: 1 + name: custom-response-patch-policy + namespace: default + resourceVersion: "10265" + uid: a35bda6e-a0cc-46d7-a63a-cee765174bc3 +spec: + jsonPatches: + - name: default/eg/http + operation: + op: add + path: /default_filter_chain/filters/0/typed_config/local_reply_config + value: + mappers: + - body: + inline_string: could not find what you are looking for + filter: + status_code_filter: + comparison: + op: EQ + value: + default_value: 404 + type: type.googleapis.com/envoy.config.listener.v3.Listener + priority: 0 + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: eg + namespace: default + type: JSONPatch +status: + conditions: + - lastTransitionTime: "2023-07-31T21:48:19Z" + message: EnvoyPatchPolicy has been accepted. + observedGeneration: 1 + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: "2023-07-31T21:48:19Z" + message: successfully applied patches. + reason: Programmed + status: "True" + type: Programmed +``` + +### Offline + +* You can use [egctl x translate][] to validate the translated xds output. + +## Caveats + +This API will always be an unstable API and the same outcome cannot be garunteed +across versions for these reasons +* The Envoy Proxy API might deprecate and remove API fields +* Envoy Gateway might alter the xDS translation creating a different xDS output +such as changing the `name` field of resources. + +[EnvoyPatchPolicy]: ../../../api/extension_types#envoypatchpolicy +[EnvoyGateway]: ../../../api/extension_types#envoygateway +[JSON Patch]: https://datatracker.ietf.org/doc/html/rfc6902 +[xDS]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/operations/dynamic_configuration +[Local Reply Modification]: https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/local_reply +[egctl x translate]: ../operations/egctl#egctl-experimental-translate diff --git a/site/content/en/v1.1/tasks/extensibility/ext-proc.md b/site/content/en/v1.1/tasks/extensibility/ext-proc.md new file mode 100644 index 00000000000..9028447ab09 --- /dev/null +++ b/site/content/en/v1.1/tasks/extensibility/ext-proc.md @@ -0,0 +1,290 @@ +--- +title: "External Processing" +--- + +This task provides instructions for configuring external processing. + +External processing calls an external gRPC service to process HTTP requests and responses. +The external processing service can inspect and mutate requests and responses. + +Envoy Gateway introduces a new CRD called [EnvoyExtensionPolicy][] that allows the user to configure external processing. +This instantiated resource can be linked to a [Gateway][Gateway] and [HTTPRoute][HTTPRoute] resource. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +Verify the Gateway status: + +```shell +kubectl get gateway/eg -o yaml +``` + +## GRPC External Processing Service + +### Installation + +Install a demo GRPC service that will be used as the external processing service: + +```shell +kubectl apply -f https://raw.githubusercontent.com/envoyproxy/gateway/latest/examples/kubernetes/ext-proc-grpc-service.yaml +``` + +Create a new HTTPRoute resource to route traffic on the path `/myapp` to the backend service. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the HTTPRoute status: + +```shell +kubectl get httproute/myapp -o yaml +``` + +### Configuration + +Create a new EnvoyExtensionPolicy resource to configure the external processing service. This EnvoyExtensionPolicy targets the HTTPRoute +"myApp" created in the previous step. It calls the GRPC external processing service "grpc-ext-proc" on port 9002 for +processing. + +By default, requests and responses are not sent to the external processor. The `processingMode` struct is used to define what should be sent to the external processor. +In this example, we configure the following processing modes: +* The empty `request` field configures envoy to send request headers to the external processor. +* The `response` field includes configuration for body processing. As a result, response headers are sent to the external processor. Additionally, the response body is streamed to the external processor. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the Envoy Extension Policy configuration: + +```shell +kubectl get envoyextensionpolicy/ext-proc-example -o yaml +``` + + +Because the gRPC external processing service is enabled with TLS, a [BackendTLSPolicy][] needs to be created to configure +the communication between the Envoy proxy and the gRPC auth service. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the BackendTLSPolicy configuration: + +```shell +kubectl get backendtlspolicy/grpc-ext-proc-btls -o yaml +``` + +### Testing + +Ensure the `GATEWAY_HOST` environment variable from the [Quickstart](../../quickstart) is set. If not, follow the +Quickstart instructions to set the variable. + +```shell +echo $GATEWAY_HOST +``` + +Send a request to the backend service without `Authentication` header: + +```shell +curl -v -H "Host: www.example.com" "http://${GATEWAY_HOST}/myapp" +``` + +You should see that the external processor added headers: +- `x-request-ext-processed` - this header was added before the request was forwarded to the backend +- `x-response-ext-processed`- this header was added before the response was returned to the client + + +``` +curl -v -H "Host: www.example.com" http://localhost:10080/myapp +[...] +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< date: Fri, 14 Jun 2024 19:30:40 GMT +< content-length: 502 +< x-response-ext-processed: true +< +{ + "path": "/myapp", + "host": "www.example.com", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { +[...] + "X-Request-Ext-Processed": [ + "true" + ], +[...] + } +``` + +## Clean-Up + +Follow the steps from the [Quickstart](../../quickstart) to uninstall Envoy Gateway and the example manifest. + +Delete the demo auth services, HTTPRoute, EnvoyExtensionPolicy and BackendTLSPolicy: + +```shell +kubectl delete -f https://raw.githubusercontent.com/envoyproxy/gateway/latest/examples/kubernetes/ext-proc-grpc-service.yaml +kubectl delete httproute/myapp +kubectl delete envoyextensionpolicy/ext-proc-example +kubectl delete backendtlspolicy/grpc-ext-proc-btls +``` + +## Next Steps + +Checkout the [Developer Guide](../../../contributions/develop) to get involved in the project. + +[EnvoyExtensionPolicy]: ../../../api/extension_types#envoyextensionpolicy +[BackendTLSPolicy]: https://gateway-api.sigs.k8s.io/api-types/backendtlspolicy/ +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute diff --git a/site/content/en/v1.1/tasks/extensibility/extension-server.md b/site/content/en/v1.1/tasks/extensibility/extension-server.md new file mode 100644 index 00000000000..7d67c23f6da --- /dev/null +++ b/site/content/en/v1.1/tasks/extensibility/extension-server.md @@ -0,0 +1,209 @@ +--- +title: "Envoy Gateway Extension Server" +--- + +This task explains how to extend Envoy Gateway using an Extension Server. Envoy Gateway +can be configured to call an external server over gRPC with the xDS configuration _before_ +it is sent to Envoy Proxy. The external server can modify the provided configuration +programmatically using any semantics supported by the [xDS][] API. + +Using an extension server allows vendors to add xDS configuration that Envoy Gateway itself +doesn't support with a very high level of control over the generated xDS configuration. + +**Note:** Modifying the xDS configuration generated by Envoy Gateway may break functionality +configured by native Envoy Gateway means. Like other cases where the xDS configuration +is modified outside of Envoy Gateway's control, this is risky and should be tested thoroughly, +especially when using the same extension server across different Envoy Gateway versions. + +## Introduction + +One of the Envoy Gateway project goals is to "provide a common foundation for vendors to +build value-added products without having to re-engineer fundamental interactions". The +Envoy Gateway Extension Server provides a mechanism where Envoy Gateway tracks all provider +resources and then calls a set of hooks that allow the generated xDS configuration to be +modified before it is sent to Envoy Proxy. See the [design documentation][] for full details. + +This task sets up an example extension server that adds the Envoy Proxy Basic Authentication +HTTP filter to all the listeners generated by Envoy Gateway. The example extension server +includes its own CRD which allows defining username/password pairs that will be accepted by +the Envoy Proxy. + +**Note:** Envoy Gateway supports adding Basic Authentication to routes using a [SecurityPolicy][]. +See [this task](../security/basic-auth) for the preferred way to configure Basic +Authentication. + + +## Quickstart + +### Prerequisites + +* Follow the steps from the [Quickstart](../quickstart) task to install Envoy Gateway and the example manifest. + Before proceeding, you should be able to query the example backend using HTTP. + +### Build and run the example Extension Server + +Build and deploy the example extension server in the `examples/extension-server` folder into the cluster +running Envoy Gateway. + +* Build the extension server image + + **Note:** The provided `Makefile` builds an image with the name `extension-server:latest`. You may need to create +a different tag for it in order to allow Kubernetes to pull it correctly. + + ```shell + make image + ``` + +* Publish the extension server image in your docker repository + + {{< tabpane text=true >}} + {{% tab header="local kind server" %}} + + ```shell + kind load docker-image --name envoy-gateway extension-server:latest + ``` + + {{% /tab %}} + {{% tab header="other Kubernetes server" %}} + + ```shell + docker tag extension-server:latest $YOUR_DOCKER_REPO + docker push $YOUR_DOCKER_REPO + ``` + + {{% /tab %}} + {{< /tabpane >}} + +* Deploy the extension server in your cluster + + If you are using your own docker image repository, make sure to update the `values.yaml` with the correct +image name and tag. + + ```shell + helm install -n envoy-gateway-system extension-server ./examples/extension-server/charts/extension-server + ``` + +### Configure Envoy Gateway + +* Grant Envoy Gateway's `ServiceAccount` permission to access the extension server's CRD + + ```shell + kubectl create clusterrole listener-context-example-viewer \ + --verb=get,list,watch \ + --resource=ListenerContextExample + + kubectl create clusterrolebinding envoy-gateway-listener-context \ + --clusterrole=listener-context-example-viewer \ + --serviceaccount=envoy-gateway-system:envoy-gateway + ``` + +* Configure Envoy Gateway to use the Extension Server + + Add the following fragment to Envoy Gateway's [configuration][] file: + + ```yaml + extensionManager: + # Envoy Gateway will watch these resource kinds and use them as extension policies + # which can be attached to Gateway resources. + policyResources: + - group: example.extensions.io + version: v1alpha1 + kind: ListenerContextExample + hooks: + # The type of hooks that should be invoked + xdsTranslator: + post: + - HTTPListener + service: + # The service that is hosting the extension server + fqdn: + hostname: extension-server.envoy-gateway-system.svc.cluster.local + port: 5005 + ``` + + After updating Envoy Gateway's configuration file, restart Envoy Gateway. + +## Testing + +Get the Gateway's address: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +The extension server adds the Basic Authentication HTTP filter to all listeners configured by +Envoy Gateway. Initially there are no valid user/password combinations available. Accessing the +example backend should fail with a 401 status: + +```console +$ curl -v --header "Host: www.example.com" "http://${GATEWAY_HOST}/example" +... +> GET /example HTTP/1.1 +> Host: www.example.com +> User-Agent: curl/7.81.0 +> Accept: */* +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 401 Unauthorized +< www-authenticate: Basic realm="http://www.example.com/example" +< content-length: 58 +< content-type: text/plain +< date: Mon, 08 Jul 2024 10:53:11 GMT +< +... +User authentication failed. Missing username and password. +... +``` + +Add a new Username/Password combination using the example extension server's CRD: + +```shell +kubectl apply -f - << EOF +apiVersion: example.extensions.io/v1alpha1 +kind: ListenerContextExample +metadata: + name: listeneruser +spec: + targetRefs: + - kind: Gateway + name: eg + group: gateway.networking.k8s.io + username: user + password: p@ssw0rd +EOF +``` + +Authenticating with this user/password combination will now work. + +```console +$ curl -v http://${GATEWAY_HOST}/example -H "Host: www.example.com" --user 'user:p@ssw0rd' +... +> GET /example HTTP/1.1 +> Host: www.example.com +> Authorization: Basic dXNlcm5hbWU6cEBzc3cwcmQ= +> User-Agent: curl/7.81.0 +> Accept: */* +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< date: Mon, 08 Jul 2024 10:56:17 GMT +< content-length: 559 +< +... + "headers": { + "Authorization": [ + "Basic dXNlcm5hbWU6cEBzc3cwcmQ=" + ], + "X-Example-Ext": [ + "user" + ], +... +``` + + +[xDS]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/operations/dynamic_configuration +[design documentation]: /contributions/design/extending-envoy-gateway +[SecurityPolicy]: /latest/api/extension_types/#securitypolicy +[configuration]: /latest/api/extension_types/#extensionmanager diff --git a/site/content/en/v1.1/tasks/extensibility/wasm.md b/site/content/en/v1.1/tasks/extensibility/wasm.md new file mode 100644 index 00000000000..d973de77950 --- /dev/null +++ b/site/content/en/v1.1/tasks/extensibility/wasm.md @@ -0,0 +1,194 @@ +--- +title: "Wasm Extensions" +--- + +This task provides instructions for extending Envoy Gateway with WebAssembly (Wasm) extensions. + +Wasm extensions allow you to extend the functionality of Envoy Gateway by running custom code against HTTP requests and responses, +without modifying the Envoy Gateway binary. These extensions can be written in any language that compiles to Wasm, such as C++, Rust, AssemblyScript, or TinyGo. + +Envoy Gateway introduces a new CRD called [EnvoyExtensionPolicy][] that allows the user to configure Wasm extensions. +This instantiated resource can be linked to a [Gateway][Gateway] and [HTTPRoute][HTTPRoute] resource. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +Verify the Gateway status: + +```shell +kubectl get gateway/eg -o yaml +``` + +## Configuration + +Envoy Gateway supports two types of Wasm extensions: +* HTTP Wasm Extension: The Wasm extension is fetched from a remote URL. +* Image Wasm Extension: The Wasm extension is packaged as an OCI image and fetched from an image registry. + +The following example demonstrates how to configure an [EnvoyExtensionPolicy][] to attach a Wasm extension to an [EnvoyExtensionPolicy][] . +This Wasm extension adds a custom header `x-wasm-custom: FOO` to the response. + +### HTTP Wasm Extension + +This [EnvoyExtensionPolicy][] configuration fetches the Wasm extension from an HTTP URL. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the EnvoyExtensionPolicy status: + +```shell +kubectl get envoyextensionpolicy/http-wasm-source-test -o yaml +``` + +### Image Wasm Extension + +This [EnvoyExtensionPolicy][] configuration fetches the Wasm extension from an OCI image. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the EnvoyExtensionPolicy status: + +```shell +kubectl get envoyextensionpolicy/http-wasm-source-test -o yaml +``` + +### Testing + +Ensure the `GATEWAY_HOST` environment variable from the [Quickstart](../../quickstart) is set. If not, follow the +Quickstart instructions to set the variable. + +```shell +echo $GATEWAY_HOST +``` + +Send a request to the backend service: + +```shell +curl -i -H "Host: www.example.com" "http://${GATEWAY_HOST}" +``` + +You should see that the wasm extension has added this header to the response: + +``` +x-wasm-custom: FOO +``` + +## Clean-Up + +Follow the steps from the [Quickstart](../../quickstart) to uninstall Envoy Gateway and the example manifest. + +Delete the EnvoyExtensionPolicy: + +```shell +kubectl delete envoyextensionpolicy/wasm-test +``` + +## Next Steps + +Checkout the [Developer Guide](../../../contributions/develop) to get involved in the project. + +[EnvoyExtensionPolicy]: ../../../api/extension_types#envoyextensionpolicy +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute diff --git a/site/content/en/v1.1/tasks/observability/_index.md b/site/content/en/v1.1/tasks/observability/_index.md new file mode 100644 index 00000000000..9ca4896ee8b --- /dev/null +++ b/site/content/en/v1.1/tasks/observability/_index.md @@ -0,0 +1,5 @@ +--- +title: "Observability" +weight: 4 +description: This section includes Observability tasks. +--- diff --git a/site/content/en/v1.1/tasks/observability/gateway-api-metrics.md b/site/content/en/v1.1/tasks/observability/gateway-api-metrics.md new file mode 100644 index 00000000000..bd9e5b89317 --- /dev/null +++ b/site/content/en/v1.1/tasks/observability/gateway-api-metrics.md @@ -0,0 +1,59 @@ +--- +title: "Gateway API Metrics" +--- + +Resource metrics for Gateway API objects are available using the [Gateway API State Metrics][gasm] project. +The project also provides example dashboard for visualising the metrics using Grafana, and example alerts using Prometheus & Alertmanager. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +Run the following commands to install the metrics stack, with the Gateway API State Metrics configuration, on your kubernetes cluster: + +```shell +kubectl apply --server-side -f https://raw.githubusercontent.com/Kuadrant/gateway-api-state-metrics/main/config/examples/kube-prometheus/bundle_crd.yaml +kubectl apply -f https://raw.githubusercontent.com/Kuadrant/gateway-api-state-metrics/main/config/examples/kube-prometheus/bundle.yaml +``` + +## Metrics and Alerts + +To access the Prometheus UI, wait for the statefulset to be ready, then use the port-forward command: + +```shell +# This first command may fail if the statefulset has not been created yet. +# In that case, try again until you get a message like 'Waiting for 2 pods to be ready...' +# or 'statefulset rolling update complete 2 pods...' +kubectl -n monitoring rollout status --watch --timeout=5m statefulset/prometheus-k8s +kubectl -n monitoring port-forward service/prometheus-k8s 9090:9090 > /dev/null & +``` + +Navigate to `http://localhost:9090`. +Metrics can be queried from the 'Graph' tab e.g. `gatewayapi_gateway_created` +See the [Gateway API State Metrics README][gasm-readme] for the full list of Gateway API metrics available. + +Alerts can be seen in the 'Alerts' tab. +Gateway API specific alerts will be grouped under the 'gateway-api.rules' heading. + +***Note:*** Alerts are defined in a PrometheusRules custom resource in the 'monitoring' namespace. You can modify the alert rules by updating this resource. + +## Dashboards + +To view the dashboards in Grafana, wait for the deployment to be ready, then use the port-forward command: + +```shell +kubectl -n monitoring wait --timeout=5m deployment/grafana --for=condition=Available +kubectl -n monitoring port-forward service/grafana 3000:3000 > /dev/null & +``` + +Navigate to `http://localhost:3000` and sign in with admin/admin. +The Gateway API State dashboards will be available in the 'Default' folder and tagged with 'gateway-api'. +See the [Gateway API State Metrics README][gasm-dashboards] for further information on available dashboards. + +***Note:*** Dashboards are loaded from configmaps. You can modify the dashboards in the Grafana UI, however you will need to export them from the UI and update the json in the configmaps to persist changes. + + +[gasm]: https://github.com/Kuadrant/gateway-api-state-metrics +[gasm-readme]: https://github.com/Kuadrant/gateway-api-state-metrics/tree/main#metrics +[gasm-dashboards]: https://github.com/Kuadrant/gateway-api-state-metrics/tree/main#dashboards diff --git a/site/content/en/v1.1/tasks/observability/gateway-exported-metrics.md b/site/content/en/v1.1/tasks/observability/gateway-exported-metrics.md new file mode 100644 index 00000000000..cf04f1d444b --- /dev/null +++ b/site/content/en/v1.1/tasks/observability/gateway-exported-metrics.md @@ -0,0 +1,97 @@ +--- +title: "Gateway Exported Metrics" +--- + +The Envoy Gateway provides a collection of self-monitoring metrics in [Prometheus format][prom-format]. + +These metrics allow monitoring of the behavior of Envoy Gateway itself (as distinct from that of the EnvoyProxy it managed). + +{{% alert title="EnvoyProxy Metrics" color="warning" %}} +For EnvoyProxy Metrics, please refer to the [EnvoyProxy Metrics](./proxy-metric) to learn more. +{{% /alert %}} + +## Watching Components + +The Resource Provider, xDS Translator and Infra Manager etc. are key components that made up of Envoy Gateway, +they all follow the design of [Watching Components](../../../contributions/design/watching). + +Envoy Gateway collects the following metrics in Watching Components: + +| Name | Description | +|----------------------------------------|--------------------------------------------------------------| +| `watchable_depth` | Current depth of watchable map. | +| `watchable_subscribe_duration_seconds` | How long in seconds a subscribed watchable queue is handled. | +| `watchable_subscribe_total` | Total number of subscribed watchable queue. | + +Each metric includes the `runner` label to identify the corresponding components, +the relationship between label values and components is as follows: + +| Value | Components | +|--------------------|---------------------------------| +| `gateway-api` | Gateway API Translator | +| `infrastructure` | Infrastructure Manager | +| `xds-server` | xDS Server | +| `xds-translator` | xDS Translator | +| `global-ratelimit` | Global RateLimit xDS Translator | + +Metrics may include one or more additional labels, such as `message`, `status` and `reason` etc. + +## Status Updater + +Envoy Gateway monitors the status updates of various resources (like `GatewayClass`, `Gateway` and `HTTPRoute` etc.) through Status Updater. + +Envoy Gateway collects the following metrics in Status Updater: + +| Name | Description | +|----------------------------------|------------------------------------------------| +| `status_update_total` | Total number of status update by object kind. | +| `status_update_duration_seconds` | How long a status update takes to finish. | + +Each metric includes `kind` label to identify the corresponding resources. + +## xDS Server + +Envoy Gateway monitors the cache and xDS connection status in xDS Server. + +Envoy Gateway collects the following metrics in xDS Server: + +| Name | Description | +|-------------------------------|--------------------------------------------------------| +| `xds_snapshot_create_total` | Total number of xds snapshot cache creates. | +| `xds_snapshot_update_total` | Total number of xds snapshot cache updates by node id. | +| `xds_stream_duration_seconds` | How long a xds stream takes to finish. | + +- For xDS snapshot cache update and xDS stream connection status, each metric includes `nodeID` label to identify the connection peer. +- For xDS stream connection status, each metric also includes `streamID` label to identify the connection stream, and `isDeltaStream` label to identify the delta connection stream. + +## Infrastructure Manager + +Envoy Gateway monitors the `apply` (`create` or `update`) and `delete` operations in Infrastructure Manager. + +Envoy Gateway collects the following metrics in Infrastructure Manager: + +| Name | Description | +|------------------------------------|---------------------------------------------------------| +| `resource_apply_total` | Total number of applied resources. | +| `resource_apply_duration_seconds` | How long in seconds a resource be applied successfully. | +| `resource_delete_total` | Total number of deleted resources. | +| `resource_delete_duration_seconds` | How long in seconds a resource be deleted successfully. | + +Each metric includes the `kind` label to identify the corresponding resources being applied or deleted by Infrastructure Manager. + +Metrics may also include `name` and `namespace` label to identify the name and namespace of corresponding Infrastructure Manager. + +## Wasm + +Envoy Gateway monitors the status of Wasm remote fetch cache. + +| Name | Description | +|---------------------------|--------------------------------------------------| +| `wasm_cache_entries` | Number of Wasm remote fetch cache entries. | +| `wasm_cache_lookup_total` | Total number of Wasm remote fetch cache lookups. | +| `wasm_remote_fetch_total` | Total number of Wasm remote fetches and results. | + +For metric `wasm_cache_lookup_total`, we are using `hit` label (boolean) to indicate whether the Wasm cache has been hit. + + +[prom-format]: https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format diff --git a/site/content/en/v1.1/tasks/observability/gateway-observability.md b/site/content/en/v1.1/tasks/observability/gateway-observability.md new file mode 100644 index 00000000000..6e0040b4f5d --- /dev/null +++ b/site/content/en/v1.1/tasks/observability/gateway-observability.md @@ -0,0 +1,176 @@ +--- +title: "Gateway Observability" +--- + +Envoy Gateway provides observability for the ControlPlane and the underlying EnvoyProxy instances. +This task show you how to config gateway control-plane observability, includes metrics. + +## Prerequisites + +{{< boilerplate o11y_prerequisites >}} + +## Metrics + +The default installation of Envoy Gateway installs a default [EnvoyGateway][] configuration and attaches it +using a `ConfigMap`. In this section, we will update this resource to enable various ways to retrieve metrics +from Envoy Gateway. + +{{% alert title="Exported Metrics" color="warning" %}} +Refer to the [Gateway Exported Metrics List](./gateway-exported-metrics) to learn more about Envoy Gateway's Metrics. +{{% /alert %}} + +### Retrieve Prometheus Metrics from Envoy Gateway + +By default, prometheus metric is enabled. You can directly retrieve metrics from Envoy Gateway: + +```shell +export ENVOY_POD_NAME=$(kubectl get pod -n envoy-gateway-system --selector=control-plane=envoy-gateway,app.kubernetes.io/instance=eg -o jsonpath='{.items[0].metadata.name}') +kubectl port-forward pod/$ENVOY_POD_NAME -n envoy-gateway-system 19001:19001 + +# check metrics +curl localhost:19001/metrics +``` + +The following is an example to disable prometheus metric for Envoy Gateway. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +After updating the `ConfigMap`, you will need to restart the `envoy-gateway` deployment so the configuration kicks in: + +```shell +kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system +``` + +### Enable Open Telemetry sink in Envoy Gateway + +The following is an example to send metric via Open Telemetry sink to OTEL gRPC Collector. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +After updating the `ConfigMap`, you will need to restart the `envoy-gateway` deployment so the configuration kicks in: + +```shell +kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system +``` + +Verify OTel-Collector metrics: + +```shell +export OTEL_POD_NAME=$(kubectl get pod -n monitoring --selector=app.kubernetes.io/name=opentelemetry-collector -o jsonpath='{.items[0].metadata.name}') +kubectl port-forward pod/$OTEL_POD_NAME -n monitoring 19001:19001 + +# check metrics +curl localhost:19001/metrics +``` + +[EnvoyGateway]: ../../api/extension_types#envoygateway diff --git a/site/content/en/v1.1/tasks/observability/grafana-integration.md b/site/content/en/v1.1/tasks/observability/grafana-integration.md new file mode 100644 index 00000000000..259f6958bf0 --- /dev/null +++ b/site/content/en/v1.1/tasks/observability/grafana-integration.md @@ -0,0 +1,87 @@ +--- +title: "Visualising metrics using Grafana" +--- + +Envoy Gateway provides support for exposing Envoy Gateway and Envoy Proxy metrics to a Prometheus instance. +This task shows you how to visualise the metrics exposed to Prometheus using Grafana. + +## Prerequisites + +{{< boilerplate o11y_prerequisites >}} + +Follow the steps from the [Gateway Observability](./gateway-observability) and [Proxy Metrics](./proxy-metric) to enable Prometheus metrics +for both Envoy Gateway (Control Plane) and Envoy Proxy (Data Plane). + +Expose endpoints: + +```shell +GRAFANA_IP=$(kubectl get svc grafana -n monitoring -o jsonpath='{.status.loadBalancer.ingress[0].ip}') +``` + +## Connecting Grafana with Prometheus datasource + +To visualise metrics from Prometheus, we have to connect Grafana with Prometheus. If you installed Grafana follow the command +from prerequisites sections, the Prometheus datasource should be already configured. + +You can also add the datasource manually by following the instructions from [Grafana Docs](https://grafana.com/docs/grafana/latest/datasources/prometheus/configure-prometheus-data-source/). + +## Accessing Grafana + +You can access the Grafana instance by visiting `http://{GRAFANA_IP}`, derived in prerequisites. + +To log in to Grafana, use the credentials `admin:admin`. + +Envoy Gateway has examples of dashboard for you to get started, you can check them out under `Dashboards/envoy-gateway`. + +If you'd like import Grafana dashboards on your own, please refer to Grafana docs for [importing dashboards](https://grafana.com/docs/grafana/latest/dashboards/manage-dashboards/#import-a-dashboard). + +### Envoy Proxy Global + +This dashboard example shows the overall downstream and upstream stats for each Envoy Proxy instance. + +![Envoy Proxy Global](/img/envoy-proxy-global-dashboard.png) + +### Envoy Clusters + +This dashboard example shows the overall stats for each cluster from Envoy Proxy fleet. + +![Envoy Clusters](/img/envoy-clusters-dashboard.png) + +### Envoy Gateway Global + +This dashboard example shows the overall stats exported by Envoy Gateway fleet. + +![Envoy Gateway Global: Watching Components](/img/envoy-gateway-global-watching-components.png) + +![Envoy Gateway Global: Status Updater](/img/envoy-gateway-global-status-updater.png) + +![Envoy Gateway Global: xDS Server](/img/envoy-gateway-global-xds-server.png) + +![Envoy Gateway Global: Infrastructure Manager](/img/envoy-gateway-global-infra-manager.png) + +### Resources Monitor + +This dashboard example shows the overall resources stats for both Envoy Gateway and Envoy Proxy fleet. + +![Envoy Gateway Resources](/img/resources-monitor-dashboard.png) + +## Update Dashboards + +All dashboards of Envoy Gateway are maintained under `charts/gateway-addons-helm/dashboards`, +feel free to make [contributions](../../../contributions/CONTRIBUTING). + +### Grafonnet + +Newer dashboards are generated with [Jsonnet](https://jsonnet.org/) with the [Grafonnet](https://grafana.github.io/grafonnet/index.html). +This is the preferred method for any new dashboards. + +You can run `make helm-generate.gateway-addons-helm` to generate new version of dashboards. +All the generated dashboards have a `.gen.json` suffix. + +### Legacy Dashboards + +Many of our older dashboards are manually created in the UI and exported as JSON and checked in. + +These example dashboards cannot be updated in-place by default, if you are trying to +make some changes to the older dashboards, you can save them directly as a JSON file +and then re-import. diff --git a/site/content/en/v1.1/tasks/observability/proxy-accesslog.md b/site/content/en/v1.1/tasks/observability/proxy-accesslog.md new file mode 100644 index 00000000000..fb0200f1739 --- /dev/null +++ b/site/content/en/v1.1/tasks/observability/proxy-accesslog.md @@ -0,0 +1,251 @@ +--- +title: "Proxy Access Logs" +--- + +Envoy Gateway provides observability for the ControlPlane and the underlying EnvoyProxy instances. +This task show you how to config proxy access logs. + +## Prerequisites + +{{< boilerplate o11y_prerequisites >}} + +By default, the Service type of `loki` is ClusterIP, you can change it to LoadBalancer type for further usage: + +```shell +kubectl patch service loki -n monitoring -p '{"spec": {"type": "LoadBalancer"}}' +``` + +Expose endpoints: + +```shell +LOKI_IP=$(kubectl get svc loki -n monitoring -o jsonpath='{.status.loadBalancer.ingress[0].ip}') +``` + +## Default Access Log + +If custom format string is not specified, Envoy Gateway uses the following default format: + +```json +{ + "start_time": "%START_TIME%", + "method": "%REQ(:METHOD)%", + "x-envoy-origin-path": "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%", + "protocol": "%PROTOCOL%", + "response_code": "%RESPONSE_CODE%", + "response_flags": "%RESPONSE_FLAGS%", + "response_code_details": "%RESPONSE_CODE_DETAILS%", + "connection_termination_details": "%CONNECTION_TERMINATION_DETAILS%", + "upstream_transport_failure_reason": "%UPSTREAM_TRANSPORT_FAILURE_REASON%", + "bytes_received": "%BYTES_RECEIVED%", + "bytes_sent": "%BYTES_SENT%", + "duration": "%DURATION%", + "x-envoy-upstream-service-time": "%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%", + "x-forwarded-for": "%REQ(X-FORWARDED-FOR)%", + "user-agent": "%REQ(USER-AGENT)%", + "x-request-id": "%REQ(X-REQUEST-ID)%", + ":authority": "%REQ(:AUTHORITY)%", + "upstream_host": "%UPSTREAM_HOST%", + "upstream_cluster": "%UPSTREAM_CLUSTER%", + "upstream_local_address": "%UPSTREAM_LOCAL_ADDRESS%", + "downstream_local_address": "%DOWNSTREAM_LOCAL_ADDRESS%", + "downstream_remote_address": "%DOWNSTREAM_REMOTE_ADDRESS%", + "requested_server_name": "%REQUESTED_SERVER_NAME%", + "route_name": "%ROUTE_NAME%" +} +``` + +> Note: Envoy Gateway disable envoy headers by default, you can enable it by setting `EnableEnvoyHeaders` to `true` in the [ClientTrafficPolicy](../../api/extension_types#backendtrafficpolicy) CRD. + + +Verify logs from loki: + +```shell +curl -s "http://$LOKI_IP:3100/loki/api/v1/query_range" --data-urlencode "query={job=\"fluentbit\"}" | jq '.data.result[0].values' +``` + +## Disable Access Log + +If you want to disable it, set the `telemetry.accesslog.disable` to `true` in the `EnvoyProxy` CRD. + +```shell +kubectl apply -f - <}} + +## Metrics + +By default, Envoy Gateway expose metrics with prometheus endpoint. + +Verify metrics: + +```shell +export ENVOY_POD_NAME=$(kubectl get pod -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}') +kubectl port-forward pod/$ENVOY_POD_NAME -n envoy-gateway-system 19001:19001 + +# check metrics +curl localhost:19001/stats/prometheus | grep "default/backend/rule/0" +``` + +You can disable metrics by setting the `telemetry.metrics.prometheus.disable` to `true` in the `EnvoyProxy` CRD. + +```shell +kubectl apply -f https://raw.githubusercontent.com/envoyproxy/gateway/latest/examples/kubernetes/metric/disable-prometheus.yaml +``` + +Envoy Gateway can send metrics to OpenTelemetry Sink. +Send metrics to OTel-Collector: + +```shell +kubectl apply -f https://raw.githubusercontent.com/envoyproxy/gateway/latest/examples/kubernetes/metric/otel-sink.yaml +``` + +Verify OTel-Collector metrics: + +```shell +export OTEL_POD_NAME=$(kubectl get pod -n monitoring --selector=app.kubernetes.io/name=opentelemetry-collector -o jsonpath='{.items[0].metadata.name}') +kubectl port-forward pod/$OTEL_POD_NAME -n monitoring 19001:19001 + +# check metrics +curl localhost:19001/metrics | grep "default/backend/rule/0" +``` diff --git a/site/content/en/v1.1/tasks/observability/proxy-trace.md b/site/content/en/v1.1/tasks/observability/proxy-trace.md new file mode 100644 index 00000000000..ddaf68e415a --- /dev/null +++ b/site/content/en/v1.1/tasks/observability/proxy-trace.md @@ -0,0 +1,233 @@ +--- +title: "Proxy Tracing" +--- + +Envoy Gateway provides observability for the ControlPlane and the underlying EnvoyProxy instances. +This task show you how to config proxy tracing. + +## Prerequisites + +{{< boilerplate o11y_prerequisites >}} + +Expose Tempo endpoints: + +```shell +TEMPO_IP=$(kubectl get svc tempo -n monitoring -o jsonpath='{.status.loadBalancer.ingress[0].ip}') +``` + +## Traces + +By default, Envoy Gateway doesn't send traces to any sink. +You can enable traces by setting the `telemetry.tracing` in the [EnvoyProxy][envoy-proxy-crd] CRD. +Currently, Envoy Gateway support OpenTelemetry and [Zipkin](../../api/extension_types#zipkintracingprovider) tracer. + +### Tracing Provider + +The following configurations show how to apply proxy with different providers: + +{{< tabpane text=true >}} +{{% tab header="OpenTelemetry" %}} + +```shell +kubectl apply -f - <}} + +Query trace by trace id: + +```shell +curl -s "http://$TEMPO_IP:3100/api/traces/" | jq +``` + + +### Sampling Rate + +Envoy Gateway use 100% sample rate, which means all requests will be traced. +This may cause performance issues when traffic is very high, you can adjust +the sample rate by setting the `telemetry.tracing.samplingRate` in the [EnvoyProxy][envoy-proxy-crd] CRD. + +The following configurations show how to apply proxy with 1% sample rates: + +```shell +kubectl apply -f - <}} + +Follow the steps from the [Global Rate Limit](../traffic/global-rate-limit) to install RateLimit. + +## Traces + +By default, the Envoy Gateway does not configure RateLimit to send traces to the OpenTelemetry Sink. +You can configure the collector in the `rateLimit.telemetry.tracing` of the `EnvoyGateway`CRD. + +RateLimit uses the OpenTelemetry Exporter to export traces to the collector. +You can configure a collector that supports the OTLP protocol, which includes but is not limited to: OpenTelemetry Collector, Jaeger, Zipkin, and so on. + +***Note:*** + +* By default, the Envoy Gateway configures a `100%` sampling rate for RateLimit, which may lead to performance issues. + +Assuming the OpenTelemetry Collector is running in the `observability` namespace, and it has a service named `otel-svc`, +we only want to sample `50%` of the trace data. We would configure it as follows: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +After updating the ConfigMap, you will need to restart the envoy-gateway deployment so the configuration kicks in: + +```shell +kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system +``` diff --git a/site/content/en/v1.1/tasks/operations/_index.md b/site/content/en/v1.1/tasks/operations/_index.md new file mode 100644 index 00000000000..d87097c7d1e --- /dev/null +++ b/site/content/en/v1.1/tasks/operations/_index.md @@ -0,0 +1,5 @@ +--- +title: "Operations" +weight: 4 +description: This section includes Operations tasks. +--- diff --git a/site/content/en/v1.1/tasks/operations/customize-envoyproxy.md b/site/content/en/v1.1/tasks/operations/customize-envoyproxy.md new file mode 100644 index 00000000000..562237bfc43 --- /dev/null +++ b/site/content/en/v1.1/tasks/operations/customize-envoyproxy.md @@ -0,0 +1,955 @@ +--- +title: "Customize EnvoyProxy" +--- + +Envoy Gateway provides an [EnvoyProxy][] CRD that can be linked to the ParametersRef +in GatewayClass, allowing cluster admins to customize the managed EnvoyProxy Deployment and +Service. To learn more about GatewayClass and ParametersRef, please refer to [Gateway API documentation][]. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +Before you start, you need to add `ParametersRef` in GatewayClass, and refer to EnvoyProxy Config: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +## Customize EnvoyProxy Deployment Replicas + +You can customize the EnvoyProxy Deployment Replicas via EnvoyProxy Config like: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +After you apply the config, you should see the replicas of envoyproxy changes to 2. +And also you can dynamically change the value. + +``` shell +kubectl get deployment -l gateway.envoyproxy.io/owning-gateway-name=eg -n envoy-gateway-system +``` + +## Customize EnvoyProxy Image + +You can customize the EnvoyProxy Image via EnvoyProxy Config like: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +After applying the config, you can get the deployment image, and see it has changed. + +## Customize EnvoyProxy Pod Annotations + +You can customize the EnvoyProxy Pod Annotations via EnvoyProxy Config like: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +After applying the config, you can get the envoyproxy pods, and see new annotations has been added. + +## Customize EnvoyProxy Deployment Resources + +You can customize the EnvoyProxy Deployment Resources via EnvoyProxy Config like: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +## Customize EnvoyProxy Deployment Env + +You can customize the EnvoyProxy Deployment Env via EnvoyProxy Config like: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +> Envoy Gateway has provided two initial `env` `ENVOY_GATEWAY_NAMESPACE` and `ENVOY_POD_NAME` for envoyproxy container. + +After applying the config, you can get the envoyproxy deployment, and see resources has been changed. + +## Customize EnvoyProxy Deployment Volumes or VolumeMounts + +You can customize the EnvoyProxy Deployment Volumes or VolumeMounts via EnvoyProxy Config like: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +After applying the config, you can get the envoyproxy deployment, and see resources has been changed. + +## Customize EnvoyProxy Service Annotations + +You can customize the EnvoyProxy Service Annotations via EnvoyProxy Config like: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +After applying the config, you can get the envoyproxy service, and see annotations has been added. + +## Customize EnvoyProxy Bootstrap Config + +You can customize the EnvoyProxy bootstrap config via EnvoyProxy Config. +There are two ways to customize it: + +* Replace: the whole bootstrap config will be replaced by the config you provided. +* Merge: the config you provided will be merged into the default bootstrap config. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +You can use [egctl translate][] +to get the default xDS Bootstrap configuration used by Envoy Gateway. + +After applying the config, the bootstrap config will be overridden by the new config you provided. +Any errors in the configuration will be surfaced as status within the `GatewayClass` resource. +You can also validate this configuration using [egctl translate][]. + +## Customize EnvoyProxy Horizontal Pod Autoscaler + +You can enable [Horizontal Pod Autoscaler](https://github.com/envoyproxy/gateway/issues/703) for EnvoyProxy Deployment. However, before enabling the HPA for EnvoyProxy, please ensure that the [metrics-server](https://github.com/kubernetes-sigs/metrics-server) component is installed in the cluster. + +Once confirmed, you can apply it via EnvoyProxy Config as shown below: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +After applying the config, the EnvoyProxy HPA (Horizontal Pod Autoscaler) is generated. However, upon activating the EnvoyProxy's HPA, the Envoy Gateway will no longer reference the `replicas` field specified in the `envoyDeployment`, as outlined [here](#customize-envoyproxy-deployment-replicas). + +## Customize EnvoyProxy Command line options + +You can customize the EnvoyProxy Command line options via `spec.extraArgs` in EnvoyProxy Config. +For example, the following configuration will add `--disable-extensions` arg in order to disable `envoy.access_loggers/envoy.access_loggers.wasm` extension: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +## Customize EnvoyProxy with Patches + +You can customize the EnvoyProxy using patches. + +### Patching Deployment for EnvoyProxy + +For example, the following configuration will add resource limits to the `envoy` and the `shutdown-manager` containers in the `envoyproxy` deployment: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +After applying the configuration, you will see the change in both containers in the `envoyproxy` deployment. + +### Patching Service for EnvoyProxy + +For example, the following configuration will add an annotation for the `envoyproxy` service: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +After applying the configuration, you will see the `custom-annotation: foobar` has been added to the `envoyproxy` service. + +## Customize Filter Order + +Under the hood, Envoy Gateway uses a series of [Envoy HTTP filters](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/http_filters) +to process HTTP requests and responses, and to apply various policies. + +By default, Envoy Gateway applies the following filters in the order shown: +* envoy.filters.http.fault +* envoy.filters.http.cors +* envoy.filters.http.ext_authz +* envoy.filters.http.basic_authn +* envoy.filters.http.oauth2 +* envoy.filters.http.jwt_authn +* envoy.filters.http.ext_proc +* envoy.filters.http.wasm +* envoy.filters.http.rbac +* envoy.filters.http.local_ratelimit +* envoy.filters.http.ratelimit +* envoy.filters.http.router + +The default order in which these filters are applied is opinionated and may not suit all use cases. +To address this, Envoy Gateway allows you to adjust the execution order of these filters with the `filterOrder` field in the [EnvoyProxy][] resource. + +`filterOrder` is a list of customized filter order configurations. Each configuration can specify a filter +name and a filter to place it before or after. These configurations are applied in the order they are listed. +If a filter occurs in multiple configurations, the final order is the result of applying all these configurations in order. +To avoid conflicts, it is recommended to only specify one configuration per filter. + +For example, the following configuration moves the `envoy.filters.http.wasm` filter before the `envoy.filters.http.jwt_authn` +filter and the `envoy.filters.http.cors` filter after the `envoy.filters.http.basic_authn` filter: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +[Gateway API documentation]: https://gateway-api.sigs.k8s.io/ +[EnvoyProxy]: ../../../api/extension_types#envoyproxy +[egctl translate]: ../egctl/#validating-gateway-api-configuration diff --git a/site/content/en/v1.1/tasks/operations/deployment-mode.md b/site/content/en/v1.1/tasks/operations/deployment-mode.md new file mode 100644 index 00000000000..16b339fe571 --- /dev/null +++ b/site/content/en/v1.1/tasks/operations/deployment-mode.md @@ -0,0 +1,1072 @@ +--- +title: "Deployment Mode" +--- +## Deployment modes + +### One GatewayClass per Envoy Gateway Controller +* An Envoy Gateway is associated with a single [GatewayClass][] resource under one controller. +This is the simplest deployment mode and is suitable for scenarios where each Gateway needs to have its own dedicated set of resources and configurations. + +### Multiple GatewayClasses per Envoy Gateway Controller +* An Envoy Gateway is associated with multiple [GatewayClass][] resources under one controller. +* Support for accepting multiple GatewayClasses was added [here][issue1231]. + +### Separate Envoy Gateway Controllers +If you've instantiated multiple GatewayClasses, you can also run separate Envoy Gateway controllers in different namespaces, linking a GatewayClass to each of them for multi-tenancy. +Please follow the example [Multi-tenancy](#multi-tenancy). + +### Merged Gateways onto a single EnvoyProxy fleet +By default, each Gateway has its own dedicated set of Envoy Proxy and its configurations. +However, for some deployments, it may be more convenient to merge listeners across multiple Gateways and deploy a single Envoy Proxy fleet. + +This can help to efficiently utilize the infra resources in the cluster and manage them in a centralized manner, or have a single IP address for all of the listeners. +Setting the `mergeGateways` field in the EnvoyProxy resource linked to GatewayClass will result in merging all Gateway listeners under one GatewayClass resource. + +* The tuple of port, protocol, and hostname must be unique across all Listeners. + +Please follow the example [Merged gateways deployment](#merged-gateways-deployment). + +### Supported Modes + +#### Kubernetes + +* The default deployment model is - Envoy Gateway **watches** for resources such a `Service` & `HTTPRoute` in **all** namespaces +and **creates** managed data plane resources such as EnvoyProxy `Deployment` in the **namespace where Envoy Gateway is running**. +* Envoy Gateway also supports [Namespaced deployment mode][], you can watch resources in the specific namespaces by assigning +`EnvoyGateway.provider.kubernetes.watch.namespaces` or `EnvoyGateway.provider.kubernetes.watch.namespaceSelector` and **creates** managed data plane resources in the **namespace where Envoy Gateway is running**. +* Support for alternate deployment modes is being tracked [here][issue1117]. + +### Multi-tenancy + +#### Kubernetes + +* A `tenant` is a group within an organization (e.g. a team or department) who shares organizational resources. We recommend +each `tenant` deploy their own Envoy Gateway controller in their respective `namespace`. Below is an example of deploying Envoy Gateway +by the `marketing` and `product` teams in separate namespaces. + +* Lets deploy Envoy Gateway in the `marketing` namespace and also watch resources only in this namespace. We are also setting the controller name to a unique string here `gateway.envoyproxy.io/marketing-gatewayclass-controller`. + +```shell +helm install \ +--set config.envoyGateway.gateway.controllerName=gateway.envoyproxy.io/marketing-gatewayclass-controller \ +--set config.envoyGateway.provider.kubernetes.watch.type=Namespaces \ +--set config.envoyGateway.provider.kubernetes.watch.namespaces={marketing} \ +eg-marketing oci://docker.io/envoyproxy/gateway-helm \ +--version v0.0.0-latest -n marketing --create-namespace +``` + +Lets create a `GatewayClass` linked to the marketing team's Envoy Gateway controller, and as well other resources linked to it, so the `backend` application operated by this team can be exposed to external clients. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Lets port forward to the generated envoy proxy service in the `marketing` namespace and send a request to it. + +```shell +export ENVOY_SERVICE=$(kubectl get svc -n marketing --selector=gateway.envoyproxy.io/owning-gateway-namespace=marketing,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}') +kubectl -n marketing port-forward service/${ENVOY_SERVICE} 8888:8080 & +``` + +```shell +curl --verbose --header "Host: www.marketing.example.com" http://localhost:8888/get +``` + +```console +* Trying 127.0.0.1:8888... +* Connected to localhost (127.0.0.1) port 8888 (#0) +> GET /get HTTP/1.1 +> Host: www.marketing.example.com +> User-Agent: curl/7.86.0 +> Accept: */* +> +Handling connection for 8888 +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< date: Thu, 20 Apr 2023 19:19:42 GMT +< content-length: 521 +< x-envoy-upstream-service-time: 0 +< server: envoy +< +{ + "path": "/get", + "host": "www.marketing.example.com", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/7.86.0" + ], + "X-Envoy-Expected-Rq-Timeout-Ms": [ + "15000" + ], + "X-Envoy-Internal": [ + "true" + ], + "X-Forwarded-For": [ + "10.1.0.157" + ], + "X-Forwarded-Proto": [ + "http" + ], + "X-Request-Id": [ + "c637977c-458a-48ae-92b3-f8c429849322" + ] + }, + "namespace": "marketing", + "ingress": "", + "service": "", + "pod": "backend-74888f465f-bcs8f" +* Connection #0 to host localhost left intact +``` + +* Lets deploy Envoy Gateway in the `product` namespace and also watch resources only in this namespace. + +```shell +helm install \ +--set config.envoyGateway.gateway.controllerName=gateway.envoyproxy.io/product-gatewayclass-controller \ +--set config.envoyGateway.provider.kubernetes.watch.type=Namespaces \ +--set config.envoyGateway.provider.kubernetes.watch.namespaces={product} \ +eg-product oci://docker.io/envoyproxy/gateway-helm \ +--version v0.0.0-latest -n product --create-namespace +``` + +Lets create a `GatewayClass` linked to the product team's Envoy Gateway controller, and as well other resources linked to it, so the `backend` application operated by this team can be exposed to external clients. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Lets port forward to the generated envoy proxy service in the `product` namespace and send a request to it. + +```shell +export ENVOY_SERVICE=$(kubectl get svc -n product --selector=gateway.envoyproxy.io/owning-gateway-namespace=product,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}') +kubectl -n product port-forward service/${ENVOY_SERVICE} 8889:8080 & +``` + +```shell +curl --verbose --header "Host: www.product.example.com" http://localhost:8889/get +``` + +```shell +* Trying 127.0.0.1:8889... +* Connected to localhost (127.0.0.1) port 8889 (#0) +> GET /get HTTP/1.1 +> Host: www.product.example.com +> User-Agent: curl/7.86.0 +> Accept: */* +> +Handling connection for 8889 +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< date: Thu, 20 Apr 2023 19:20:17 GMT +< content-length: 517 +< x-envoy-upstream-service-time: 0 +< server: envoy +< +{ + "path": "/get", + "host": "www.product.example.com", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/7.86.0" + ], + "X-Envoy-Expected-Rq-Timeout-Ms": [ + "15000" + ], + "X-Envoy-Internal": [ + "true" + ], + "X-Forwarded-For": [ + "10.1.0.156" + ], + "X-Forwarded-Proto": [ + "http" + ], + "X-Request-Id": [ + "39196453-2250-4331-b756-54003b2853c2" + ] + }, + "namespace": "product", + "ingress": "", + "service": "", + "pod": "backend-74888f465f-64fjs" +* Connection #0 to host localhost left intact +``` + +With the below command you can ensure that you are no able to access the marketing team's backend exposed using the `www.marketing.example.com` hostname +and the product team's data plane. + +```shell +curl --verbose --header "Host: www.marketing.example.com" http://localhost:8889/get +``` + +```console +* Trying 127.0.0.1:8889... +* Connected to localhost (127.0.0.1) port 8889 (#0) +> GET /get HTTP/1.1 +> Host: www.marketing.example.com +> User-Agent: curl/7.86.0 +> Accept: */* +> +Handling connection for 8889 +* Mark bundle as not supporting multiuse +< HTTP/1.1 404 Not Found +< date: Thu, 20 Apr 2023 19:22:13 GMT +< server: envoy +< content-length: 0 +< +* Connection #0 to host localhost left intact +``` + +### Merged gateways deployment + +In this example, we will deploy GatewayClass + +```shell +apiVersion: gateway.networking.k8s.io/v1 +kind: GatewayClass +metadata: + name: merged-eg +spec: + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parametersRef: + group: gateway.envoyproxy.io + kind: EnvoyProxy + name: custom-proxy-config + namespace: envoy-gateway-system +``` + +with a referenced [EnvoyProxy][] resource configured to enable merged Gateways deployment mode. + +```shell +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyProxy +metadata: + name: custom-proxy-config + namespace: envoy-gateway-system +spec: + mergeGateways: true +``` + +#### Deploy merged-gateways example + +Deploy resources on your cluster from the example. + +```shell +kubectl apply -f https://raw.githubusercontent.com/envoyproxy/gateway/latest/examples/kubernetes/merged-gateways.yaml +``` + +Verify that Gateways are deployed and programmed + +```shell +kubectl get gateways -n default + +NAMESPACE NAME CLASS ADDRESS PROGRAMMED AGE +default merged-eg-1 merged-eg 172.18.255.202 True 2m4s +default merged-eg-2 merged-eg 172.18.255.202 True 2m4s +default merged-eg-3 merged-eg 172.18.255.202 True 2m4s +``` + +Verify that HTTPRoutes are deployed + +```shell +kubectl get httproute -n default +NAMESPACE NAME HOSTNAMES AGE +default hostname1-route ["www.merged1.com"] 2m4s +default hostname2-route ["www.merged2.com"] 2m4s +default hostname3-route ["www.merged3.com"] 2m4s +``` + +If you take a look at the deployed Envoy Proxy service you would notice that all of the Gateway listeners ports are added to that service. + +```shell +kubectl get service -n envoy-gateway-system +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +envoy-gateway ClusterIP 10.96.141.4 18000/TCP,18001/TCP 6m43s +envoy-gateway-metrics-service ClusterIP 10.96.113.191 19001/TCP 6m43s +envoy-merged-eg-668ac7ae LoadBalancer 10.96.48.255 172.18.255.202 8081:30467/TCP,8082:31793/TCP,8080:31153/TCP 3m17s +``` + +There should be also one deployment (envoy-merged-eg-668ac7ae-775f9865d-55zhs) for every Gateway and its name should reference the name of the GatewayClass. + +```shell +kubectl get pods -n envoy-gateway-system +NAME READY STATUS RESTARTS AGE +envoy-gateway-5d998778f6-wr6m9 1/1 Running 0 6m43s +envoy-merged-eg-668ac7ae-775f9865d-55zhs 2/2 Running 0 3m17s +``` + +#### Testing the Configuration + +Get the name of the merged gateways Envoy service: + +```shell +export ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gatewayclass=merged-eg -o jsonpath='{.items[0].metadata.name}') +``` + +Fetch external IP of the service: + +```shell +export GATEWAY_HOST=$(kubectl get svc/${ENVOY_SERVICE} -n envoy-gateway-system -o jsonpath='{.status.loadBalancer.ingress[0].ip}') +``` + +In certain environments, the load balancer may be exposed using a hostname, instead of an IP address. If so, replace +`ip` in the above command with `hostname`. + +Curl the route hostname-route2 through Envoy proxy: + +```shell +curl --header "Host: www.merged2.com" http://$GATEWAY_HOST:8081/example2 +``` + +```shell +{ + "path": "/example2", + "host": "www.merged2.com", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/8.4.0" + ], + "X-Envoy-Internal": [ + "true" + ], + "X-Forwarded-For": [ + "172.18.0.2" + ], + "X-Forwarded-Proto": [ + "http" + ], + "X-Request-Id": [ + "deed2767-a483-4291-9429-0e256ab3a65f" + ] + }, + "namespace": "default", + "ingress": "", + "service": "", + "pod": "merged-backend-64ddb65fd7-ttv5z" +} +``` + +Curl the route hostname-route1 through Envoy proxy: + +```shell +curl --header "Host: www.merged1.com" http://$GATEWAY_HOST:8080/example +``` + +```shell +{ + "path": "/example", + "host": "www.merged1.com", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/8.4.0" + ], + "X-Envoy-Internal": [ + "true" + ], + "X-Forwarded-For": [ + "172.18.0.2" + ], + "X-Forwarded-Proto": [ + "http" + ], + "X-Request-Id": [ + "20a53440-6327-4c3c-bc8b-8e79e7311043" + ] + }, + "namespace": "default", + "ingress": "", + "service": "", + "pod": "merged-backend-64ddb65fd7-ttv5z" +} +``` + +#### Verify deployment of multiple GatewayClass + +Install the GatewayClass, Gateway, HTTPRoute and example app from [Quickstart][] example: + +```shell +kubectl apply -f https://github.com/envoyproxy/gateway/releases/download/latest/quickstart.yaml -n default +``` + +Lets create also and additional `Gateway` linked to the GatewayClass and `backend` application from Quickstart example. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify that Gateways are deployed and programmed + +```shell +kubectl get gateways -n default +``` + +```shell +NAME CLASS ADDRESS PROGRAMMED AGE +eg eg 172.18.255.203 True 114s +eg-2 eg 172.18.255.204 True 89s +merged-eg-1 merged-eg 172.18.255.202 True 8m33s +merged-eg-2 merged-eg 172.18.255.202 True 8m33s +merged-eg-3 merged-eg 172.18.255.202 True 8m33s +``` + +Verify that HTTPRoutes are deployed + +```shell +kubectl get httproute -n default +``` + +```shell +NAMESPACE NAME HOSTNAMES AGE +default backend ["www.example.com"] 2m29s +default eg-2 ["www.quickstart.example.com"] 87s +default hostname1-route ["www.merged1.com"] 10m4s +default hostname2-route ["www.merged2.com"] 10m4s +default hostname3-route ["www.merged3.com"] 10m4s +``` + +Verify that services are now deployed separately. + +```shell +kubectl get service -n envoy-gateway-system +``` + +```shell +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +envoy-default-eg-2-7e515b2f LoadBalancer 10.96.121.46 172.18.255.204 8080:32705/TCP 3m27s +envoy-default-eg-e41e7b31 LoadBalancer 10.96.11.244 172.18.255.203 80:31930/TCP 2m26s +envoy-gateway ClusterIP 10.96.141.4 18000/TCP,18001/TCP 14m25s +envoy-gateway-metrics-service ClusterIP 10.96.113.191 19001/TCP 14m25s +envoy-merged-eg-668ac7ae LoadBalancer 10.96.243.32 172.18.255.202 8082:31622/TCP,8080:32262/TCP,8081:32305/TCP 10m59s +``` + +There should be two deployments for each of newly deployed Gateway and its name should reference the name of the namespace and the Gateway. + +```shell +kubectl get pods -n envoy-gateway-system +``` + +```shell +NAME READY STATUS RESTARTS AGE +envoy-default-eg-2-7e515b2f-8c98fdf88-p6jhg 2/2 Running 0 3m27s +envoy-default-eg-e41e7b31-6f998d85d7-jpvmj 2/2 Running 0 2m26s +envoy-gateway-5d998778f6-wr6m9 1/1 Running 0 14m25s +envoy-merged-eg-668ac7ae-5958f7b7f6-9h9v2 2/2 Running 0 10m59s +``` + +#### Testing the Configuration + +Get the name of the merged gateways Envoy service: + +```shell +export DEFAULT_ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}') +``` + +Fetch external IP of the service: + +```shell +export DEFAULT_GATEWAY_HOST=$(kubectl get svc/${DEFAULT_ENVOY_SERVICE} -n envoy-gateway-system -o jsonpath='{.status.loadBalancer.ingress[0].ip}') +``` + +Curl the route Quickstart backend route through Envoy proxy: + +```shell +curl --header "Host: www.example.com" http://$DEFAULT_GATEWAY_HOST +``` + +```shell +{ + "path": "/", + "host": "www.example.com", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/8.4.0" + ], + "X-Envoy-Internal": [ + "true" + ], + "X-Forwarded-For": [ + "172.18.0.2" + ], + "X-Forwarded-Proto": [ + "http" + ], + "X-Request-Id": [ + "70a40595-67a1-4776-955b-2dee361baed7" + ] + }, + "namespace": "default", + "ingress": "", + "service": "", + "pod": "backend-96f75bbf-6w67z" +} +``` + +Curl the route hostname-route3 through Envoy proxy: + +```shell +curl --header "Host: www.merged3.com" http://$GATEWAY_HOST:8082/example3 +``` + +```shell +{ + "path": "/example3", + "host": "www.merged3.com", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/8.4.0" + ], + "X-Envoy-Internal": [ + "true" + ], + "X-Forwarded-For": [ + "172.18.0.2" + ], + "X-Forwarded-Proto": [ + "http" + ], + "X-Request-Id": [ + "47aeaef3-abb5-481a-ab92-c2ae3d0862d6" + ] + }, + "namespace": "default", + "ingress": "", + "service": "", + "pod": "merged-backend-64ddb65fd7-k84gv" +} +``` + +[Quickstart]: ../quickstart.md +[EnvoyProxy]: ../../api/extension_types#envoyproxy +[GatewayClass]: https://gateway-api.sigs.k8s.io/api-types/gatewayclass/ +[Namespaced deployment mode]: ../../api/extension_types#kuberneteswatchmode +[issue1231]: https://github.com/envoyproxy/gateway/issues/1231 +[issue1117]: https://github.com/envoyproxy/gateway/issues/1117 diff --git a/site/content/en/v1.1/tasks/operations/egctl.md b/site/content/en/v1.1/tasks/operations/egctl.md new file mode 100644 index 00000000000..ac1f13d7a61 --- /dev/null +++ b/site/content/en/v1.1/tasks/operations/egctl.md @@ -0,0 +1,908 @@ +--- +title: "Use egctl" +--- + +`egctl` is a command line tool to provide additional functionality for Envoy Gateway users. + + + +## egctl experimental translate + +This subcommand allows users to translate from an input configuration type to an output configuration type. + +The `translate` subcommand can translate Kubernetes resources to: +* Gateway API resources + This is useful in order to see how validation would occur if these resources were applied to Kubernetes. + + Use the `--to gateway-api` parameter to translate to Gateway API resources. + +* Envoy Gateway intermediate representation (IR) + This represents Envoy Gateway's translation of the Gateway API resources. + + Use the `--to ir` parameter to translate to Envoy Gateway intermediate representation. + +* Envoy Proxy xDS + This is the xDS configuration provided to Envoy Proxy. + + Use the `--to xds` parameter to translate to Envoy Proxy xDS. + + +In the below example, we will translate the Kubernetes resources (including the Gateway API resources) into xDS +resources. + +```shell +cat < Note: If CRDs are already installed, then we need to specify `--skip-crds` to avoid repeated installation of CRDs resources. + +```bash +egctl x install --name shop-backend --namespace shop +``` + + +## egctl experimental uninstall + +This subcommand can be used to uninstall envoy-gateway. + +```bash +egctl x uninstall +``` + +By default, this will only uninstall the envoy-gateway workload resource, if we want to also uninstall CRDs, we need to specify `--with-crds` + +```bash +egctl x uninstall --with-crds +``` \ No newline at end of file diff --git a/site/content/en/v1.1/tasks/quickstart.md b/site/content/en/v1.1/tasks/quickstart.md new file mode 100644 index 00000000000..4075eb5d478 --- /dev/null +++ b/site/content/en/v1.1/tasks/quickstart.md @@ -0,0 +1,156 @@ +--- +title: "Quickstart" +weight: 1 +description: Get started with Envoy Gateway in a few simple steps. +--- + +This "quick start" will help you get started with Envoy Gateway in a few simple steps. + +## Prerequisites + +A Kubernetes cluster. + +__Note:__ Refer to the [Compatibility Matrix](/news/releases/matrix) for supported Kubernetes versions. + +__Note:__ In case your Kubernetes cluster, does not have a LoadBalancer implementation, we recommend installing one +so the `Gateway` resource has an Address associated with it. We recommend using [MetalLB](https://metallb.universe.tf/installation/). + +## Installation + +Install the Gateway API CRDs and Envoy Gateway: + +```shell +helm install eg oci://docker.io/envoyproxy/gateway-helm --version v0.0.0-latest -n envoy-gateway-system --create-namespace +``` + +Wait for Envoy Gateway to become available: + +```shell +kubectl wait --timeout=5m -n envoy-gateway-system deployment/envoy-gateway --for=condition=Available +``` + +Install the GatewayClass, Gateway, HTTPRoute and example app: + +```shell +kubectl apply -f https://github.com/envoyproxy/gateway/releases/download/latest/quickstart.yaml -n default +``` + +**Note**: [`quickstart.yaml`] defines that Envoy Gateway will listen for +traffic on port 80 on its globally-routable IP address, to make it easy to use +browsers to test Envoy Gateway. When Envoy Gateway sees that its Listener is +using a privileged port (<1024), it will map this internally to an +unprivileged port, so that Envoy Gateway doesn't need additional privileges. +It's important to be aware of this mapping, since you may need to take it into +consideration when debugging. + +[`quickstart.yaml`]: https://github.com/envoyproxy/gateway/releases/download/latest/quickstart.yaml + +## Testing the Configuration + +{{< tabpane text=true >}} +{{% tab header="With External LoadBalancer Support" %}} + +You can also test the same functionality by sending traffic to the External IP. To get the external IP of the +Envoy service, run: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +In certain environments, the load balancer may be exposed using a hostname, instead of an IP address. If so, replace +`ip` in the above command with `hostname`. + +Curl the example app through Envoy proxy: + +```shell +curl --verbose --header "Host: www.example.com" http://$GATEWAY_HOST/get +``` + +{{% /tab %}} +{{% tab header="Without LoadBalancer Support" %}} + +Get the name of the Envoy service created the by the example Gateway: + +```shell +export ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}') +``` + +Port forward to the Envoy service: + +```shell +kubectl -n envoy-gateway-system port-forward service/${ENVOY_SERVICE} 8888:80 & +``` + +Curl the example app through Envoy proxy: + +```shell +curl --verbose --header "Host: www.example.com" http://localhost:8888/get +``` + +{{% /tab %}} +{{< /tabpane >}} + +## v1.1 Upgrade Notes + +Due to breaking changes in the Gateway API v1.1, some manual migration steps are required to upgrade Envoy Gateway to v1.1. + +Delete `BackendTLSPolicy` CRD (and resources): + +```shell +kubectl delete crd backendtlspolicies.gateway.networking.k8s.io +``` + +Update Gateway-API and Envoy Gateway CRDs: + +```shell +helm pull oci://docker.io/envoyproxy/gateway-helm --version v1.1.0 --untar +kubectl apply -f ./gateway-helm/crds/gatewayapi-crds.yaml +kubectl apply -f ./gateway-helm/crds/generated +``` + +Update your `BackendTLSPolicy` and `GRPCRoute` resources according to Gateway-API [v1.1 Upgrade Notes](https://gateway-api.sigs.k8s.io/guides/#v11-upgrade-notes) + +Update your Envoy Gateway xPolicy resources: remove the namespace section from targetRef. + +Install Envoy Gateway v1.1.0: + +```shell +helm upgrade eg oci://docker.io/envoyproxy/gateway-helm --version v1.1.0 -n envoy-gateway-system +``` + +## What to explore next? + +In this quickstart, you have: +- Installed Envoy Gateway +- Deployed a backend service, and a gateway +- Configured the gateway using Kubernetes Gateway API resources [Gateway](https://gateway-api.sigs.k8s.io/api-types/gateway/) and [HttpRoute](https://gateway-api.sigs.k8s.io/api-types/httproute/) to direct incoming requests over HTTP to the backend service. + +Here is a suggested list of follow-on tasks to guide you in your exploration of Envoy Gateway: + +- [HTTP Routing](traffic/http-routing) +- [Traffic Splitting](traffic/http-traffic-splitting) +- [Secure Gateways](security/secure-gateways/) +- [Global Rate Limit](traffic/global-rate-limit/) +- [gRPC Routing](traffic/grpc-routing/) + +Review the [Tasks](./) section for the scenario matching your use case. The Envoy Gateway tasks are organized by category: traffic management, security, extensibility, observability, and operations. + +## Clean-Up + +Use the steps in this section to uninstall everything from the quickstart. + +Delete the GatewayClass, Gateway, HTTPRoute and Example App: + +```shell +kubectl delete -f https://github.com/envoyproxy/gateway/releases/download/latest/quickstart.yaml --ignore-not-found=true +``` + +Delete the Gateway API CRDs and Envoy Gateway: + +```shell +helm uninstall eg -n envoy-gateway-system +``` + +## Next Steps + +Checkout the [Developer Guide](../../contributions/develop) to get involved in the project. diff --git a/site/content/en/v1.1/tasks/security/_index.md b/site/content/en/v1.1/tasks/security/_index.md new file mode 100644 index 00000000000..0e6a64144a7 --- /dev/null +++ b/site/content/en/v1.1/tasks/security/_index.md @@ -0,0 +1,5 @@ +--- +title: "Security" +weight: 2 +description: This section includes Security tasks. +--- diff --git a/site/content/en/v1.1/tasks/security/backend-mtls.md b/site/content/en/v1.1/tasks/security/backend-mtls.md new file mode 100644 index 00000000000..1d91c7a95f8 --- /dev/null +++ b/site/content/en/v1.1/tasks/security/backend-mtls.md @@ -0,0 +1,200 @@ +--- +title: "Backend Mutual TLS: Gateway to Backend" +--- + +This task demonstrates how mTLS can be achieved between the Gateway and a backend. +This task uses a self-signed CA, so it should be used for testing and demonstration purposes only. + +Envoy Gateway supports the Gateway-API defined [BackendTLSPolicy][] to establish TLS. For mTLS, the Gateway must authenticate by presenting a client certificate to the backend. + +## Prerequisites + +- OpenSSL to generate TLS assets. + +## Installation + +Follow the steps from the [Backend TLS][] to install Envoy Gateway and configure TLS to the backend server. + +## TLS Certificates + +Generate the certificates and keys used by the Gateway for authentication against the backend. + +Create a root certificate and private key to sign certificates: + +```shell +openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -subj '/O=example Inc./CN=example.com' -keyout clientca.key -out clientca.crt +``` + +Create a certificate and a private key for `www.example.com`: + +```shell +openssl req -new -newkey rsa:2048 -nodes -keyout client.key -out client.csr -subj "/CN=example-client/O=example organization" +openssl x509 -req -days 365 -CA clientca.crt -CAkey clientca.key -set_serial 0 -in client.csr -out client.crt +``` + +Store the cert/key in a Secret: + +```shell +kubectl -n envoy-gateway-system create secret tls example-client-cert --key=client.key --cert=client.crt +``` + +Store the CA Cert in another Secret: + +```shell +kubectl create configmap example-client-ca --from-file=clientca.crt +``` + +## Enforce Client Certificate Authentication on the backend + +Patch the existing quickstart backend to enforce Client Certificate Authentication. The patch will mount the server certificate and key required for TLS, and the CA certificate into the backend as volumes. + +```shell +kubectl patch deployment backend --type=json --patch ' + - op: add + path: /spec/template/spec/containers/0/volumeMounts + value: + - name: client-certs-volume + mountPath: /etc/client-certs + - name: secret-volume + mountPath: /etc/secret-volume + - op: add + path: /spec/template/spec/volumes + value: + - name: client-certs-volume + configMap: + name: example-client-ca + items: + - key: clientca.crt + path: crt + - name: secret-volume + secret: + secretName: example-cert + items: + - key: tls.crt + path: crt + - key: tls.key + path: key + - op: add + path: /spec/template/spec/containers/0/env/- + value: + name: TLS_CLIENT_CACERTS + value: /etc/client-certs/crt + ' +``` + +## Configure Envoy Proxy to use a client certificate + +In addition to enablement of backend TLS with the Gateway-API BackendTLSPolicy, Envoy Gateway supports customizing TLS parameters such as TLS Client Certificate. +To achieve this, the [EnvoyProxy][] resource can be used to specify a TLS Client Certificate. + +First, you need to add ParametersRef in GatewayClass, and refer to EnvoyProxy Config: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +## Testing mTLS + +Query the TLS-enabled backend through Envoy proxy: + +```shell +curl -v -HHost:www.example.com --resolve "www.example.com:80:127.0.0.1" \ +http://www.example.com:80/get +``` + +Inspect the output and see that the response contains the details of the TLS handshake between Envoy and the backend. +The response now contains a "peerCertificates" attribute that reflects the client certificate used by the Gateway to establish mTLS with the backend. + +```shell +< HTTP/1.1 200 OK +[...] + "tls": { + "version": "TLSv1.2", + "serverName": "www.example.com", + "negotiatedProtocol": "http/1.1", + "cipherSuite": "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" + "peerCertificates": ["-----BEGIN CERTIFICATE-----\n[...]-----END CERTIFICATE-----\n"] + } +``` + +[Backend TLS]: ./backend-tls +[BackendTLSPolicy]: https://gateway-api.sigs.k8s.io/api-types/backendtlspolicy/ +[EnvoyProxy]: ../../api/extension_types#envoyproxy \ No newline at end of file diff --git a/site/content/en/v1.1/tasks/security/backend-tls.md b/site/content/en/v1.1/tasks/security/backend-tls.md new file mode 100644 index 00000000000..53e9ccbd44a --- /dev/null +++ b/site/content/en/v1.1/tasks/security/backend-tls.md @@ -0,0 +1,390 @@ +--- +title: "Backend TLS: Gateway to Backend" +--- + +This task demonstrates how TLS can be achieved between the Gateway and a backend. +This task uses a self-signed CA, so it should be used for testing and demonstration purposes only. + +Envoy Gateway supports the Gateway-API defined [BackendTLSPolicy][]. + +## Prerequisites + +- OpenSSL to generate TLS assets. + +## Installation + +Follow the steps from the [Quickstart][] to install Envoy Gateway and the example manifest. + +## TLS Certificates + +Generate the certificates and keys used by the backend to terminate TLS connections from the Gateways. + +Create a root certificate and private key to sign certificates: + +```shell +openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -subj '/O=example Inc./CN=example.com' -keyout ca.key -out ca.crt +``` + +Create a certificate and a private key for `www.example.com`: + +```shell +openssl req -out www.example.com.csr -newkey rsa:2048 -nodes -keyout www.example.com.key -subj "/CN=www.example.com/O=example organization" -addext "subjectAltName = DNS:www.example.com" +openssl x509 -req -days 365 -CA ca.crt -CAkey ca.key -set_serial 0 -in www.example.com.csr -out www.example.com.crt +``` + +Note that the certificate must contain a DNS SAN for the relevant domain. + +Store the cert/key in a Secret: + +```shell +kubectl create secret tls example-cert --key=www.example.com.key --cert=www.example.com.crt +``` + +Store the CA Cert in another Secret: + +```shell +kubectl create configmap example-ca --from-file=ca.crt +``` + +## Setup TLS on the backend + +Patch the existing quickstart backend to enable TLS. The patch will mount the TLS certificate secret into the backend as volume. + +```shell +kubectl patch deployment backend --type=json --patch ' + - op: add + path: /spec/template/spec/containers/0/volumeMounts + value: + - name: secret-volume + mountPath: /etc/secret-volume + - op: add + path: /spec/template/spec/volumes + value: + - name: secret-volume + secret: + secretName: example-cert + items: + - key: tls.crt + path: crt + - key: tls.key + path: key + - op: add + path: /spec/template/spec/containers/0/env/- + value: + name: TLS_SERVER_CERT + value: /etc/secret-volume/crt + - op: add + path: /spec/template/spec/containers/0/env/- + value: + name: TLS_SERVER_PRIVKEY + value: /etc/secret-volume/key + ' +``` + +Create a service that exposes port 443 on the backend service. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Create a [BackendTLSPolicy][] instructing Envoy Gateway to establish a TLS connection with the backend and validate the backend certificate is issued by a trusted CA and contains an appropriate DNS SAN. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Patch the HTTPRoute's backend reference, so that it refers to the new TLS-enabled service: + +```shell +kubectl patch HTTPRoute backend --type=json --patch ' + - op: replace + path: /spec/rules/0/backendRefs/0/port + value: 443 + - op: replace + path: /spec/rules/0/backendRefs/0/name + value: tls-backend + ' +``` + +Verify the HTTPRoute status: + +```shell +kubectl get HTTPRoute backend -o yaml +``` + +## Testing backend TLS + +{{< tabpane text=true >}} +{{% tab header="With External LoadBalancer Support" %}} + +Get the External IP of the Gateway: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +Query the example app through the Gateway: + +```shell +curl -v -HHost:www.example.com --resolve "www.example.com:80:${GATEWAY_HOST}" \ +http://www.example.com:80/get +``` + +Inspect the output and see that the response contains the details of the TLS handshake between Envoy and the backend: + +```shell +< HTTP/1.1 200 OK +[...] + "tls": { + "version": "TLSv1.2", + "serverName": "www.example.com", + "negotiatedProtocol": "http/1.1", + "cipherSuite": "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" + } +``` + +{{% /tab %}} +{{% tab header="Without LoadBalancer Support" %}} + +Get the name of the Envoy service created the by the example Gateway: + +```shell +export ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}') +``` + +Port forward to the Envoy service: + +```shell +kubectl -n envoy-gateway-system port-forward service/${ENVOY_SERVICE} 80:80 & +``` + +Query the TLS-enabled backend through Envoy proxy: + +```shell +curl -v -HHost:www.example.com --resolve "www.example.com:80:127.0.0.1" \ +http://www.example.com:80/get +``` + +Inspect the output and see that the response contains the details of the TLS handshake between Envoy and the backend: + +```shell +< HTTP/1.1 200 OK +[...] + "tls": { + "version": "TLSv1.2", + "serverName": "www.example.com", + "negotiatedProtocol": "http/1.1", + "cipherSuite": "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" + } +``` + +{{% /tab %}} +{{< /tabpane >}} + +## Customize backend TLS Parameters + +In addition to enablement of backend TLS with the Gateway-API BackendTLSPolicy, Envoy Gateway supports customizing TLS parameters. +To achieve this, the [EnvoyProxy][] resource can be used to specify TLS parameters. We will customize the TLS version in this example. + +First, you need to add ParametersRef in GatewayClass, and refer to EnvoyProxy Config: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +You can customize the EnvoyProxy Backend TLS Parameters via EnvoyProxy Config like: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +## Testing TLS Parameters + +Query the TLS-enabled backend through Envoy proxy: + +```shell +curl -v -HHost:www.example.com --resolve "www.example.com:80:127.0.0.1" \ +http://www.example.com:80/get +``` + +Inspect the output and see that the response contains the details of the TLS handshake between Envoy and the backend. +The TLS version is now TLS1.3, as configured in the EnvoyProxy resource. The TLS cipher is also changed, since TLS1.3 supports different ciphers from TLS1.2. + +```shell +< HTTP/1.1 200 OK +[...] + "tls": { + "version": "TLSv1.3", + "serverName": "www.example.com", + "negotiatedProtocol": "http/1.1", + "cipherSuite": "TLS_AES_128_GCM_SHA256" + } +``` + +[Quickstart]: ../quickstart +[BackendTLSPolicy]: https://gateway-api.sigs.k8s.io/api-types/backendtlspolicy/ +[EnvoyProxy]: ../../api/extension_types#envoyproxy \ No newline at end of file diff --git a/site/content/en/v1.1/tasks/security/basic-auth.md b/site/content/en/v1.1/tasks/security/basic-auth.md new file mode 100644 index 00000000000..956963b6da5 --- /dev/null +++ b/site/content/en/v1.1/tasks/security/basic-auth.md @@ -0,0 +1,221 @@ +--- +title: "Basic Authentication" +--- + +This task provides instructions for configuring [HTTP Basic authentication][http Basic authentication]. +HTTP Basic authentication checks if an incoming request has a valid username and password before routing the request to +a backend service. + +Envoy Gateway introduces a new CRD called [SecurityPolicy][SecurityPolicy] that allows the user to configure HTTP Basic +authentication. +This instantiated resource can be linked to a [Gateway][Gateway], [HTTPRoute][HTTPRoute] or [GRPCRoute][GRPCRoute] resource. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## Configuration + +Envoy Gateway uses [.htpasswd][.htpasswd] format to store the username-password pairs for authentication. +The file must be stored in a kubernetes secret and referenced in the [SecurityPolicy][SecurityPolicy] configuration. +The secret is an Opaque secret, and the username-password pairs must be stored in the key ".htpasswd". + +### Create a root certificate + +Create a root certificate and private key to sign certificates: + +```shell +openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -subj '/O=example Inc./CN=example.com' -keyout example.com.key -out example.com.crt +``` + +### Create a certificate secret + +Create a certificate and a private key for `www.example.com`: + +```shell +openssl req -out www.example.com.csr -newkey rsa:2048 -nodes -keyout www.example.com.key -subj "/CN=www.example.com/O=example organization" +openssl x509 -req -days 365 -CA example.com.crt -CAkey example.com.key -set_serial 0 -in www.example.com.csr -out www.example.com.crt +``` + +### Create certificate + +```shell +kubectl create secret tls example-cert --key=www.example.com.key --cert=www.example.com.crt +``` + +### Enable HTTPS +Update the Gateway from the Quickstart to include an HTTPS listener that listens on port `443` and references the +`example-cert` Secret: + +```shell +kubectl patch gateway eg --type=json --patch ' + - op: add + path: /spec/listeners/- + value: + name: https + protocol: HTTPS + port: 443 + tls: + mode: Terminate + certificateRefs: + - kind: Secret + group: "" + name: example-cert + ' +``` + +### Create a .htpasswd file +First, create a [.htpasswd][.htpasswd] file with the username and password you want to use for authentication. + +Note: Please always use HTTPS with Basic Authentication. This prevents credentials from being transmitted in plain text. + +The input password won't be saved, instead, a hash will be generated and saved in the output file. When a request +tries to access protected resources, the password in the "Authorization" HTTP header will be hashed and compared with the +saved hash. + +Note: only SHA hash algorithm is supported for now. + +```shell +htpasswd -cbs .htpasswd foo bar +``` + +You can also add more users to the file: + +```shell +htpasswd -bs .htpasswd foo1 bar1 +``` + +### Create a basic-auth secret + + +Next, create a kubernetes secret with the generated .htpasswd file in the previous step. + +```shell +kubectl create secret generic basic-auth --from-file=.htpasswd +``` + +### Create a SecurityPolicy + +The below example defines a SecurityPolicy that authenticates requests against the user list in the kubernetes +secret generated in the previous step. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the SecurityPolicy configuration: + +```shell +kubectl get securitypolicy/basic-auth-example -o yaml +``` + +## Testing + +Ensure the `GATEWAY_HOST` environment variable from the [Quickstart](../../quickstart) is set. If not, follow the +Quickstart instructions to set the variable. + +```shell +echo $GATEWAY_HOST +``` + +Send a request to the backend service without `Authentication` header: + +```shell +curl -kv -H "Host: www.example.com" "https://${GATEWAY_HOST}/" +``` + +You should see `401 Unauthorized` in the response, indicating that the request is not allowed without authentication. + +```shell +* Connected to 127.0.0.1 (127.0.0.1) port 443 +... +* Server certificate: +* subject: CN=www.example.com; O=example organization +* issuer: O=example Inc.; CN=example.com +> GET / HTTP/2 +> Host: www.example.com +> User-Agent: curl/8.6.0 +> Accept: */* +... +< HTTP/2 401 +< content-length: 58 +< content-type: text/plain +< date: Wed, 06 Mar 2024 15:59:36 GMT +< + +* Connection #0 to host 127.0.0.1 left intact +User authentication failed. Missing username and password. +``` + +Send a request to the backend service with `Authentication` header: + +```shell +curl -kv -H "Host: www.example.com" -u 'foo:bar' "https://${GATEWAY_HOST}/" +``` + +The request should be allowed and you should see the response from the backend service. + +```shell + +## Clean-Up + +Follow the steps from the [Quickstart](../../quickstart) to uninstall Envoy Gateway and the example manifest. + +Delete the SecurityPolicy and the secret + +```shell +kubectl delete securitypolicy/basic-auth-example +kubectl delete secret/basic-auth +kubectl delete secret/example-cert +``` + +## Next Steps + +Checkout the [Developer Guide](../../../contributions/develop) to get involved in the project. + +[SecurityPolicy]: ../../../contributions/design/security-policy +[http Basic authentication]: https://tools.ietf.org/html/rfc2617 +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute +[GRPCRoute]: https://gateway-api.sigs.k8s.io/api-types/grpcroute +[.htpasswd]: https://httpd.apache.org/docs/current/programs/htpasswd.html diff --git a/site/content/en/v1.1/tasks/security/cors.md b/site/content/en/v1.1/tasks/security/cors.md new file mode 100644 index 00000000000..cfbe979cd22 --- /dev/null +++ b/site/content/en/v1.1/tasks/security/cors.md @@ -0,0 +1,177 @@ +--- +title: "CORS" +--- + +This task provides instructions for configuring [Cross-Origin Resource Sharing (CORS)][cors] on Envoy Gateway. +CORS defines a way for client web applications that are loaded in one domain to interact with resources in a different +domain. + +Envoy Gateway introduces a new CRD called [SecurityPolicy][SecurityPolicy] that allows the user to configure CORS. +This instantiated resource can be linked to a [Gateway][Gateway], [HTTPRoute][HTTPRoute] or [GRPCRoute][GRPCRoute] resource. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## Configuration + +When configuring CORS either an origin with a precise hostname can be configured or an hostname containing a wildcard prefix, +allowing all subdomains of the specified hostname. +In addition to that the entire origin (with or without specifying a scheme) can be a wildcard to allow all origins. + +The below example defines a SecurityPolicy that allows CORS for all HTTP requests originating from `www.foo.com`. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the SecurityPolicy configuration: + +```shell +kubectl get securitypolicy/cors-example -o yaml +``` + +## Testing + +Ensure the `GATEWAY_HOST` environment variable from the [Quickstart](../../quickstart) is set. If not, follow the +Quickstart instructions to set the variable. + +```shell +echo $GATEWAY_HOST +``` + +Verify that the CORS headers are present in the response of the OPTIONS request from `http://www.foo.com`: + +```shell +curl -H "Origin: http://www.foo.com" \ + -H "Host: www.example.com" \ + -H "Access-Control-Request-Method: GET" \ + -X OPTIONS -v -s \ + http://$GATEWAY_HOST \ + 1> /dev/null +``` + +You should see the below response, indicating that the request from `http://www.foo.com` is allowed: + +```shell +< access-control-allow-origin: http://www.foo.com +< access-control-allow-methods: GET, POST +< access-control-allow-headers: x-header-1, x-header-2 +< access-control-max-age: 86400 +< access-control-expose-headers: x-header-3, x-header-4 +``` + +If you try to send a request from `http://www.bar.com`, you should see the below response: + +```shell +curl -H "Origin: http://www.bar.com" \ + -H "Host: www.example.com" \ + -H "Access-Control-Request-Method: GET" \ + -X OPTIONS -v -s \ + http://$GATEWAY_HOST \ + 1> /dev/null +``` + +You won't see any CORS headers in the response, indicating that the request from `http://www.bar.com` was not allowed. + +If you try to send a request from `http://www.foo.com:8080`, you should also see similar response because the port number +`8080` is not included in the allowed origins. + +```shell +```shell +curl -H "Origin: http://www.foo.com:8080" \ + -H "Host: www.example.com" \ + -H "Access-Control-Request-Method: GET" \ + -X OPTIONS -v -s \ + http://$GATEWAY_HOST \ + 1> /dev/null +``` + +Note: +* CORS specification requires that the browsers to send a preflight request to the server to ask if it's allowed +to access the limited resource in another domains. The browsers are supposed to follow the response from the server to +determine whether to send the actual request or not. The CORS filter only response to the preflight requests according to +its configuration. It won't deny any requests. The browsers are responsible for enforcing the CORS policy. +* The targeted HTTPRoute or the HTTPRoutes that the targeted Gateway routes to must allow the OPTIONS method for the CORS +filter to work. Otherwise, the OPTIONS request won't match the routes and the CORS filter won't be invoked. + + +## Clean-Up + +Follow the steps from the [Quickstart](../../quickstart) to uninstall Envoy Gateway and the example manifest. + +Delete the SecurityPolicy: + +```shell +kubectl delete securitypolicy/cors-example +``` + +## Next Steps + +Checkout the [Developer Guide](../../../contributions/develop) to get involved in the project. + +[SecurityPolicy]: ../../../contributions/design/security-policy +[cors]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute +[GRPCRoute]: https://gateway-api.sigs.k8s.io/api-types/grpcroute diff --git a/site/content/en/v1.1/tasks/security/ext-auth.md b/site/content/en/v1.1/tasks/security/ext-auth.md new file mode 100644 index 00000000000..5fc73321106 --- /dev/null +++ b/site/content/en/v1.1/tasks/security/ext-auth.md @@ -0,0 +1,460 @@ +--- +title: "External Authorization" +--- + +This task provides instructions for configuring external authentication. + +External authorization calls an external HTTP or gRPC service to check whether an incoming HTTP request is authorized +or not. If the request is deemed unauthorized, then the request will be denied with a 403 (Forbidden) response. If the +request is authorized, then the request will be allowed to proceed to the backend service. + +Envoy Gateway introduces a new CRD called [SecurityPolicy][SecurityPolicy] that allows the user to configure external authorization. +This instantiated resource can be linked to a [Gateway][Gateway] and [HTTPRoute][HTTPRoute] resource. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +Verify the Gateway status: + +```shell +kubectl get gateway/eg -o yaml +``` + +## HTTP External Authorization Service + +### Installation + +Install a demo HTTP service that will be used as the external authorization service: + +```shell +kubectl apply -f https://raw.githubusercontent.com/envoyproxy/gateway/latest/examples/kubernetes/ext-auth-http-service.yaml +``` + +Create a new HTTPRoute resource to route traffic on the path `/myapp` to the backend service. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the HTTPRoute status: + +```shell +kubectl get httproute/myapp -o yaml +``` + +### Configuration + +Create a new SecurityPolicy resource to configure the external authorization. This SecurityPolicy targets the HTTPRoute +"myApp" created in the previous step. It calls the HTTP external authorization service "http-ext-auth" on port 9002 for +authorization. The `headersToBackend` field specifies the headers that will be sent to the backend service if the request +is successfully authorized. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the SecurityPolicy configuration: + +```shell +kubectl get securitypolicy/ext-auth-example -o yaml +``` + +### Testing + +Ensure the `GATEWAY_HOST` environment variable from the [Quickstart](../../quickstart) is set. If not, follow the +Quickstart instructions to set the variable. + +```shell +echo $GATEWAY_HOST +``` + +Send a request to the backend service without `Authentication` header: + +```shell +curl -v -H "Host: www.example.com" "http://${GATEWAY_HOST}/myapp" +``` + +You should see `403 Forbidden` in the response, indicating that the request is not allowed without authentication. + +```shell +* Connected to 172.18.255.200 (172.18.255.200) port 80 (#0) +> GET /myapp HTTP/1.1 +> Host: www.example.com +> User-Agent: curl/7.68.0 +> Accept: */* +... +< HTTP/1.1 403 Forbidden +< date: Mon, 11 Mar 2024 03:41:15 GMT +< x-envoy-upstream-service-time: 0 +< content-length: 0 +< +* Connection #0 to host 172.18.255.200 left intact +``` + +Send a request to the backend service with `Authentication` header: + +```shell +curl -v -H "Host: www.example.com" -H "Authorization: Bearer token1" "http://${GATEWAY_HOST}/myapp" +``` + +The request should be allowed and you should see the response from the backend service. +Because the `x-current-user` header from the auth response has been sent to the backend service, +you should see the `x-current-user` header in the response. + +``` +"X-Current-User": [ + "user1" + ], +``` + +## GRPC External Authorization Service + +### Installation + +Install a demo gRPC service that will be used as the external authorization service. The demo gRPC service is enabled +with TLS and a BackendTLSConfig is created to configure the communication between the Envoy proxy and the gRPC service. + +Note: TLS is optional for HTTP or gRPC external authorization services. However, enabling TLS is recommended for enhanced +security in production environments. + +```shell +kubectl apply -f https://raw.githubusercontent.com/envoyproxy/gateway/latest/examples/kubernetes/ext-auth-grpc-service.yaml +``` + +The HTTPRoute created in the previous section is still valid and can be used with the gRPC auth service, but if you have +not created the HTTPRoute, you can create it now. + +Create a new HTTPRoute resource to route traffic on the path `/myapp` to the backend service. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the HTTPRoute status: + +```shell +kubectl get httproute/myapp -o yaml +``` + +### Configuration + +Update the SecurityPolicy that was created in the previous section to use the gRPC external authorization service. +It calls the gRPC external authorization service "grpc-ext-auth" on port 9002 for authorization. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the SecurityPolicy configuration: + +```shell +kubectl get securitypolicy/ext-auth-example -o yaml +``` + +Because the gRPC external authorization service is enabled with TLS, a BackendTLSConfig needs to be created to configure +the communication between the Envoy proxy and the gRPC auth service. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the BackendTLSPolicy configuration: + +```shell +kubectl get backendtlspolicy/grpc-ext-auth-btls -o yaml +``` + +### Testing + +Ensure the `GATEWAY_HOST` environment variable from the [Quickstart](../../quickstart) is set. If not, follow the +Quickstart instructions to set the variable. + +```shell +echo $GATEWAY_HOST +``` + +Send a request to the backend service without `Authentication` header: + +```shell +curl -v -H "Host: www.example.com" "http://${GATEWAY_HOST}/myapp" +``` + +You should see `403 Forbidden` in the response, indicating that the request is not allowed without authentication. + +```shell +* Connected to 172.18.255.200 (172.18.255.200) port 80 (#0) +> GET /myapp HTTP/1.1 +> Host: www.example.com +> User-Agent: curl/7.68.0 +> Accept: */* +... +< HTTP/1.1 403 Forbidden +< date: Mon, 11 Mar 2024 03:41:15 GMT +< x-envoy-upstream-service-time: 0 +< content-length: 0 +< +* Connection #0 to host 172.18.255.200 left intact +``` + +Send a request to the backend service with `Authentication` header: + +```shell +curl -v -H "Host: www.example.com" -H "Authorization: Bearer token1" "http://${GATEWAY_HOST}/myapp" +``` + +## Clean-Up + +Follow the steps from the [Quickstart](../../quickstart) to uninstall Envoy Gateway and the example manifest. + +Delete the demo auth services, HTTPRoute, SecurityPolicy and BackendTLSPolicy: + +```shell +kubectl delete -f https://raw.githubusercontent.com/envoyproxy/gateway/latest/examples/kubernetes/ext-auth-http-service.yaml +kubectl delete -f https://raw.githubusercontent.com/envoyproxy/gateway/latest/examples/kubernetes/ext-auth-grpc-service.yaml +kubectl delete httproute/myapp +kubectl delete securitypolicy/ext-auth-example +kubectl delete backendtlspolicy/grpc-ext-auth-btls +``` + +## Next Steps + +Checkout the [Developer Guide](../../../contributions/develop) to get involved in the project. + +[SecurityPolicy]: ../../../contributions/design/security-policy +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute diff --git a/site/content/en/v1.1/tasks/security/jwt-authentication.md b/site/content/en/v1.1/tasks/security/jwt-authentication.md new file mode 100644 index 00000000000..8b160403882 --- /dev/null +++ b/site/content/en/v1.1/tasks/security/jwt-authentication.md @@ -0,0 +1,170 @@ +--- +title: "JWT Authentication" +--- + +This task provides instructions for configuring [JSON Web Token (JWT)][jwt] authentication. JWT authentication checks +if an incoming request has a valid JWT before routing the request to a backend service. Currently, Envoy Gateway only +supports validating a JWT from an HTTP header, e.g. `Authorization: Bearer `. + +Envoy Gateway introduces a new CRD called [SecurityPolicy][SecurityPolicy] that allows the user to configure JWT authentication. +This instantiated resource can be linked to a [Gateway][Gateway], [HTTPRoute][HTTPRoute] or [GRPCRoute][GRPCRoute] resource. + +## Prerequisites + +Follow the steps from the [Quickstart](../quickstart) to install Envoy Gateway and the example manifest. +For GRPC - follow the steps from the [GRPC Routing](../traffic/grpc-routing) example. +Before proceeding, you should be able to query the example backend using HTTP or GRPC. + +## Configuration + +Allow requests with a valid JWT by creating an [SecurityPolicy][SecurityPolicy] and attaching it to the example +HTTPRoute or GRPCRoute. + +### HTTPRoute + +```shell +kubectl apply -f https://raw.githubusercontent.com/envoyproxy/gateway/latest/examples/kubernetes/jwt/jwt.yaml +``` + +Two HTTPRoute has been created, one for `/foo` and another for `/bar`. A SecurityPolicy has been created and targeted +HTTPRoute foo to authenticate requests for `/foo`. The HTTPRoute bar is not targeted by the SecurityPolicy and will allow +unauthenticated requests to `/bar`. + +Verify the HTTPRoute configuration and status: + +```shell +kubectl get httproute/foo -o yaml +kubectl get httproute/bar -o yaml +``` + +The SecurityPolicy is configured for JWT authentication and uses a single [JSON Web Key Set (JWKS)][jwks] +provider for authenticating the JWT. + +Verify the SecurityPolicy configuration: + +```shell +kubectl get securitypolicy/jwt-example -o yaml +``` + +### GRPCRoute + +```shell +kubectl apply -f https://raw.githubusercontent.com/envoyproxy/gateway/latest/examples/kubernetes/jwt/grpc-jwt.yaml +``` + +A SecurityPolicy has been created and targeted GRPCRoute yages to authenticate all requests for `yages` service.. + +Verify the GRPCRoute configuration and status: + +```shell +kubectl get grpcroute/yages -o yaml +``` + +The SecurityPolicy is configured for JWT authentication and uses a single [JSON Web Key Set (JWKS)][jwks] +provider for authenticating the JWT. + +Verify the SecurityPolicy configuration: + +```shell +kubectl get securitypolicy/jwt-example -o yaml +``` + +## Testing + +Ensure the `GATEWAY_HOST` environment variable from the [Quickstart](../../quickstart) is set. If not, follow the +Quickstart instructions to set the variable. + +```shell +echo $GATEWAY_HOST +``` + +### HTTPRoute + +Verify that requests to `/foo` are denied without a JWT: + +```shell +curl -sS -o /dev/null -H "Host: www.example.com" -w "%{http_code}\n" http://$GATEWAY_HOST/foo +``` + +A `401` HTTP response code should be returned. + +Get the JWT used for testing request authentication: + +```shell +TOKEN=$(curl https://raw.githubusercontent.com/envoyproxy/gateway/main/examples/kubernetes/jwt/test.jwt -s) && echo "$TOKEN" | cut -d '.' -f2 - | base64 --decode - +``` + +__Note:__ The above command decodes and returns the token's payload. You can replace `f2` with `f1` to view the token's +header. + +Verify that a request to `/foo` with a valid JWT is allowed: + +```shell +curl -sS -o /dev/null -H "Host: www.example.com" -H "Authorization: Bearer $TOKEN" -w "%{http_code}\n" http://$GATEWAY_HOST/foo +``` + +A `200` HTTP response code should be returned. + +Verify that requests to `/bar` are allowed __without__ a JWT: + +```shell +curl -sS -o /dev/null -H "Host: www.example.com" -w "%{http_code}\n" http://$GATEWAY_HOST/bar +``` + +### GRPCRoute + +Verify that requests to `yages`service are denied without a JWT: + +```shell +grpcurl -plaintext -authority=grpc-example.com ${GATEWAY_HOST}:80 yages.Echo/Ping +``` + +You should see the below response + +```shell +Error invoking method "yages.Echo/Ping": rpc error: code = Unauthenticated desc = failed to query for service descriptor "yages.Echo": Jwt is missing +``` + +Get the JWT used for testing request authentication: + +```shell +TOKEN=$(curl https://raw.githubusercontent.com/envoyproxy/gateway/main/examples/kubernetes/jwt/test.jwt -s) && echo "$TOKEN" | cut -d '.' -f2 - | base64 --decode - +``` + +__Note:__ The above command decodes and returns the token's payload. You can replace `f2` with `f1` to view the token's +header. + +Verify that a request to `yages` service with a valid JWT is allowed: + +```shell +grpcurl -plaintext -H "authorization: Bearer $TOKEN" -authority=grpc-example.com ${GATEWAY_HOST}:80 yages.Echo/Ping +``` + +You should see the below response + +```shell +{ + "text": "pong" +} +``` + +## Clean-Up + +Follow the steps from the [Quickstart](../../quickstart) to uninstall Envoy Gateway and the example manifest. + +Delete the SecurityPolicy: + +```shell +kubectl delete securitypolicy/jwt-example +``` + +## Next Steps + +Checkout the [Developer Guide](../../../contributions/develop) to get involved in the project. + +[SecurityPolicy]: ../../../contributions/design/security-policy +[jwt]: https://tools.ietf.org/html/rfc7519 +[jwks]: https://tools.ietf.org/html/rfc7517 +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute +[GRPCRoute]: https://gateway-api.sigs.k8s.io/api-types/grpcroute diff --git a/site/content/en/v1.1/tasks/security/mutual-tls.md b/site/content/en/v1.1/tasks/security/mutual-tls.md new file mode 100644 index 00000000000..64f471ba19d --- /dev/null +++ b/site/content/en/v1.1/tasks/security/mutual-tls.md @@ -0,0 +1,186 @@ +--- +title: "Mutual TLS: External Clients to the Gateway" +--- + +This task demonstrates how mutual TLS can be achieved between external clients and the Gateway. +This task uses a self-signed CA, so it should be used for testing and demonstration purposes only. + +## Prerequisites + +- OpenSSL to generate TLS assets. + +## Installation + +Follow the steps from the [Quickstart](../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## TLS Certificates + +Generate the certificates and keys used by the Gateway to terminate client TLS connections. + +Create a root certificate and private key to sign certificates: + +```shell +openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -subj '/O=example Inc./CN=example.com' -keyout example.com.key -out example.com.crt +``` + +Create a certificate and a private key for `www.example.com`: + +```shell +openssl req -out www.example.com.csr -newkey rsa:2048 -nodes -keyout www.example.com.key -subj "/CN=www.example.com/O=example organization" +openssl x509 -req -days 365 -CA example.com.crt -CAkey example.com.key -set_serial 0 -in www.example.com.csr -out www.example.com.crt +``` + +Store the cert/key in a Secret: + +```shell +kubectl create secret tls example-cert --key=www.example.com.key --cert=www.example.com.crt --certificate-authority=example.com.crt +``` + +Store the CA Cert in another Secret: + +```shell +kubectl create secret generic example-ca-cert --from-file=ca.crt=example.com.crt +``` + +Create a certificate and a private key for the client `client.example.com`: + +```shell +openssl req -out client.example.com.csr -newkey rsa:2048 -nodes -keyout client.example.com.key -subj "/CN=client.example.com/O=example organization" +openssl x509 -req -days 365 -CA example.com.crt -CAkey example.com.key -set_serial 0 -in client.example.com.csr -out client.example.com.crt +``` + +Update the Gateway from the Quickstart to include an HTTPS listener that listens on port `443` and references the +`example-cert` Secret: + +```shell +kubectl patch gateway eg --type=json --patch ' + - op: add + path: /spec/listeners/- + value: + name: https + protocol: HTTPS + port: 443 + tls: + mode: Terminate + certificateRefs: + - kind: Secret + group: "" + name: example-cert + ' +``` + +Verify the Gateway status: + +```shell +kubectl get gateway/eg -o yaml +``` + +Create a [ClientTrafficPolicy][] to enforce client validation using the CA Certificate as a trusted anchor. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +## Testing + +{{< tabpane text=true >}} +{{% tab header="With External LoadBalancer Support" %}} + +Get the External IP of the Gateway: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +Query the example app through the Gateway: + +```shell +curl -v -HHost:www.example.com --resolve "www.example.com:443:${GATEWAY_HOST}" \ +--cert client.example.com.crt --key client.example.com.key \ +--cacert example.com.crt https://www.example.com/get +``` + +Don't specify the client key and certificate in the above command, and ensure that the connection fails: + +```shell +curl -v -HHost:www.example.com --resolve "www.example.com:443:${GATEWAY_HOST}" \ +--cacert example.com.crt https://www.example.com/get +``` + +{{% /tab %}} +{{% tab header="Without LoadBalancer Support" %}} + +Get the name of the Envoy service created the by the example Gateway: + +```shell +export ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}') +``` + +Port forward to the Envoy service: + +```shell +kubectl -n envoy-gateway-system port-forward service/${ENVOY_SERVICE} 8443:443 & +``` + +Query the example app through Envoy proxy: + +```shell +curl -v -HHost:www.example.com --resolve "www.example.com:8443:127.0.0.1" \ +--cert client.example.com.crt --key client.example.com.key \ +--cacert example.com.crt https://www.example.com:8443/get +``` + +{{% /tab %}} +{{< /tabpane >}} + +[ClientTrafficPolicy]: ../../../api/extension_types#clienttrafficpolicy diff --git a/site/content/en/v1.1/tasks/security/oidc.md b/site/content/en/v1.1/tasks/security/oidc.md new file mode 100644 index 00000000000..ac7d6d60ba9 --- /dev/null +++ b/site/content/en/v1.1/tasks/security/oidc.md @@ -0,0 +1,322 @@ +--- +title: "OIDC Authentication" +--- + +This task provides instructions for configuring [OpenID Connect (OIDC)][oidc] authentication. +OpenID Connect (OIDC) is an authentication standard built on top of OAuth 2.0. +It enables EG to rely on authentication that is performed by an OpenID Connect Provider (OP) +to verify the identity of a user. + +Envoy Gateway introduces a new CRD called [SecurityPolicy][SecurityPolicy] that allows the user to configure OIDC +authentication. +This instantiated resource can be linked to a [Gateway][Gateway] and [HTTPRoute][HTTPRoute] resource. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +EG OIDC authentication requires the redirect URL to be HTTPS. Follow the [Secure Gateways](../secure-gateways) guide +to generate the TLS certificates and update the Gateway configuration to add an HTTPS listener. + +Verify the Gateway status: + +```shell +kubectl get gateway/eg -o yaml +``` + +Let's create an HTTPRoute that represents an application protected by OIDC. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the HTTPRoute status: + +```shell +kubectl get httproute/myapp -o yaml +``` + +## OIDC Authentication for a HTTPRoute + +OIDC can be configured at the Gateway level to authenticate all the HTTPRoutes that are associated with the Gateway with +the same OIDC configuration, or at the HTTPRoute level to authenticate each HTTPRoute with different OIDC configurations. + +This section demonstrates how to configure OIDC authentication for a specific HTTPRoute. + +### Register an OIDC application + +This task uses Google as the OIDC provider to demonstrate the configuration of OIDC. However, EG works with any OIDC +providers, including Auth0, Azure AD, Keycloak, Okta, OneLogin, Salesforce, UAA, etc. + +Follow the steps in the [Google OIDC documentation][google-oidc] to register an OIDC application. Please make sure the +redirect URL is set to the one you configured in the SecurityPolicy that you will create in the step below. In this example, +the redirect URL is `http://www.example.com:8443/myapp/oauth2/callback`. + +After registering the application, you should have the following information: +* Client ID: The client ID of the OIDC application. +* Client Secret: The client secret of the OIDC application. + +### Create a kubernetes secret + +Next, create a kubernetes secret with the Client Secret created in the previous step. The secret is an Opaque secret, +and the Client Secret must be stored in the key "client-secret". + +Note: please replace the ${CLIENT_SECRET} with the actual Client Secret that you got from the previous step. + +```shell +kubectl create secret generic my-app-client-secret --from-literal=client-secret=${CLIENT_SECRET} +``` + +### Create a SecurityPolicy + +**Please notice that the `redirectURL` and `logoutPath` must match the target HTTPRoute.** In this example, the target +HTTPRoute is configured to match the host `www.example.com` and the path `/myapp`, so the `redirectURL` must be prefixed +with `https://www.example.com:8443/myapp`, and `logoutPath` must be prefixed with`/myapp`, otherwise the OIDC authentication +will fail because the redirect and logout requests will not match the target HTTPRoute and therefore can't be processed +by the OAuth2 filter on that HTTPRoute. + +Note: please replace the ${CLIENT_ID} in the below yaml snippet with the actual Client ID that you got from the OIDC provider. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the SecurityPolicy configuration: + +```shell +kubectl get securitypolicy/oidc-example -o yaml +``` + +### Testing + +Port forward gateway port to localhost: + +```shell +export ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}') + +kubectl -n envoy-gateway-system port-forward service/${ENVOY_SERVICE} 8443:443 +``` + +Put www.example.com in the /etc/hosts file in your test machine, so we can use this host name to access the gateway from a browser: + +```shell +... +127.0.0.1 www.example.com +``` + +Open a browser and navigate to the `https://www.example.com:8443/myapp` address. You should be redirected to the Google +login page. After you successfully login, you should see the response from the backend service. + +Clean the cookies in the browser and try to access `https://www.example.com:8443/foo` address. You should be able to see +this page since the path `/foo` is not protected by the OIDC policy. + +## OIDC Authentication for a Gateway + +OIDC can be configured at the Gateway level to authenticate all the HTTPRoutes that are associated with the Gateway with +the same OIDC configuration, or at the HTTPRoute level to authenticate each HTTPRoute with different OIDC configurations. + +This section demonstrates how to configure OIDC authentication for a Gateway. + +### Register an OIDC application + +If you haven't registered an OIDC application, follow the steps in the previous section to register an OIDC application. + +### Create a kubernetes secret + +If you haven't created a kubernetes secret, follow the steps in the previous section to create a kubernetes secret. + +### Create a SecurityPolicy + +Create or update the SecurityPolicy to target the Gateway instead of the HTTPRoute. **Please notice that the `redirectURL` +and `logoutPath` must match one of the HTTPRoutes associated with the Gateway.** In this example, the target Gateway has +two HTTPRoutes associated with it, one with the host `www.example.com` and the path `/myapp`, and the other with the host +`www.example.com` and the path `/`. Either one of the HTTPRoutes can be used to match the `redirectURL` and `logoutPath`. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the SecurityPolicy configuration: + +```shell +kubectl get securitypolicy/oidc-example -o yaml +``` + +### Testing + +If you haven't done so, follow the steps in the previous section to port forward gateway port to localhost and put +www.example.com in the /etc/hosts file in your test machine. + +Open a browser and navigate to the `https://www.example.com:8443/foo` address. You should be redirected to the Google +login page. After you successfully login, you should see the response from the backend service. + +You can also try to access `https://www.example.com:8443/myapp` address. You should be able to see this page since the +path `/myapp` is protected by the same OIDC policy. + +## Clean-Up + +Follow the steps from the [Quickstart](../../quickstart) to uninstall Envoy Gateway and the example manifest. + +Delete the SecurityPolicy, the secret and the HTTPRoute: + +```shell +kubectl delete securitypolicy/oidc-example +kubectl delete secret/my-app-client-secret +kubectl delete httproute/myapp +``` + +## Next Steps + +Checkout the [Developer Guide](../../../../contributions/develop) to get involved in the project. + +[oidc]: https://openid.net/connect/ +[google-oidc]: https://developers.google.com/identity/protocols/oauth2/openid-connect +[SecurityPolicy]: ../../../../contributions/design/security-policy +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute diff --git a/site/content/en/v1.1/tasks/security/private-key-provider.md b/site/content/en/v1.1/tasks/security/private-key-provider.md new file mode 100644 index 00000000000..cf40a96e9e1 --- /dev/null +++ b/site/content/en/v1.1/tasks/security/private-key-provider.md @@ -0,0 +1,621 @@ +--- +title: "Accelerating TLS Handshakes using Private Key Provider in Envoy" +--- + +TLS operations can be accelerated or the private key can be protected using specialized hardware. This can be leveraged in Envoy using [Envoy Private Key Provider](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/transport_sockets/tls/v3/common.proto#extensions-transport-sockets-tls-v3-privatekeyprovider) is added to Envoy. + +Today, there are two private key providers implemented in Envoy as contrib extensions: +- [QAT in Envoy 1.24 release](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/private_key_providers/qat/v3alpha/qat.proto#extensions-private-key-providers-qat-v3alpha-qatprivatekeymethodconfig) +- [CryptoMB in Envoy 1.20 release](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/private_key_providers/cryptomb/v3alpha/cryptomb.proto ) + +Both of them are used to accelerate the TLS handshake through the hardware capabilities. + +This task will walk you through the steps required to configure TLS Termination mode for TCP traffic while also using the Envoy Private Key Provider to accelerate the TLS handshake by leveraging QAT and the HW accelerator available on Intel SPR/EMR Xeon server platforms. + +## Prerequisites + +### For QAT + +- Install Linux kernel 5.17 or similar +- Ensure the node has QAT devices by checking the QAT physical function devices presented. [Supported Devices](https://intel.github.io/quickassist/qatlib/requirements.html#qat2-0-qatlib-supported-devices) + + ```shell + echo `(lspci -d 8086:4940 && lspci -d 8086:4941 && lspci -d 8086:4942 && lspci -d 8086:4943 && lspci -d 8086:4946 && lspci -d 8086:4947) | wc -l` supported devices found. + ``` + +- Enable IOMMU from BIOS +- Enable IOMMU for Linux kernel + + Figure out the QAT VF device id + + ```shell + lspci -d 8086:4941 && lspci -d 8086:4943 && lspci -d 8086:4947 + ``` + + Attach the QAT device to vfio-pci through kernel parameter by the device id gotten from previous command. + + ```shell + cat /etc/default/grub: + GRUB_CMDLINE_LINUX="intel_iommu=on vfio-pci.ids=[QAT device id]" + update-grub + reboot + ```` + + Once the system is rebooted, check if the IOMMU has been enabled via the following command: + + ```shell + dmesg| grep IOMMU + [ 1.528237] DMAR: IOMMU enabled + ``` + +- Enable virtual function devices for QAT device + + ```shell + modprobe vfio_pci + rmmod qat_4xxx + modprobe qat_4xxx + qat_device=$(lspci -D -d :[QAT device id] | awk '{print $1}') + for i in $qat_device; do echo 16|sudo tee /sys/bus/pci/devices/$i/sriov_numvfs; done + chmod a+rw /dev/vfio/* + ``` + +- Increase the container runtime memory lock limit (using the containerd as example here) + + ```shell + mkdir /etc/systemd/system/containerd.service.d + cat <>/etc/systemd/system/containerd.service.d/memlock.conf + [Service] + LimitMEMLOCK=134217728 + EOF + ``` + + Restart the container runtime (for containerd, CRIO has similar concept) + + ```shell + systemctl daemon-reload + systemctl restart containerd + ``` + +- Install [Intel® QAT Device Plugin for Kubernetes](https://github.com/intel/intel-device-plugins-for-kubernetes) + + ```shell + kubectl apply -k 'https://github.com/intel/intel-device-plugins-for-kubernetes/deployments/qat_plugin?ref=main' + ``` + + Verification of the plugin deployment and detection of QAT hardware can be confirmed by examining the resource allocations on the nodes: + + ```shell + kubectl get node -o yaml| grep qat.intel.com + ``` + +### For CryptoMB: + +It required the node with 3rd generation Intel Xeon Scalable processor server processors, or later. +- For kubernetes Cluster, if not all nodes that support Intel® AVX-512 in Kubernetes cluster, you need to add some labels to divide these two kinds of nodes manually or using [NFD](https://github.com/kubernetes-sigs/node-feature-discovery). + + ```shell + kubectl apply -k https://github.com/kubernetes-sigs/node-feature-discovery/deployment/overlays/default?ref=v0.15.1 + ``` + +- Checking the available nodes with required cpu instructions: + - Check the node labels if using [NFD](https://github.com/kubernetes-sigs/node-feature-discovery): + + ```shell + kubectl get nodes -l feature.node.kubernetes.io/cpu-cpuid.AVX512F,feature.node.kubernetes.io/cpu-cpuid.AVX512DQ,feature.node.kubernetes.io/cpu-cpuid.AVX512BW,feature.node.kubernetes.io/cpu-cpuid.AVX512VBMI2,feature.node.kubernetes.io/cpu-cpuid.AVX512IFMA + ``` + + - Check CPUIDS manually on the node if without using NFD: + + ```shell + cat /proc/cpuinfo |grep avx512f|grep avx512dq|grep avx512bw|grep avx512_vbmi2|grep avx512ifma + ``` + +## Installation + +* Follow the steps from the [Quickstart](../quickstart) to install Envoy Gateway. + +* Enable the EnvoyPatchPolicy feature, which will allow us to directly configure the Private Key Provider Envoy Filter, since Envoy Gateway does not directly expose this functionality. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +* After updating the `ConfigMap`, you will need to restart the `envoy-gateway` deployment so the configuration kicks in + + ```shell + kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system + ``` + +## Create gateway for TLS termination + +* Follow the instructions in [TLS Termination for TCP](./tls-termination) to setup a TCP gateway to terminate the TLS connection. + +* Update GatewayClass for using the envoyproxy image with contrib extensions and requests required resources. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +### Change EnvoyProxy configuration for QAT + +Using the envoyproxy image with contrib extensions and add qat resources requesting, ensure the k8s scheduler find out a machine with required resource. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +### Change EnvoyProxy configuration for CryptoMB + +Using the envoyproxy image with contrib extensions and add the node affinity to scheduling the Envoy Gateway pod on the machine with required CPU instructions. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Or using `preferredDuringSchedulingIgnoredDuringExecution` for best effort scheduling, or not doing any node affinity, just doing the random scheduling. The CryptoMB private key provider supports software fallback if the required CPU instructions aren't here. + +## Apply EnvoyPatchPolicy to enable private key provider + +### Benchmark before enabling private key provider + +First follow the instructions in [TLS Termination for TCP](./tls-termination) to do the functionality test. + +Ensure the cpu frequency governor set as `performance`. + +```shell +export NUM_CPUS=`lscpu | grep "^CPU(s):"|awk '{print $2}'` +for i in `seq 0 1 $NUM_CPUS`; do sudo cpufreq-set -c $i -g performance; done +``` + +Using the nodeport as the example, fetch the node port from envoy gateway service. + +```shell +export ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}') +export NODE_PORT=$(kubectl -n envoy-gateway-system get svc/$ENVOY_SERVICE -o jsonpath='{.spec.ports[0].nodePort}') +``` + +```shell +echo "127.0.0.1 www.example.com" >> /etc/hosts +``` + +Benchmark the gateway with fortio. + +```shell +fortio load -c 10 -k -qps 0 -t 30s -keepalive=false https://www.example.com:${NODE_PORT} +``` + +### For QAT + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +### For CryptoMB + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +### Benchmark after enabling private key provider + +First follow the instructions in [TLS Termination for TCP](./tls-termination) to do the functionality test again. + +Benchmark the gateway with fortio. + +```shell +fortio load -c 64 -k -qps 0 -t 30s -keepalive=false https://www.example.com:${NODE_PORT} +``` + +You will see a performance boost after private key provider enabled. For example, you will get results as below. + +Without private key provider: + +```shell +All done 43069 calls (plus 10 warmup) 6.966 ms avg, 1435.4 qps +``` + +With CryptoMB private key provider, the QPS is over 2 times than without private key provider. + +```shell +All done 93983 calls (plus 128 warmup) 40.880 ms avg, 3130.5 qps +``` + +With QAT private key provider, the QPS is over 3 times than without private key provider + +```shell +All done 134746 calls (plus 128 warmup) 28.505 ms avg, 4489.6 qps +``` diff --git a/site/content/en/v1.1/tasks/security/restrict-ip-access.md b/site/content/en/v1.1/tasks/security/restrict-ip-access.md new file mode 100644 index 00000000000..ba6af118252 --- /dev/null +++ b/site/content/en/v1.1/tasks/security/restrict-ip-access.md @@ -0,0 +1,197 @@ +--- +title: "IP Allowlist/Denylist" +--- + +This task provides instructions for configuring IP allowlist/denylist on Envoy Gateway. IP allowlist/denylist +checks if an incoming request is from an allowed IP address before routing the request to a backend service. + +Envoy Gateway introduces a new CRD called [SecurityPolicy][SecurityPolicy] that allows the user to configure IP allowlist/denylist. +This instantiated resource can be linked to a [Gateway][Gateway], [HTTPRoute][HTTPRoute] or [GRPCRoute][GRPCRoute] resource. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## Configuration + +### Create a SecurityPolicy + +The below SecurityPolicy restricts access to the backend service by allowing requests only from the IP addresses `10.0.1.0/24`. + +In this example, the default action is set to `Deny`, which means that only requests from the specified IP addresses with `Allow` +action are allowed, and all other requests are denied. You can also change the default action to `Allow` to allow all requests +except those from the specified IP addresses with `Deny` action. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the SecurityPolicy configuration: + +```shell +kubectl get securitypolicy/authorization-client-ip -o yaml +``` + +### Original Source IP + +It's important to note that the IP address used for allowlist/denylist is the original source IP address of the request. +You can use a [ClientTrafficPolicy] to configure how Envoy Gateway should determine the original source IP address. + +For example, the below ClientTrafficPolicy configures Envoy Gateway to use the `X-Forwarded-For` header to determine the original source IP address. +The `numTrustedHops` field specifies the number of trusted hops in the `X-Forwarded-For` header. In this example, the `numTrustedHops` is set to `1`, +which means that the first rightmost IP address in the `X-Forwarded-For` header is used as the original source IP address. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + + +## Testing + +Ensure the `GATEWAY_HOST` environment variable from the [Quickstart](../../quickstart) is set. If not, follow the +Quickstart instructions to set the variable. + +```shell +echo $GATEWAY_HOST +``` + +Send a request to the backend service without the `X-Forwarded-For` header: + +```shell +curl -v -H "Host: www.example.com" "http://${GATEWAY_HOST}/" +``` + +You should see `403 Forbidden` in the response, indicating that the request is not allowed. + +```shell +* Connected to 172.18.255.200 (172.18.255.200) port 80 +> GET /get HTTP/1.1 +> Host: www.example.com +> User-Agent: curl/8.8.0-DEV +> Accept: */* +> +* Request completely sent off +< HTTP/1.1 403 Forbidden +< content-length: 19 +< content-type: text/plain +< date: Mon, 08 Jul 2024 04:23:31 GMT +< +* Connection #0 to host 172.18.255.200 left intact +RBAC: access denied +``` + +Send a request to the backend service with the `X-Forwarded-For` header: + +```shell +curl -v -H "Host: www.example.com" -H "X-Forwarded-For: 10.0.1.1" "http://${GATEWAY_HOST}/" +``` + +The request should be allowed and you should see the response from the backend service. + +## Clean-Up + +Follow the steps from the [Quickstart](../../quickstart) to uninstall Envoy Gateway and the example manifest. + +Delete the SecurityPolicy and the ClientTrafficPolicy + +```shell +kubectl delete securitypolicy/authorization-client-ip +kubectl delete clientTrafficPolicy/enable-client-ip-detection +``` + +## Next Steps + +Checkout the [Developer Guide](../../../contributions/develop) to get involved in the project. + +[SecurityPolicy]: ../../../contributions/design/security-policy +[ClientTrafficPolicy]: ../../../api/extension_types#clienttrafficpolicy +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute +[GRPCRoute]: https://gateway-api.sigs.k8s.io/api-types/grpcroute diff --git a/site/content/en/v1.1/tasks/security/secure-gateways.md b/site/content/en/v1.1/tasks/security/secure-gateways.md new file mode 100644 index 00000000000..af5e922412d --- /dev/null +++ b/site/content/en/v1.1/tasks/security/secure-gateways.md @@ -0,0 +1,520 @@ +--- +title: "Secure Gateways" +--- + +This task will help you get started using secure Gateways. +This task uses a self-signed CA, so it should be used for testing and demonstration purposes only. + +## Prerequisites + +- OpenSSL to generate TLS assets. + +## Installation + +Follow the steps from the [Quickstart](../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## TLS Certificates + +Generate the certificates and keys used by the Gateway to terminate client TLS connections. + +Create a root certificate and private key to sign certificates: + +```shell +openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -subj '/O=example Inc./CN=example.com' -keyout example.com.key -out example.com.crt +``` + +Create a certificate and a private key for `www.example.com`: + +```shell +openssl req -out www.example.com.csr -newkey rsa:2048 -nodes -keyout www.example.com.key -subj "/CN=www.example.com/O=example organization" +openssl x509 -req -days 365 -CA example.com.crt -CAkey example.com.key -set_serial 0 -in www.example.com.csr -out www.example.com.crt +``` + +Store the cert/key in a Secret: + +```shell +kubectl create secret tls example-cert --key=www.example.com.key --cert=www.example.com.crt +``` + +Update the Gateway from the Quickstart to include an HTTPS listener that listens on port `443` and references the +`example-cert` Secret: + +```shell +kubectl patch gateway eg --type=json --patch ' + - op: add + path: /spec/listeners/- + value: + name: https + protocol: HTTPS + port: 443 + tls: + mode: Terminate + certificateRefs: + - kind: Secret + group: "" + name: example-cert + ' +``` + +Verify the Gateway status: + +```shell +kubectl get gateway/eg -o yaml +``` + +## Testing + +{{< tabpane text=true >}} +{{% tab header="With External LoadBalancer Support" %}} + +Get the External IP of the Gateway: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +Query the example app through the Gateway: + +```shell +curl -v -HHost:www.example.com --resolve "www.example.com:443:${GATEWAY_HOST}" \ +--cacert example.com.crt https://www.example.com/get +``` + +{{% /tab %}} +{{% tab header="Without LoadBalancer Support" %}} + +Get the name of the Envoy service created the by the example Gateway: + +```shell +export ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}') +``` + +Port forward to the Envoy service: + +```shell +kubectl -n envoy-gateway-system port-forward service/${ENVOY_SERVICE} 8443:443 & +``` + +Query the example app through Envoy proxy: + +```shell +curl -v -HHost:www.example.com --resolve "www.example.com:8443:127.0.0.1" \ +--cacert example.com.crt https://www.example.com:8443/get +``` + +{{% /tab %}} +{{< /tabpane >}} + + +## Multiple HTTPS Listeners + +Create a TLS cert/key for the additional HTTPS listener: + +```shell +openssl req -out foo.example.com.csr -newkey rsa:2048 -nodes -keyout foo.example.com.key -subj "/CN=foo.example.com/O=example organization" +openssl x509 -req -days 365 -CA example.com.crt -CAkey example.com.key -set_serial 0 -in foo.example.com.csr -out foo.example.com.crt +``` + +Store the cert/key in a Secret: + +```shell +kubectl create secret tls foo-cert --key=foo.example.com.key --cert=foo.example.com.crt +``` + +Create another HTTPS listener on the example Gateway: + +```shell +kubectl patch gateway eg --type=json --patch ' + - op: add + path: /spec/listeners/- + value: + name: https-foo + protocol: HTTPS + port: 443 + hostname: foo.example.com + tls: + mode: Terminate + certificateRefs: + - kind: Secret + group: "" + name: foo-cert + ' +``` + +Update the HTTPRoute to route traffic for hostname `foo.example.com` to the example backend service: + +```shell +kubectl patch httproute backend --type=json --patch ' + - op: add + path: /spec/hostnames/- + value: foo.example.com + ' +``` + +Verify the Gateway status: + +```shell +kubectl get gateway/eg -o yaml +``` + +Follow the steps in the [Testing section](#testing) to test connectivity to the backend app through both Gateway +listeners. Replace `www.example.com` with `foo.example.com` to test the new HTTPS listener. + +## Cross Namespace Certificate References + +A Gateway can be configured to reference a certificate in a different namespace. This is allowed by a [ReferenceGrant][] +created in the target namespace. Without the ReferenceGrant, a cross-namespace reference is invalid. + +Before proceeding, ensure you can query the HTTPS backend service from the [Testing section](#testing). + +To demonstrate cross namespace certificate references, create a ReferenceGrant that allows Gateways from the "default" +namespace to reference Secrets in the "envoy-gateway-system" namespace: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Delete the previously created Secret: + +```shell +kubectl delete secret/example-cert +``` + +The Gateway HTTPS listener should now surface the `Ready: False` status condition and the example HTTPS backend should +no longer be reachable through the Gateway. + +```shell +kubectl get gateway/eg -o yaml +``` + +Recreate the example Secret in the `envoy-gateway-system` namespace: + +```shell +kubectl create secret tls example-cert -n envoy-gateway-system --key=www.example.com.key --cert=www.example.com.crt +``` + +Update the Gateway HTTPS listener with `namespace: envoy-gateway-system`, for example: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The Gateway HTTPS listener status should now surface the `Ready: True` condition and you should once again be able to +query the HTTPS backend through the Gateway. + +Lastly, test connectivity using the above [Testing section](#testing). + +## Clean-Up + +Follow the steps from the [Quickstart](../quickstart) to uninstall Envoy Gateway and the example manifest. + +Delete the Secrets: + +```shell +kubectl delete secret/example-cert +kubectl delete secret/foo-cert +``` + +# RSA + ECDSA Dual stack certificates + +This section gives a walkthrough to generate RSA and ECDSA derived certificates and keys for the Server, which can then be configured in the Gateway listener, to terminate TLS traffic. + +## Prerequisites + +Follow the steps from the [Quickstart](../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +Follow the steps in the [TLS Certificates](#tls-certificates) section to generate self-signed RSA derived Server certificate and private key, and configure those in the Gateway listener configuration to terminate HTTPS traffic. + +## Pre-checks + +While testing in [Cluster without External LoadBalancer Support](#clusters-without-external-loadbalancer-support), we can query the example app through Envoy proxy while enforcing an RSA cipher, as shown below: + +```shell +curl -v -HHost:www.example.com --resolve "www.example.com:8443:127.0.0.1" \ +--cacert example.com.crt https://www.example.com:8443/get -Isv --ciphers ECDHE-RSA-CHACHA20-POLY1305 --tlsv1.2 --tls-max 1.2 +``` + +Since the Secret configured at this point is an RSA based Secret, if we enforce the usage of an ECDSA cipher, the call should fail as follows + +```shell +$ curl -v -HHost:www.example.com --resolve "www.example.com:8443:127.0.0.1" \ +--cacert example.com.crt https://www.example.com:8443/get -Isv --ciphers ECDHE-ECDSA-CHACHA20-POLY1305 --tlsv1.2 --tls-max 1.2 + +* Added www.example.com:8443:127.0.0.1 to DNS cache +* Hostname www.example.com was found in DNS cache +* Trying 127.0.0.1:8443... +* Connected to www.example.com (127.0.0.1) port 8443 (#0) +* ALPN: offers h2 +* ALPN: offers http/1.1 +* Cipher selection: ECDHE-ECDSA-CHACHA20-POLY1305 +* CAfile: example.com.crt +* CApath: none +* (304) (OUT), TLS handshake, Client hello (1): +* error:1404B410:SSL routines:ST_CONNECT:sslv3 alert handshake failure +* Closing connection 0 +``` + +Moving forward in the doc, we will be configuring the existing Gateway listener to accept both kinds of ciphers. + +## TLS Certificates + +Reuse the CA certificate and key pair generated in the [Secure Gateways](#tls-certificates) task and use this CA to sign both RSA and ECDSA Server certificates. +Note the CA certificate and key names are `example.com.crt` and `example.com.key` respectively. + + +Create an ECDSA certificate and a private key for `www.example.com`: + +```shell +openssl ecparam -noout -genkey -name prime256v1 -out www.example.com.ecdsa.key +openssl req -new -SHA384 -key www.example.com.ecdsa.key -nodes -out www.example.com.ecdsa.csr -subj "/CN=www.example.com/O=example organization" +openssl x509 -req -SHA384 -days 365 -in www.example.com.ecdsa.csr -CA example.com.crt -CAkey example.com.key -CAcreateserial -out www.example.com.ecdsa.crt +``` + +Store the cert/key in a Secret: + +```shell +kubectl create secret tls example-cert-ecdsa --key=www.example.com.ecdsa.key --cert=www.example.com.ecdsa.crt +``` + +Patch the Gateway with this additional ECDSA Secret: + +```shell +kubectl patch gateway eg --type=json --patch ' + - op: add + path: /spec/listeners/1/tls/certificateRefs/- + value: + name: example-cert-ecdsa + ' +``` + +Verify the Gateway status: + +```shell +kubectl get gateway/eg -o yaml +``` + +## Testing + +Again, while testing in Cluster without External LoadBalancer Support, we can query the example app through Envoy proxy while enforcing an RSA cipher, which should work as it did before: + +```shell +curl -v -HHost:www.example.com --resolve "www.example.com:8443:127.0.0.1" \ +--cacert example.com.crt https://www.example.com:8443/get -Isv --ciphers ECDHE-RSA-CHACHA20-POLY1305 --tlsv1.2 --tls-max 1.2 +``` + +```shell +... +* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1): +* TLSv1.2 (IN), TLS handshake, Finished (20): +* SSL connection using TLSv1.2 / ECDHE-RSA-CHACHA20-POLY1305 +... +``` + +Additionally, querying the example app while enforcing an ECDSA cipher should also work now: + +```shell +curl -v -HHost:www.example.com --resolve "www.example.com:8443:127.0.0.1" \ +--cacert example.com.crt https://www.example.com:8443/get -Isv --ciphers ECDHE-ECDSA-CHACHA20-POLY1305 --tlsv1.2 --tls-max 1.2 +``` + +```shell +... +* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1): +* TLSv1.2 (IN), TLS handshake, Finished (20): +* SSL connection using TLSv1.2 / ECDHE-ECDSA-CHACHA20-POLY1305 +... +``` + +# SNI based Certificate selection + +This sections gives a walkthrough to generate multiple certificates corresponding to different FQDNs. The same Gateway listener can then be configured to terminate TLS traffic for multiple FQDNs based on the SNI matching. + +## Prerequisites + +Follow the steps from the [Quickstart](../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +Follow the steps in the [TLS Certificates](#tls-certificates) section to generate self-signed RSA derived Server certificate and private key, and configure those in the Gateway listener configuration to terminate HTTPS traffic. + +## Additional Configurations + +Using the [TLS Certificates](#tls-certificates) section, we first generate additional Secret for another Host `www.sample.com`. + +```shell +openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -subj '/O=sample Inc./CN=sample.com' -keyout sample.com.key -out sample.com.crt + +openssl req -out www.sample.com.csr -newkey rsa:2048 -nodes -keyout www.sample.com.key -subj "/CN=www.sample.com/O=sample organization" +openssl x509 -req -days 365 -CA sample.com.crt -CAkey sample.com.key -set_serial 0 -in www.sample.com.csr -out www.sample.com.crt + +kubectl create secret tls sample-cert --key=www.sample.com.key --cert=www.sample.com.crt +``` + +Note that all occurrences of `example.com` were just replaced with `sample.com` + + +Next we update the `Gateway` configuration to accommodate the new Certificate which will be used to Terminate TLS traffic: + +```shell +kubectl patch gateway eg --type=json --patch ' + - op: add + path: /spec/listeners/1/tls/certificateRefs/- + value: + name: sample-cert + ' +``` + +Finally, we update the HTTPRoute to route traffic for hostname `www.sample.com` to the example backend service: + +```shell +kubectl patch httproute backend --type=json --patch ' + - op: add + path: /spec/hostnames/- + value: www.sample.com + ' +``` + +## Testing + +{{< tabpane text=true >}} +{{% tab header="With External LoadBalancer Support" %}} + +Refer to the steps mentioned earlier under [Testing in clusters with External LoadBalancer Support](#testing) + + +{{% /tab %}} +{{% tab header="Without LoadBalancer Support" %}} + +Get the name of the Envoy service created the by the example Gateway: + +```shell +export ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}') +``` + +Port forward to the Envoy service: + +```shell +kubectl -n envoy-gateway-system port-forward service/${ENVOY_SERVICE} 8443:443 & +``` + +Query the example app through Envoy proxy: + +```shell +curl -v -HHost:www.example.com --resolve "www.example.com:8443:127.0.0.1" \ +--cacert example.com.crt https://www.example.com:8443/get -I +``` + +Similarly, query the sample app through the same Envoy proxy: + +```shell +curl -v -HHost:www.sample.com --resolve "www.sample.com:8443:127.0.0.1" \ +--cacert sample.com.crt https://www.sample.com:8443/get -I +``` + +Since the multiple certificates are configured on the same Gateway listener, Envoy was able to provide the client with appropriate certificate based on the SNI in the client request. + +{{% /tab %}} +{{< /tabpane >}} + +## Next Steps + +Checkout the [Developer Guide](../../../contributions/develop) to get involved in the project. + +[ReferenceGrant]: https://gateway-api.sigs.k8s.io/api-types/referencegrant/ diff --git a/site/content/en/v1.1/tasks/security/threat-model.md b/site/content/en/v1.1/tasks/security/threat-model.md new file mode 100644 index 00000000000..a16319f9d72 --- /dev/null +++ b/site/content/en/v1.1/tasks/security/threat-model.md @@ -0,0 +1,665 @@ +--- +title: "Threat Model" +--- + +# Envoy Gateway Threat Model and End User Recommendations + +## About + +This work was performed by [ControlPlane](https://control-plane.io/) and commissioned by the [Linux Foundation](https://www.linuxfoundation.org/). ControlPlane is a global cloud native and open source cybersecurity consultancy, trusted as the partner of choice in securing: multinational banks; major public clouds; international financial institutions; critical national infrastructure programs; multinational oil and gas companies, healthcare and insurance providers; and global media firms. + +## Threat Modelling Team + +James Callaghan, Torin van den Bulk, Eduardo Olarte + +## Reviewers + +Arko Dasgupta, Matt Turner, Zack Butcher, Marco De Benedictis + +## Introduction + +As we embrace the proliferation of microservice-based architectures in the cloud-native landscape, simplicity in setup and configuration becomes paramount as DevOps teams face the challenge of choosing between numerous similar technologies. One such choice which every team deploying to Kubernetes faces is what to use as an ingress controller. With a plethora of options available, and the existence of vendor-specific annotations leading to small inconsistencies between implementations, the [Gateway API](https://gateway-api.sigs.k8s.io/) project was introduced by the SIG-NETWORK community, with the goal of eventually replacing the Ingress resource. + +Envoy Gateway is configured by Gateway API resources, and serves as an intuitive and feature-rich wrapper over the widely acclaimed Envoy Proxy. With a convenient setup based on Kubernetes (K8s) manifests, Envoy Gateway streamlines the management of Envoy Proxy instances in an edge-proxy setting, reducing the operational overhead of managing low-level Envoy configurations. Envoy Gateway benefits cloud-native DevOps teams through its role-oriented configuration, providing granular control based on Role-Based Access Control (RBAC) principles. These features form the basis of our exploration into Envoy Gateway and the rich feature set it brings to the table. + +In this threat model, we aim to provide an analysis of Envoy Gateway's design components and their capabilities (at version 1.0) through a threat-driven approach. It should be noted that this does not constitute a security audit of the Envoy Gateway project, but instead focuses on different possible deployment topologies for Envoy Gateway with the goal of deriving recommendations and best practice guidance for end users. + +The Envoy Gateway project recommends a [multi-tenancy model](../operations/deployment-mode#multi-tenancy) whereby each tenant deploys their own Envoy Gateway controller in a namespace which they own. We will also explore the implications and risks associated with multiple tenants using a shared controller. + +### Scope + +The primary focus of this threat model is to identify and assess security risks associated with deploying and operating Envoy Gateway within a multi-tenant Kubernetes (K8s) cluster. This model aims to provide a comprehensive understanding of the system, its transmission points, and potential vulnerabilities to enumerated threats. + +### In Scope + +**Envoy Gateway**: As the primary focus of this threat model, all aspects of Envoy Gateway, including its configuration, deployment, and operation will be analysed. This includes how the gateway manages TLS certificates, authentication, service-to-service traffic routing, and more. + +**Kubernetes Cluster**: Configuration and operation of the underlying Kubernetes cluster, including how it manages network policies, access control, and resource isolation for different namespaces/tenants in relation to Envoy will be considered. + +**Tenant Workloads**: Tenant workloads (and the pods they run on) will be considered, focusing on how they interact with the Envoy Gateway and potential vulnerabilities that could be exploited. + +#### Out of Scope + +This threat model will not consider security risks associated with the underlying infrastructure (e.g., EC2 compute instances and S3 buckets) or non-Envoy related components within the Kubernetes Cluster. It will focus solely on the Envoy Gateway and its interaction with the Kubernetes cluster and tenant workloads. + +Implementation of Envoy Gateway as an egress traffic controller is out of scope for this threat model and will not be considered in the report's findings. + +### Related Resources + +[Introducing Envoy Gateway](https://blog.envoyproxy.io/introducing-envoy-gateway-ad385cc59532) + +[Envoy Proxy Threat Model](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/security/threat_model#threat-model) + +[Configuring Envoy as an Edge Proxy](https://www.envoyproxy.io/docs/envoy/latest/configuration/best_practices/edge#best-practices-edge) + +[Envoy Gateway Deployment Mode](../operations/deployment-mode) + +[Kubernetes Gateway API Security Model](https://gateway-api.sigs.k8s.io/concepts/security-model/) + +## Architecture Overview + +### Summary + +To provide an in-depth look into both the system design and end-user deployment of Envoy Gateway, we will be focusing on the [Deployment Architecture Diagram](#deployment-architecture-diagram) below. + +The Deployment Architecture Diagram provides a high-level model of an end-user deployment of Envoy Gateway. For simplicity, we will look at different deployment topologies on a single multi-tenant Kubernetes cluster. Envoy Gateway operates as an edge proxy within this environment, handling the traffic flow between external interfaces and services within the cluster. The example will use two Envoy Gateway controllers - one dedicated controller for a single tenant, and one shared controller for two other tenants. Each Envoy Gateway controller will accept a single GatewayClass resource. + +### Deployment Architecture Diagram + +As Envoy Gateway implements the [Kubernetes GatewayAPI](https://gateway-api.sigs.k8s.io/concepts/api-overview/), this threat model will focus on the key objects in the Gateway API resource model: + +1. **GatewayClass:** defines a set of gateways with a commonconfiguration and behaviour. It is a cluster scoped resource. + +2. **Gateway:** requests a point where traffic can be translated to Services within the cluster. + +3. **Routes:** describe how traffic coming via the Gateway maps to theServices. + +At the time of writing, Envoy Gateway only supports a Kubernetes provider. As such, we will consider a reference architecture where multiple teams are working on the same Kubernetes cluster within different namespaces (Tenant A, B, & C). We will assume that some teams have similar security and performance needs, and a decision has been made to use a shared Gateway. However, we will also consider the case that some teams require dedicated Gateways, perhaps for compliance reasons or requirements driven by an internal threat model. + +We will consider the following organisational roles, as per the [Gateway API security model](https://gateway-api.sigs.k8s.io/concepts/security-model/): + +1. **Infrastructure provider**: The infrastructure provider (infra) is responsible for the overall environment that the cluster(s) are operating in. Examples include: the cloud provider (AWS, Azure, GCP, ...) or the PaaS provider in a company. + +2. **Cluster operator**: The cluster operator (ops) is responsible for administration of entire clusters. They manage policies, network access, application permissions. + +3. **Application developer**: The application developer (dev) is responsible for defining their application configuration (e.g. timeouts, request matching/filter) and Service composition (e.g. path routing to backends). + +4. **Application admin**: The application admin has administrative access to some namespaces within a cluster, but not the cluster as a whole. + +Our threat model will be based on the high-level setup shown below, where Envoy is used in an edge-proxy scenario: + +![Architecture](/img/architecture_threat_model.png) + +The following use cases will be considered, in line with the [Envoy Gateway tasks](../quickstart): + +1. Routing and controlling traffic, including: + a. HTTP \ + b. TCP \ + c. UDP \ + d. gRPC \ + e.TLS passthrough +2. TLS termination +3. Request Authentication +4. Rate Limiting + +## Key Assumptions + +This section outlines the foundational premises that shape our analysis and recommendations for the deployment and management of Envoy Gateway within an organisation. The key assumptions are as follows: + +**1. Kubernetes Provider**: For the purposes of this analysis, we assume that a K8s provider will be used to host the cluster. + +**2. Multi-tenant cluster**: In order to produce a broad set of recommendations, it is assumed that within the single cluster, there is: + +- A dedicated cluster operation (ops) team responsible for maintaining the core cluster infrastructure. + +- Multiple application teams who wish to define their own Gateway resources, which will route traffic to their respective applications. + +**3. Soft multi-tenancy model**: It is assumed that co-tenants will have some level of trust between themselves, and will not act in an overtly hostile manner to each other. + +**4. Ingress Control**: It's assumed that Envoy Gateway is the only ingress controller in the K8s cluster as multiple controllers can lead to complex routing challenges and introduce out-of-scope security vulnerabilities. + +**5. Container Security**: This threat model focuses on evaluating the security of the Envoy Gateway and Envoy Proxy images. All other container images running in tenant clusters, not associated with the edge proxy deployment, are assumed to be secure and obtained from trusted registries such as Docker Hub or Google Container Registry (GCR). + +**6. Cloud Provider Security**: It is assumed that the K8s cluster is running on secure cloud infrastructure provided by a trusted Cloud Service Provider (CSP) such as AWS, GCP, or Azure Cloud. + +## Data + +### Data Dictionary + +Ultimately, the data of interest in a threat model is the business data processed by the system in question. However, in the case of this threat model, we are looking at a generic deployment architecture involving Envoy Gateway in order to draw out a set of generalised threats which can be considered by teams looking to adopt an implementation of Gateway API. As such, we do not know the business impacts of a compromise of confidentiality, integrity or availability that would typically be captured in a data impact assessment. Instead, will we base our threat assessment on high-level groupings of data structures used in the configuration and operation of the general use cases considered (e.g. HTTP routing, TLS termination, request authentication etc.). We will then assign a confidentiality, integrity and availability impact based on a worst-case scenario of how each compromise could potentially affect business data processed by the generic deployment. + +| Data Name / Type | Notes | Confidentiality | Integrity | Availability | +| ------------ | ------------ | ------------ |--------------- | ------------ | +| Static Configuration Data | Static configuration data is used to configure Envoy Gateway at startup. This data structure allows for a Provider to be set, which Envoy Gateway calls to establish its runtime configuration, resolve services and persist data. Unauthorised modification of static configuration data could enable the Envoy Gateway admin interface to be configured, logging parameters to be modified, global rate limiting configuration to be misconfigured, or malicious extensions registered for the Envoy Gateway Control Plane. A compromise of confidentiality could potentially give an attacker some useful reconnaissance information. A compromise of the availability of this information at startup time would result in Envoy Gateway starting with default parameters. | Medium | High | Low | +| Dynamic Configuration Data | Dynamic configuration data represents the desired state of the Data Plane, and is defined through Envoy Gateway and Gateway API Kubernetes resources. Unauthorised modification of this data could lead to vulnerabilities in an organisation’s Data Plane infrastructure via misconfiguration of an EnvoyProxy custom resource. Misconfiguration of Gateway API objects such as HTTPRoutes or TLSRoutes could result in traffic being directed to incorrect backends. A compromise of confidentiality could potentially give an attacker some useful reconnaissance information. A compromise of the availability of this information could result in tenant application traffic not being routable until the configuration is recovered and reapplied. | Medium | High | Medium | +| TLS Private Keys | TLS Private Keys, typically in PEM format, are used to initiate secure connections and encrypt communications. In the context of this threat model, private keys will be associated with the server side of an inbound TLS connection being terminated at a secure gateway configured through Envoy Gateway. Unauthorised exposure could lead to security threats such as person-in-the-middle attacks, whereby the confidentiality or integrity of business data could be compromised. A compromise of integrity may lead to similar consequences if an attacker could insert their own key material. An availability compromise could lead to tenant services being unavailable until new key material is generated and an appropriate CSR submitted. | High | High | Medium | +| TLS Certificates | X.509 certificates represent the binding of a public key (associated with the private key described above) to an identity in a TLS handshake. If an attacker could compromise the integrity of a certificate, they may be able to bind the identity of a TLS termination point to a key pair under their control, enabling person-in-the middle attacks. An availability compromise could lead to tenant services being unavailable until new key material is generated and an appropriate CSR submitted. | Low | High | Medium | +| JWKs | JWK (JSON Web Key) containing a public key used to validate JWTs for the client authentication use case considered in this threat model. If an attacker could compromise the integrity of a JWK or JSON web key set (JWKS), they could potentially authenticate to a service maliciously. Unavailability of an endpoint exposing JWKs could lead to client requests which require authentication being denied. | Low | High | Medium | +| JWTs | JWTs, formatted as compact, URL-safe JSON data structures, are utilised for the client authentication use case considered in this threat model. Maintaining their confidentiality and integrity is vital to prevent unauthorised access and ensure correct user identification. | High | High | Low | +| OIDC credentials | In OIDC authentication scenarios, the application credentials are represented by a client ID and a client secret. A compromise of its confidentiality or integrity could allow malicious actors to impersonate the application, potentially being able to access resources on behalf of the application and request ID tokens on behalf of users. Unavailability of this data would produce a rejection of the requests coming from legitimate users. | High | High | Medium | +| Basic authentiation password hashes | In basic authentication scenarios, passwords are stored as Kubernetes secrets in [htpasswd](https://httpd.apache.org/docs/current/programs/htpasswd.html) format, where each entry is formed by the username and the hashed password. A compromise of these credentials' confidentiality and integrity could lead to unauthorised access to the application. Unavailability of these credentials will cause login failures from the application users. | High | High | Medium | + +### CIA Impact Assessment + +| Priority | Description | +| --- | --- | +| **Confidentiality** | | +| High | Compromise of sensitive client data | +| Medium | Information leaked which could be useful for attacker reconnaissance | +| Low | Non-sensitive information leakage | +| **Integrity** | | +| High | Compromise of source code repositories and gateway deployments | +| Medium | Traffic routing fails due to misconfiguration / invalid configuration | +| Low | Non-critical operation is blocked due to misconfiguration / invalid configuration | +| **Availability** | | +| High | Large scale DoS | +| Medium | Tenant application is blocked for a significant period | +| Low | Tenant application is blocked for a short period | + +### Data Flow Diagrams + +The Data Flow Diagrams (DFDs) below describe the flow of data between the various processes, entities and data stores in a system, as well as the trust boundaries between different user roles and network interfaces. The DFDs are drawn at two different levels, starting at L0 (high-level system view) and increasing in granularity (to L1). + +### DFD L0 + +![DFD L0](/img/DFDL0.png) + +### DFD L1 + +![DFD L1](/img/DFDL1.png) + +## Key Threats and Recommendations + +The scope of this threat model led to us categorising threats into priorities of High, Medium or Low; notably in a production implementation some of the threats' prioritisation may be upgraded or downgraded depending on the business context and data classification. + +### Risk vs. Threat + +For every finding, the risk and threat are stated. Risk defines the potential for negative outcome while threat defines the event that causes the negative outcome. + +### Threat Categorization + +Throughout this threat model, we categorised threats into different areas based on their origin and the segment of the system that they impact. Here's an overview of each category: + +**Container Security (CS)**: These threats are general to containerised applications. Therefore, they are not associated with Envoy Gateway or the Gateway API and could occur in most containerised workloads. They can originate from misconfigurations or vulnerabilities in the orchestrator or the container. + +**Gateway API (GW)**: These are threats related to the Gateway API that could affect any of its implementations. Malicious actors could benefit from misconfigurations or excessive permissions on the Gateway API resources (e.g. xRoutes or Gateways) to compromise the confidentiality, integrity, or availability of the application. + +**Envoy Gateway (EG)**: These threats are associated with specific configurations or features from Envoy Gateway or Envoy Proxy. If not set properly, these features could be leveraged to gain unauthorised access to protected resources. + +### Threat Actors + +In order to provide a realistic set of threats that is applicable to most organisations, we de-scoped the most advanced and hard to mitigate threat actors as described below: + +#### In Scope Threat Actors + +When considering internal threat actors, we chose to follow the [security model](https://gateway-api.sigs.k8s.io/concepts/security-model/) of the Kubernetes Gateway API. + +##### Internal Attacker + +- Cluster Operator: The cluster operator (ops) is responsible for administration of entire clusters. They manage policies, network access, application permissions. + +- Application Developer: The application developer (dev) is responsible for defining their application configuration (e.g. timeouts, request matching/filter) and Service composition (e.g. path routing to backends). + +- Application Administrator: The application admin has administrative access to some namespaces within a cluster, but not the cluster as a whole. + +##### External Attacker + +- Vandal: Script kiddie, trespasser + +- Motivated Individual: Political activist, thief, terrorist + +- Organised Crime: Syndicates, state-affiliated groups + +#### Out of Scope Threat Actors + +##### External Actors + +- Infrastructure Provider: The infrastructure provider (infra) is responsible for the overall environment that the cluster(s) are operating in. Examples include: the cloud provider, or the PaaS provider in a company. + +- Cloud Service Insider: Employee, external contractor, temporary worker + +- Foreign Intelligence Services (FIS): Nation states + +## High Priority Findings + +### EGTM-001 Usage of self-signed certificates + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-001|EGTM-GW-001|Gateway API|High| + + **Risk**: Self-signed certificates (which do not comply with PKI best practices) could lead to unauthorised access to the private key associated with the certificate used for inbound TLS termination at Envoy Proxy, compromising the confidentiality and integrity of proxied traffic. + + **Threat**: Compromise of the private key associated with the certificate used for inbound TLS terminating at Envoy Proxy. + + **Recommendation**: The Envoy Gateway quickstart demonstrates how to set up a Secure Gateway using an example where a self-signed root certificate is created using openssl. As stated in the Envoy Gateway documentation, this is not a suitable configuration for Production usage. It is recommended that PKI best practices are followed, whereby certificates are signed by an Intermediary CA which sits underneath an organisational \'offline\' Root CA. + + PKI best practices should also apply to the management of client certificates when using mTLS. The Envoy Gateway [mTLS](../security/mutual-tls) task shows how to set up client certificates using self-signed certificates. In the same way as gateway certificates and, as mentioned in the documentation, this configuration should not be used in production environments. + +### EGTM-002 Private keys are stored as Kubernetes secrets + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|--------------|------------------------|-----------------| +|EGTM-002|EGTM-CS-001|Container Security|High| + + **Risk**: There is a risk that a threat actor could compromise the Kubernetes secret containing the Envoy private key, allowing the attacker to decrypt Envoy Proxy traffic, compromising the confidentiality of proxied traffic. + + **Threat**: Kubernetes secret containing the Envoy private key is compromised and used to decrypt proxied traffic. + + **Recommendation**: Certificate management best practices mandate short-lived key material where practical, meaning that a mechanism for rotation of private keys and certificates is required, along with a way for certificates to be mounted into Envoy containers. If Kubernetes secrets are used, when a certificate expires, the associated secret must be updated, and Envoy containers must be redeployed. Instead of a manual configuration, it is recommended that [cert-manager](https://github.com/cert-manager/cert-manager) is used. + +### EGTM-004 Usage of ClusterRoles with wide permissions + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|--------------|------------------------|-----------------| +|EGTM-004|EGTM-K8-002|Container Security|High| + + **Risk**: There is a risk that a threat actor could abuse misconfigured RBAC to access the Envoy Gateway ClusterRole (envoy-gateway-role) and use it to expose all secrets across the cluster, thus compromising the confidentiality and integrity of tenant data. + + **Threat**: Compromised Envoy Gateway or misconfigured ClusterRoleBinding (envoy-gateway-rolebinding) to Envoy Gateway ClusterRole (envoy-gateway-role), provides access to resources and secrets in different namespaces. + + **Recommendation**: Users should be aware that Envoy Gateway uses a ClusterRole (envoy-gateway-role) when deployed via the Helm chart, to allow management of Envoy Proxies across different namespaces. This ClusterRole is powerful and includes the ability to read secrets in namespaces which may not be within the purview of Envoy Gateway. + + Kubernetes best-practices involve restriction of ClusterRoleBindings, with the use of RoleBindings where possible to limit access per namespace by specifying the namespace in metadata. Namespace isolation reduces the impact of compromise from cluster-scoped roles. Ideally, fine-grained K8s roles should be created per the principle of least privilege to ensure they have the minimum access necessary for role functions. + + The pull request \#[1656](https://github.com/envoyproxy/gateway/pull/1656) introduced the use of Roles and RoleBindings in [namespaced mode](https://gateway.envoyproxy.io/latest/api/extension_types/#kuberneteswatchmode). This feature can be leveraged to reduce the amount of permissions required by the Envoy Gateway. + +### EGTM-007 Misconfiguration of Envoy Gateway dynamic config + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-007|EGTM-EG-002|Envoy Gateway|High| + + **Risk**: There is a risk that a threat actor could exploit misconfigured Kubernetes RBAC to create or modify Gateway API resources with no business need, potentially leading to the compromise of the confidentiality, integrity, and availability of resources and traffic within the cluster. + + **Threat**: Unauthorised creation or misconfiguration of Gateway API resources by a threat actor with cluster-scoped access. + + **Recommendation**: Configure the apiGroup and resource fields in RBAC policies to restrict access to [Gateway](https://gateway-api.sigs.k8s.io/) and [GatewayClass](https://gateway-api.sigs.k8s.io/api-types/gatewayclass/) resources. Enable namespace isolation by using the namespace field, preventing unauthorised access to gateways in other namespaces. + +### EGTM-009 Co-tenant misconfigures resource across namespaces + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-009|EGTM-GW-002|Gateway API|High| + + **Risk**: There is a risk that a co-tenant misconfigures Gateway or Route resources, compromising the confidentiality, integrity, and availability of routed traffic through Envoy Gateway. + + **Threat**: Malicious or accidental co-tenant misconfiguration of Gateways and Routes associated with other application teams. + + **Recommendation**: Dedicated Envoy Gateways should be provided to each tenant within their respective namespace. A one-to-one relationship should be established between GatewayClass and Gateway resources, meaning that each tenant namespace should have their own GatewayClass watched by a unique Envoy Gateway Controller as defined here in the [Deployment Mode](../operations/deployment-mode) documentation. + + Application Admins should have write permissions on the Gateway resource, but only in their specific namespaces, and Application Developers should only hold write permissions on Route resources. To enact this access control schema, follow the [Write Permissions for Advanced 4 Tier Model](https://gateway-api.sigs.k8s.io/concepts/security-model/#write-permissions-for-advanced-4-tier-model) described in the Kubernetes Gateway API security model. Examples of secured gateway-route topologies can be found [here](https://gateway-api.sigs.k8s.io/concepts/api-overview/#attaching-routes-to-gateways) within the Kubernetes Gateway API docs. + + Optionally, consider a GitOps model, where only the GitOps operator has the permission to deploy or modify custom resources in production. + +### EGTM-014 Malicious image admission + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-014|EGTM-CS-006|Container Security|High| + + **Risk**: There is a risk that a supply chain attack on Envoy Gateway results in an arbitrary compromise of the confidentiality, integrity or availability of tenant data. + + **Threat**: Supply chain threat actor introduces malicious code into Envoy Gateway or Proxy. + + **Recommendation**: The Envoy Gateway project should continue to work towards conformance with supply-chain security best practices throughout the project lifecycle (for example, as set out in the [CNCF Software Supply Chain Best Practices Whitepaper](https://github.com/cncf/tag-security/blob/main/supply-chain-security/supply-chain-security-paper/CNCF_SSCP_v1.pdf)). Adherence to [Supply-chain Levels for Software Artefacts](https://slsa.dev/) (SLSA) standards is crucial for maintaining the security of the system. Employ version control systems to monitor the source and build platforms and assign responsibility to a specific stakeholder. + + Integrate a supply chain security tool such as Sigstore, which provides native capabilities for signing and verifying container images and software artefacts. [Software Bill of Materials](https://www.cisa.gov/sbom) (SBOM), [Vulnerability Exploitability eXchange](https://www.ntia.gov/files/ntia/publications/vex_one-page_summary.pdf) (VEX), and signed artefacts should also be incorporated into the security protocol. + +### EGTM-020 Out of date or misconfigured Envoy Proxy image + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-020|EGTM-CS-009|Container Security|High| + + **Risk**: There is a risk that a threat actor exploits an Envoy Proxy vulnerability to remote code execution (RCE) due to out of date or misconfigured Envoy Proxy pod deployment, compromising the confidentiality and integrity of Envoy Proxy along with the availability of the proxy service. + + **Threat**: Deployment of an Envoy Proxy or Gateway image containing exploitable CVEs. + + **Recommendation**: Always use the latest version of the Envoy Proxy image. Regularly check for updates and patch the system as soon as updates become available. Implement a CI/CD pipeline that includes security checks for images and prevents deployment of insecure configurations. A suitable tool should be chosen to provide container vulnerability scanning to mitigate the risk of known vulnerabilities. + + Utilise the [Pod Security Admission](https://kubernetes.io/docs/concepts/security/pod-security-admission/) controller to enforce [Pod Security Standards](https://kubernetes.io/docs/concepts/security/pod-security-standards/) and configure the [pod security context](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/) to limit its capabilities per the principle of least privilege. + +### EGTM-022 Credentials are stored as Kubernetes Secrets + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-022|EGTM-CS-010|Container Security|High| + + **Risk**: There is a risk that the OIDC client secret (for OIDC authentication) and user password hashes (for basic authentication) get leaked due to misconfigured RBAC permissions. + + **Threat**: Unauthorised access to the application due to credential leakage. + + **Recommendation**: Ensure that only authorised users and service accounts are able to access secrets. This is especially important in namespaces where SecurityPolicy objects are configured, since those namespaces are the ones to store secrets containing the client secret (in OIDC scenarios) and user password hashes (in basic authentication scenarios). + + To do so, minimise the use of ClusterRoles and Roles allowing listing and getting secrets. Perform periodic audits of RBAC permissions. + +### EGTM-023 Weak Authentication + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-023|EGTM-EG-007|Envoy Gateway|High| + + **Risk**: There is a risk of unauthorised access due to the use of basic authentication, which does not enforce any password restriction in terms of complexity and length. In addition, password hashes are stored in SHA1 format, which is a deprecated hashing function. + + **Threat**: Unauthorised access to the application due to weak authentication mechanisms. + + **Recommendation**: It is recommended to make use of stronger authentication mechanisms (i.e. JWT authentication and OIDC authentication) instead of basic authentication. These authentication mechanisms have many advantages, such as the use of short-lived credentials and a central management of security policies through the identity provider. + +## Medium Priority Findings + +### EGTM-008 Misconfiguration of Envoy Gateway static config + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-008|EGTM-EG-003|Envoy Gateway|Medium| + + **Risk**: There is a risk of a threat actor misconfiguring static config and compromising the integrity of Envoy Gateway, ultimately leading to the compromised confidentiality, integrity, or availability of tenant data and cluster resources. + + **Threat**: Accidental or deliberate misconfiguration of static configuration leads to a misconfigured deployment of Envoy Gateway, for example logging parameters could be modified or global rate limiting configuration misconfigured. + + **Recommendation**: Implement a GitOps model, utilising Kubernetes\' Role-Based Access Control (RBAC) and adhering to the principle of least privilege to minimise human intervention on the cluster. For instance, tools like [Flux](https://fluxcd.io/) and [ArgoCD](https://argo-cd.readthedocs.io/en/stable/) can be used for declarative GitOps deployments, ensuring all changes are tracked and reviewed. Additionally, configure your source control management (SCM) system to include mandatory pull request (PR) reviews, commit signing, and protected branches to ensure only authorised changes can be committed to the start-up configuration. + +### EGTM-010 Weak pod security contexts and policies + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-010|EGTM-CS-005|Container Security|Medium| + + **Risk**: There is a risk that a threat actor exploits a weak pod security context, compromising the CIA of a node and the resources / services which run on it. + + **Threat**: Threat Actor who has compromised a pod exploits weak security context to escape to a node, potentially leading to the compromise of Envoy Proxy or Gateway running on the same node. + + **Recommendation**: To mitigate this risk, apply [Pod Security Standards](https://kubernetes.io/docs/concepts/security/pod-security-standards/) at a minimum of [Baseline](https://kubernetes.io/docs/concepts/security/pod-security-standards/#baseline) level to all namespaces, especially those containing Envoy Gateway and Proxy Pods. Pod security standards are implemented through K8s [Pod Security Admission](https://kubernetes.io/docs/concepts/security/pod-security-admission/) to provide [admission control modes](https://kubernetes.io/docs/concepts/security/pod-security-admission/#pod-security-admission-labels-for-namespaces) (enforce, audit, and warn) for namespaces. Pod security standards can be enforced by namespace labels as shown [here](https://kubernetes.io/docs/tasks/configure-pod-container/enforce-standards-namespace-labels/), to enforce a baseline level of pod security to specific namespaces. + + Further enhance the security by implementing a sandboxing solution such as [gVisor](https://gvisor.dev/) for Envoy Gateway and Proxy Pods to isolate the application from the host kernel. This can be set within the runtimeClassName of the Pod specification. + +### EGTM-012 ClusterRoles and Roles with permission to deploy ReferenceGrants + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|----------------|----------------------|-----------------| +|EGTM-012|EGTM-GW-004|Gateway API|Medium| + + **Risk**: There is a risk that a threat actor could abuse excessive RBAC privileges to create ReferenceGrant resources. These resources could then be used to create cross-namespace communication, leading to unauthorised access to the application. This could compromise the confidentiality and integrity of resources and configuration in the affected namespaces and potentially disrupt the availability of services that rely on these object references. + + **Threat**: A ReferenceGrant is created, which validates traffic to cross namespace trust boundaries without a valid business reason, such as a route in one tenant\'s namespace referencing a backend in another. + + **Recommendation**: Ensure that the ability to create ReferenceGrant resources is restricted to the minimum number of people. Pay special attention to ClusterRoles that allow that action. + +### EGTM-018 Network Denial of Service (DoS) + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|----------------|----------------------|-----------------| +|EGTM-018|EGTM-GW-006|Gateway API|Medium| + + **Risk**: There is a risk that malicious requests could lead to a Denial of Service (DoS) attack, thereby reducing API gateway availability due to misconfigurations in rate-limiting or load balancing controls, or a lack of route timeout enforcement. + + **Threat**: Reduced API gateway availability due to an attacker\'s maliciously crafted request (e.g., QoD) potentially inducing a Denial of Service (DoS) attack. + + **Recommendation**: To ensure high availability and mitigate potential security threats, follow the guidelines in the Envoy Gateway documentation for configuring [local rate limit](../traffic/local-rate-limit) filters, [global rate limit](../traffic/global-rate-limit) filters, and load balancing. + + Further, adhere to best practices for configuring Envoy Proxy as an edge proxy documented [here](https://www.envoyproxy.io/docs/envoy/latest/configuration/best_practices/edge#configuring-envoy-as-an-edge-proxy) within the EnvoyProxy docs. This involves configuring TCP and HTTP proxies with specific settings, including restricting access to the admin endpoint, setting the [overload manager](https://www.envoyproxy.io/docs/envoy/latest/configuration/operations/overload_manager/overload_manager#config-overload-manager) and [listener](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) / [cluster](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) buffer limits, enabling [use_remote_address](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-use-remote-address), setting [connection and stream timeouts](https://www.envoyproxy.io/docs/envoy/latest/faq/configuration/timeouts#faq-configuration-timeouts), limiting [maximum concurrent streams](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-field-config-core-v3-http2protocoloptions-max-concurrent-streams), setting [initial stream window size limit](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-field-config-core-v3-http2protocoloptions-initial-stream-window-size), and configuring action on [headers_with_underscores](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-field-config-core-v3-httpprotocoloptions-headers-with-underscores-action). + + [Path normalisation](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-normalize-path) should be enabled to minimise path confusion vulnerabilities. These measures help protect against volumetric threats such as Denial of Service (DoS) attacks. Utilise custom resources to implement policy attachment, thereby exposing request limit configuration for route types. + +### EGTM-019 JWT-based authentication replay attacks + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-019|EGTM-DP-004|Container Security|Medium| + + **Risk**: There is a risk that replay attacks using stolen or reused JSON Web Tokens (JWTs) can compromise transmission integrity, thereby undermining the confidentiality and integrity of the data plane. + + **Threat**: Transmission integrity is compromised due to replay attacks using stolen or reused JSON Web Tokens (JWTs). + + **Recommendation**: Comply with JWT best practices for enhanced security, paying special attention to the use of short-lived tokens, which reduce the window of opportunity for a replay attack. The [exp](https://datatracker.ietf.org/doc/html/rfc7519#page-9) claim can be used to set token expiration times. + +### EGTM-024 Excessive privileges via extension policies + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-024|EGTM-EG-008|Envoy Gateway|Medium| + + **Risk**: There is a risk of developers getting more privileges than required due to the use of SecurityPolicy, ClientTrafficPolicy, EnvoyPatchPolicy and BackendTrafficPolicy. These resources can be attached to a Gateway resource. Therefore, a developer with permission to deploy them would be able to modify a Gateway configuration by targeting the gateway in the policy manifest. This conflicts with the [Advanced 4 Tier Model](https://gateway-api.sigs.k8s.io/concepts/security-model/#write-permissions-for-advanced-4-tier-model), where developers do not have write permissions on Gateways. + + **Threat**: Excessive developer permissions lead to a misconfiguration and/or unauthorised access. + + **Recommendation**: Considering the Tenant C scenario (represented in the Architecture Diagram), if a developer can create SecurityPolicy, ClientTrafficPolicy, EnvoyPatchPolicy or BackendTrafficPolicy objects in namespace C, they would be able to modify a Gateway configuration by attaching the policy to the gateway. In such scenarios, it is recommended to either: + + a. Create a separate namespace, where developers have no permissions, to host tenant C\'s gateway. Note that, due to design decisions, the SecurityPolicy/EnvoyPatchPolicy/ClientTrafficPolicy/BackendTrafficPolicy object can only target resources deployed in the same namespace. Therefore, having a separate namespace for the gateway would prevent developers from attaching the policy to the gateway. + + b. Forbid the creation of these policies for developers in namespace C. + + On the other hand, in scenarios similar to tenants A and B, where a shared gateway namespace is in place, this issue is more limited. Note that in this scenario, developers don\'t have access to the shared gateway namespace. + + In addition, it is important to mention that EnvoyPatchPolicy resources can also be attached to GatewayClass resources. This means that, in order to comply with the Advanced 4 Tier model, individuals with the Application Administrator role should not have access to this resource either. + +## Low Priority Findings + +### EGTM-003 Misconfiguration leads to insecure TLS settings + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|--------------|------------------------|-----------------| +|EGTM-003|EGTM-EG-001|Envoy Gateway|Low| + + **Risk**: There is a risk that a threat actor could downgrade the security of proxied connections by configuring a weak set of cipher suites, compromising the confidentiality and integrity of proxied traffic. + + **Threat**: Exploit weak cipher suite configuration to downgrade security of proxied connections. + + **Recommendation**: Users operating in highly regulated environments may need to tightly control the TLS protocol and associated cipher suites, blocking non-conforming incoming connections to the gateway. + + EnvoyProxy bootstrap config can be customised as per the [customise EnvoyProxy](../operations/customize-envoyproxy) documentation. In addition, from v.1.0.0, it is possible to configure common TLS properties for a Gateway or XRoute through the [ClientTrafficPolicy](https://gateway.envoyproxy.io/latest/api/extension_types/#clienttrafficpolicy) object. + +### EGTM-005 Envoy Gateway Helm chart deployment does not set AppArmor and Seccomp profiles + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-005|EGTM-CP-002|Container Security|Low| + + **Risk**: Threat actor who has obtained access to Envoy Gateway pod could exploit the lack of AppArmor and Seccomp profiles in the Envoy Gateway deployment to attempt a container breakout, given the presence of an exploitable vulnerability, potentially impacting the confidentiality and integrity node resources. + + **Threat**: Unauthorised syscalls and malicious code running in the Envoy Gateway pod. + + **Recommendation**: Implement [AppArmor](https://kubernetes.io/docs/tutorials/security/apparmor/) policies by setting \: \ within container.apparmor.security.beta.kubernetes.io (note, this config is set *per container*). Well-defined AppArmor policies may provide greater protection from unknown threats. + + Enforce [Seccomp](https://kubernetes.io/docs/tutorials/security/seccomp/) profiles by setting the seccompProfile under securityContext. Ideally, a [fine-grained](https://kubernetes.io/docs/tutorials/security/seccomp/#create-pod-with-a-seccomp-profile-that-only-allows-necessary-syscalls) profile should be used to restrict access to only necessary syscalls, however the \--seccomp-default flag can be set to resort to [RuntimeDefault](https://kubernetes.io/docs/tutorials/security/seccomp/#create-pod-that-uses-the-container-runtime-default-seccomp-profile) which provides a container runtime specific. Example seccomp profiles can be found [here](https://kubernetes.io/docs/tutorials/security/seccomp/#download-profiles). + + To further enhance pod security, consider implementing [SELinux](https://en.wikipedia.org/wiki/Security-Enhanced_Linux) via seLinuxOptions for additional syscall attack surface reduction. Setting readOnlyRootFilesystem == true enforces an immutable root filesystem, preventing the addition of malicious binaries to the PATH and increasing the attack cost. Together, these configuration items improve the pods [Security Context](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/). + +### EGTM-006 Envoy Proxy pods deployed with a shell enabled + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-006|EGTM-CS-004|Container Security|Low| + + **Risk**: There is a risk that a threat actor exploits a vulnerability in Envoy Proxy to expose a reverse shell, enabling them to compromise the confidentiality, integrity and availability of tenant data via a secondary attack. + + **Threat**: If an external attacker managed to exploit a vulnerability in Envoy, the presence of a shell would be greatly helpful for the attacker in terms of potentially pivoting, escalating, or establishing some form of persistence. + + **Recommendation**: By default, Envoy uses a [distroless](https://github.com/GoogleContainerTools/distroless) image since v.0.6.0, which does not ship a shell. Therefore, ensure EnvoyProxy image is up-to-date and patched with the latest stable version. + + If using private EnvoyProxy images, use a lightweight EnvoyProxy image without a shell or debugging tool(s) which may be useful for an attacker. + + An [AuditPolicy](https://kubernetes.io/docs/tasks/debug/debug-cluster/audit/#audit-policy) (audit.k8s.io/v1beta1) can be configured to record API calls made within your cluster, allowing for identification of malicious traffic and enabling incident response. Requests are recorded based on stages which delineate between the lifecycle stage of the request made (e.g., RequestReceived, ResponseStarted, & ResponseComplete). + +### EGTM-011 Route Bindings on custom labels + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-011|EGTM-GW-003|Gateway API|Low| + + **Risk**: There is a risk that a gateway owner (or someone with the ability to set namespace labels) maliciously or accidentally binds routes across namespace boundaries, potentially compromising the confidentiality and integrity of traffic in a multitenant scenario. + + **Threat**: If a Route Binding within a Gateway Listener is configured based on a custom label, it could allow a malicious internal actor with the ability to label namespaces to change the set of namespaces supported by the Gateway. + + **Recommendation**: Consider the use of custom admission control to restrict what labels can be set on namespaces through tooling such as [Kubewarden](https://kyverno.io/policies/pod-security/), [Kyverno](https://github.com/kubewarden), and [OPA Gatekeeper](https://github.com/open-policy-agent/gatekeeper). Route binding should follow the Kubernetes Gateway API security model, as shown [here](https://gateway-api.sigs.k8s.io/concepts/security-model/#1-route-binding), to connect gateways in different namespaces. + +### EGTM-013 GatewayClass namespace validation is not configured + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-013|EGTM-GW-005|Gateway API|Low| + + **Risk**: There is a risk that an unauthorised actor deploys an unauthorised GatewayClass due to GatewayClass namespace validation not being configured, leading to non-compliance with business and security requirements. + + **Threat**: Unauthorised deployment of Gateway resource via GatewayClass template which crosses namespace trust boundaries. + + **Recommendation**: Leverage GatewayClass namespace validation to limit the namespaces where GatewayClasses can be run through a tool such as [OPA Gatekeeper](https://github.com/open-policy-agent/gatekeeper). Reference pull request \#[24](https://github.com/open-policy-agent/gatekeeper-library/pull/24) within gatekeeper-library which outlines how to add GatewayClass namespace validation through a GatewayClassNamespaces API resource kind within the constraints.gatekeeper.sh/v1beta1 apiGroup. + +### EGTM-015 ServiceAccount token authentication + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-015|EGTM-CS-007|Container Security|Low| + + **Risk**: There is a risk that threat actors could exploit ServiceAccount tokens for illegitimate authentication, thereby leading to privilege escalation and the undermining of gateway API resources\' integrity, confidentiality, and availability. + + **Threat**: The threat arises from threat actors impersonating the envoy-gateway ServiceAccount through the replay of ServiceAccount tokens, thereby achieving escalated privileges and gaining unauthorised access to Kubernetes resources. + + **Recommendation**: Limit the creation of ServiceAccounts to only when necessary, specifically refraining from using default service account tokens, especially for high-privilege service accounts. For legacy clusters running Kubernetes version 1.21 or earlier, note that ServiceAccount tokens are long-lived by default. To disable the automatic mounting of the service account token, set automountServiceAccountToken: false in the PodSpec. + +### EGTM-016 Misconfiguration leads to lack of Envoy Proxy access activity visibility + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-016|EGTM-EG-004|Envoy Gateway|Low| + + **Risk**: There is a risk that threat actors establish persistence and move laterally through the cluster unnoticed due to limited visibility into access and application-level activity. + + **Threat**: Threat actors establish persistence and move laterally through the cluster unnoticed. + + **Recommendation**: Configure [access logging](../../../contributions/design/proxy-accesslog) in the EnvoyProxy. Use [ProxyAccessLogFormatType](../../api/extension_types#proxyaccesslogformattype) (Text or JSON) to specify the log format and ensure that the logs are sent to the desired sink types by setting the [ProxyAccessLogSinkType](https://gateway.envoyproxy.io/latest/api/extension_types/#proxyaccesslogsinktype). Make use of [FileEnvoyProxyAccessLog](https://gateway.envoyproxy.io/latest/api/extension_types/#fileenvoyproxyaccesslog) or [OpenTelemetryEnvoyProxyAccessLog](https://gateway.envoyproxy.io/latest/api/extension_types/#opentelemetryenvoyproxyaccesslog) to configure File and OpenTelemetry sinks, respectively. If the settings aren\'t defined, the default format is sent to stdout. + + Additionally, consider leveraging a central logging mechanism such as [Fluentd](https://github.com/fluent/fluentd) to enhance visibility into access activity and enable effective incident response (IR). + +### EGTM-017 Misconfiguration leads to lack of Envoy Gateway activity visibility + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-017|EGTM-EG-005|Envoy Gateway|Low| + + **Risk**: There is a risk that an insider misconfigures an envoy gateway component and goes unnoticed due to a low-touch logging configuration (via default) which responsible stakeholders are not aptly aware of or have immediate access to. + + **Threat**: The threat emerges from an insider misconfiguring an Envoy Gateway component without detection. + + **Recommendation**: Configure the logging level of the Envoy Gateway using the \'level\' field in [EnvoyGatewayLogging](https://gateway.envoyproxy.io/latest/api/extension_types/#envoygatewaylogging). Ensure the appropriate logging levels are set for relevant components such as \'gateway-api\', \'xds-translator\', or \'global-ratelimit\'. If left unspecified, the logging level defaults to \"info\", which may not provide sufficient detail for security monitoring. + + Employ a centralised logging mechanism, like [Fluentd](https://github.com/fluent/fluentd), to enhance visibility into application-level activity and to enable efficient incident response. + +### EGTM-021 Exposed Envoy Proxy admin interface + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|---------------|-----------------------|-----------------| +|EGTM-021|EGTM-EG-006|Envoy Gateway|Low| + + **Risk**: There is a risk that the admin interface is exposed without valid business reason, increasing the attack surface. + + **Threat**: Exposed admin interfaces give internal attackers the option to affect production traffic in unauthorised ways, and the option to exploit any vulnerabilities which may be present in the admin interface (e.g. by orchestrating malicious GET requests to the admin interface through CSRF, compromising Envoy Proxy global configuration or shutting off the service entirely e.g. /quitquitquit). + + **Recommendation**: The Envoy Proxy admin interface is only exposed to localhost, meaning that it is secure by default. However, due to the risk of misconfiguration, this recommendation is included. + + Due to the importance of the admin interface, it is recommended to ensure that Envoy Proxies have not been accidentally misconfigured to expose the admin interface to untrusted networks. + +### EGTM-025 Envoy Proxy pods deployed running as root user in the container + +|**ID**|**UID**|**Category**|**Priority**| +|--------------|--------------|------------------------|-----------------| +|EGTM-025|EGTM-CS-011|Container Security|Low| + +**Risk**: The presence of a vulnerability, be it in the kernel or another system component, when coupled with containers running as root, could enable a threat actor to escape the container, thereby compromising the confidentiality, integrity, or availability of cluster resources + + **Threat**: The Envoy Proxy container's root-user configuration can be leveraged by an attacker to escalate privileges, execute a container breakout, and traverse across trust boundaries. + + **Recommendation**: By default, Envoy Gateway deployments do not use root users. Nonetheless, in case a custom image or deployment manifest is to be used, make sure Envoy Proxy pods run as a non-root user with a high UID within the container. + +Set runAsUser and runAsGroup security context options to specific UIDs (e.g., runAsUser: 1000 & runAsGroup: 3000) to ensure the container operates with the stipulated non-root user and group ID. If using helm chart deployment, define the user and group ID in the values.yaml file or via the command line during helm install / upgrade. + +## Appendix + +### In Scope Threat Actor Details + +|Threat Actor | Capability | Personal Motivation | Envoy Gateway Attack Samples| +|-|-|-|-| +|Application Developer | Leverage internal knowledge and personal access to the Envoy Gateway infrastructure to move laterally and transit trust boundaries | Disgruntled / personal grievances.

Financial incentives | Misconfigure XRoute resources to expose internal applications.

Misconfigure SecurityPolicy objects, reducing the security posture of an application.| +|Application Administrator | Abuse privileged status to disrupt operations and tenant cluster services through Envoy Gateway misconfig | Disgruntled / personal grievances.

Financial incentives | Create malicious routes to internal applications.

Introduce malicious Envoy Proxy images.

Expose the Envoy Proxy Admin interface.| +|Cluster Operator | Alter application-level deployments by misconfiguring resource dependencies & SCM to introduce vulnerabilities | Disgruntled / personal grievances.

Financial incentives.

Notoriety | Deploy malicious resources to expose internal applications.

Access authentication secrets.

Fall victim to phishing attacks and inadvertently share authentication credentials to cloud infrastructure or Kubernetes clusters.| +|Vandal: Script Kiddie, Trespasser | Uses publicly available tools and applications (Nmap,Metasploit, CVE PoCs) | Curiosity.

Personal fame through defacement / denial of service of prominent public facing web resources | Small scale DOS.

Launches prepackaged exploits, runs crypto mining tools.

Exploit public-facing application services such as the bastion host to gain an initial foothold in the environment| +|Motivated individual: Political activist, Thief, Terrorist | Write tools and exploits required for their means if sufficiently motivated.

Tend to use these in a targeted fashion against specific organisations. May combine publicly available exploits in a targeted fashion. Tamper with open source supply chains | Personal Gain (Political or Ideological) | Phishing, DDOS, exploit known vulnerabilities.

Compromise third-party components such as Helm charts and container images to inject malicious codes to propagate access throughout the environment.| +|Organised crime: syndicates, state-affiliated groups | Write tools and exploits required for their means.

Tend to use these in a non-targeted fashion, unless motivation is sufficiently high.

Devotes considerable resources, writes exploits, can bribe/coerce, can launch targeted attacks | Ransom.

Mass extraction of PII / credentials / PCI data.

Financial incentives | Social Engineering, phishing, ransomware, coordinated attacks.

Intercept and replay JWT tokens (via MiTM) between tenant user(s) and envoy gateway to modify app configs in-transit| + +### Identified Threats by Priority + +|ID|UID|Category|Risk|Threat|Priority| Recommendation | +|-|-|-|-|-|-|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|EGTM-001|EGTM-GW-001|Gateway API| Self-signed certificates (which do not comply with PKI best practices) could lead to unauthorised access to the private key associated with the certificate used for inbound TLS termination at Envoy Proxy, compromising the confidentiality and integrity of proxied traffic.

| Compromise of the private key associated with the certificate used for inbound TLS terminating at Envoy Proxy.

|High| The Envoy Gateway quickstart demonstrates how to set up a Secure Gateway using an example where a self-signed root certificate is created using openssl. As stated in the Envoy Gateway documentation, this is not a suitable configuration for Production usage. It is recommended that PKI best practices are followed, whereby certificates are signed by an Intermediary CA which sits underneath an organisational \'offline\' Root CA.

PKI best practices should also apply to the management of client certificates when using mTLS. The Envoy Gateway [mTLS](../security/mutual-tls) task shows how to set up client certificates using self-signed certificates. In the same way as gateway certificates and, as mentioned in the documentation, this configuration should not be used in production environments. | +|EGTM-002|EGTM-CS-001|Container Security| There is a risk that a threat actor could compromise the Kubernetes secret containing the Envoy private key, allowing the attacker to decrypt Envoy Proxy traffic, compromising the confidentiality of proxied traffic.

| Kubernetes secret containing the Envoy private key is compromised and used to decrypt proxied traffic.

|High| Certificate management best practices mandate short-lived key material where practical, meaning that a mechanism for rotation of private keys and certificates is required, along with a way for certificates to be mounted into Envoy containers. If Kubernetes secrets are used, when a certificate expires, the associated secret must be updated, and Envoy containers must be redeployed. Instead of a manual configuration, it is recommended that [cert-manager](https://github.com/cert-manager/cert-manager) is used. | +|EGTM-004|EGTM-K8-002|Container Security| There is a risk that a threat actor could abuse misconfigured RBAC to access the Envoy Gateway ClusterRole (envoy-gateway-role) and use it to expose all secrets across the cluster, thus compromising the confidentiality and integrity of tenant data.

| Compromised Envoy Gateway or misconfigured ClusterRoleBinding (envoy-gateway-rolebinding) to Envoy Gateway ClusterRole (envoy-gateway-role), provides access to resources and secrets in different namespaces.

|High| Users should be aware that Envoy Gateway uses a ClusterRole (envoy-gateway-role) when deployed via the Helm chart, to allow management of Envoy Proxies across different namespaces. This ClusterRole is powerful and includes the ability to read secrets in namespaces which may not be within the purview of Envoy Gateway.

Kubernetes best-practices involve restriction of ClusterRoleBindings, with the use of RoleBindings where possible to limit access per namespace by specifying the namespace in metadata. Namespace isolation reduces the impact of compromise from cluster-scoped roles. Ideally, fine-grained K8s roles should be created per the principle of least privilege to ensure they have the minimum access necessary for role functions.

The pull request \#[1656](https://github.com/envoyproxy/gateway/pull/1656) introduced the use of Roles and RoleBindings in [namespaced mode](https://gateway.envoyproxy.io/latest/api/extension_types/#kuberneteswatchmode). This feature can be leveraged to reduce the amount of permissions required by the Envoy Gateway. | +|EGTM-007|EGTM-EG-002|Envoy Gateway| There is a risk that a threat actor could exploit misconfigured Kubernetes RBAC to create or modify Gateway API resources with no business need, potentially leading to the compromise of the confidentiality, integrity, and availability of resources and traffic within the cluster.

| Unauthorised creation or misconfiguration of Gateway API resources by a threat actor with cluster-scoped access.

|High| Configure the apiGroup and resource fields in RBAC policies to restrict access to [Gateway](https://gateway-api.sigs.k8s.io/) and [GatewayClass](https://gateway-api.sigs.k8s.io/api-types/gatewayclass/) resources. Enable namespace isolation by using the namespace field, preventing unauthorised access to gateways in other namespaces. | +|EGTM-009|EGTM-GW-002|Gateway API| There is a risk that a co-tenant misconfigures Gateway or Route resources, compromising the confidentiality, integrity, and availability of routed traffic through Envoy Gateway.

| Malicious or accidental co-tenant misconfiguration of Gateways and Routes associated with other application teams.

|High| Dedicated Envoy Gateways should be provided to each tenant within their respective namespace. A one-to-one relationship should be established between GatewayClass and Gateway resources, meaning that each tenant namespace should have their own GatewayClass watched by a unique Envoy Gateway Controller as defined here in the [Deployment Mode](../operations/deployment-mode) documentation.

Application Admins should have write permissions on the Gateway resource, but only in their specific namespaces, and Application Developers should only hold write permissions on Route resources. To enact this access control schema, follow the [Write Permissions for Advanced 4 Tier Model](https://gateway-api.sigs.k8s.io/concepts/security-model/#write-permissions-for-advanced-4-tier-model) described in the Kubernetes Gateway API security model. Examples of secured gateway-route topologies can be found [here](https://gateway-api.sigs.k8s.io/concepts/api-overview/#attaching-routes-to-gateways) within the Kubernetes Gateway API docs.

Optionally, consider a GitOps model, where only the GitOps operator has the permission to deploy or modify custom resources in production. | +|EGTM-014|EGTM-CS-006|Container Security| There is a risk that a supply chain attack on Envoy Gateway results in an arbitrary compromise of the confidentiality, integrity or availability of tenant data.

| Supply chain threat actor introduces malicious code into Envoy Gateway or Proxy.

|High| The Envoy Gateway project should continue to work towards conformance with supply-chain security best practices throughout the project lifecycle (for example, as set out in the [CNCF Software Supply Chain Best Practices Whitepaper](https://github.com/cncf/tag-security/blob/main/supply-chain-security/supply-chain-security-paper/CNCF_SSCP_v1.pdf). Adherence to [Supply-chain Levels for Software Artefacts](https://slsa.dev/) (SLSA) standards is crucial for maintaining the security of the system. Employ version control systems to monitor the source and build platforms and assign responsibility to a specific stakeholder.

Integrate a supply chain security tool such as Sigstore, which provides native capabilities for signing and verifying container images and software artefacts. [Software Bill of Materials](https://www.cisa.gov/sbom) (SBOM), [Vulnerability Exploitability eXchange](https://www.ntia.gov/files/ntia/publications/vex_one-page_summary.pdf) (VEX), and signed artefacts should also be incorporated into the security protocol. | +|EGTM-020|EGTM-CS-009|Container Security| There is a risk that a threat actor exploits an Envoy Proxy vulnerability to remote code execution (RCE) due to out of date or misconfigured Envoy Proxy pod deployment, compromising the confidentiality and integrity of Envoy Proxy along with the availability of the proxy service.

| Deployment of an Envoy Proxy or Gateway image containing exploitable CVEs.

|High| Always use the latest version of the Envoy Proxy image. Regularly check for updates and patch the system as soon as updates become available. Implement a CI/CD pipeline that includes security checks for images and prevents deployment of insecure configurations. A tool such as Snyk can be used to provide container vulnerability scanning to mitigate the risk of known vulnerabilities.

Utilise the [Pod Security Admission](https://kubernetes.io/docs/concepts/security/pod-security-admission/) controller to enforce [Pod Security Standards](https://kubernetes.io/docs/concepts/security/pod-security-standards/) and configure the [pod security context](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/) to limit its capabilities per the principle of least privilege. | +|EGTM-022|EGTM-CS-010|Container Security| There is a risk that the OIDC client secret (for OIDC authentication) and user password hashes (for basic authentication) get leaked due to misconfigured RBAC permissions.

| Unauthorised access to the application due to credential leakage.

|High| Ensure that only authorised users and service accounts are able to access secrets. This is especially important in namespaces where SecurityPolicy objects are configured, since those namespaces are the ones to store secrets containing the client secret (in OIDC scenarios) and user password hashes (in basic authentication scenarios).

To do so, minimise the use of ClusterRoles and Roles allowing listing and getting secrets. Perform periodic audits of RBAC permissions. | +|EGTM-023|EGTM-EG-007|Envoy Gateway| There is a risk of unauthorised access due to the use of basic authentication, which does not enforce any password restriction in terms of complexity and length. In addition, password hashes are stored in SHA1 format, which is a deprecated hashing function.

| Unauthorised access to the application due to weak authentication mechanisms.

|High| It is recommended to make use of stronger authentication mechanisms (i.e. JWT authentication and OIDC authentication) instead of basic authentication. These authentication mechanisms have many advantages, such as the use of short-lived credentials and a central management of security policies through the identity provider. | +|EGTM-008|EGTM-EG-003|Envoy Gateway| There is a risk of a threat actor misconfiguring static config and compromising the integrity of Envoy Gateway, ultimately leading to the compromised confidentiality, integrity, or availability of tenant data and cluster resources.

| Accidental or deliberate misconfiguration of static configuration leads to a misconfigured deployment of Envoy Gateway, for example logging parameters could be modified or global rate limiting configuration misconfigured.

|Medium| Implement a GitOps model, utilising Kubernetes\' Role-Based Access Control (RBAC) and adhering to the principle of least privilege to minimise human intervention on the cluster. For instance, tools like [ArgoCD](https://argo-cd.readthedocs.io/en/stable/) can be used for declarative GitOps deployments, ensuring all changes are tracked and reviewed. Additionally, configure your source control management (SCM) system to include mandatory pull request (PR) reviews, commit signing, and protected branches to ensure only authorised changes can be committed to the start-up configuration. | +|EGTM-010|EGTM-CS-005|Container Security| There is a risk that a threat actor exploits a weak pod security context, compromising the CIA of a node and the resources / services which run on it.

| Threat Actor who has compromised a pod exploits weak security context to escape to a node, potentially leading to the compromise of Envoy Proxy or Gateway running on the same node.

|Medium| To mitigate this risk, apply [Pod Security Standards](https://kubernetes.io/docs/concepts/security/pod-security-standards/) at a minimum of [Baseline](https://kubernetes.io/docs/concepts/security/pod-security-standards/#baseline) level to all namespaces, especially those containing Envoy Gateway and Proxy Pods. Pod security standards are implemented through K8s [Pod Security Admission](https://kubernetes.io/docs/concepts/security/pod-security-admission/) to provide [admission control modes](https://kubernetes.io/docs/concepts/security/pod-security-admission/#pod-security-admission-labels-for-namespaces) (enforce, audit, and warn) for namespaces. Pod security standards can be enforced by namespace labels as shown [here](https://kubernetes.io/docs/tasks/configure-pod-container/enforce-standards-namespace-labels/), to enforce a baseline level of pod security to specific namespaces.

Further enhance the security by implementing a sandboxing solution such as [gVisor](https://gvisor.dev/) for Envoy Gateway and Proxy Pods to isolate the application from the host kernel. This can be set within the runtimeClassName of the Pod specification. | +|EGTM-012|EGTM-GW-004|Gateway API| There is a risk that a threat actor could abuse excessive RBAC privileges to create ReferenceGrant resources. These resources could then be used to create cross-namespace communication, leading to unauthorised access to the application. This could compromise the confidentiality and integrity of resources and configuration in the affected namespaces and potentially disrupt the availability of services that rely on these object references.

| A ReferenceGrant is created, which validates traffic to cross namespace trust boundaries without a valid business reason, such as a route in one tenant\'s namespace referencing a backend in another.

|Medium| Ensure that the ability to create ReferenceGrant resources is restricted to the minimum number of people. Pay special attention to ClusterRoles that allow that action. | +|EGTM-018|EGTM-GW-006|Gateway API| There is a risk that malicious requests could lead to a Denial of Service (DoS) attack, thereby reducing API gateway availability due to misconfigurations in rate-limiting or load balancing controls, or a lack of route timeout enforcement.

| Reduced API gateway availability due to an attacker\'s maliciously crafted request (e.g., QoD) potentially inducing a Denial of Service (DoS) attack.

|Medium| To ensure high availability and to mitigate potential security threats, adhere to the Envoy Gateway documentation for the configuration of a [rate-limiting](../traffic/global-rate-limit) filter and load balancing.

Further, adhere to best practices for configuring Envoy Proxy as an edge proxy documented [here](https://www.envoyproxy.io/docs/envoy/latest/configuration/best_practices/edge#configuring-envoy-as-an-edge-proxy) within the EnvoyProxy docs. This involves configuring TCP and HTTP proxies with specific settings, including restricting access to the admin endpoint, setting the [overload manager](https://www.envoyproxy.io/docs/envoy/latest/configuration/operations/overload_manager/overload_manager#config-overload-manager) and [listener](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) / [cluster](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) buffer limits, enabling [use_remote_address](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-use-remote-address), setting [connection and stream timeouts](https://www.envoyproxy.io/docs/envoy/latest/faq/configuration/timeouts#faq-configuration-timeouts), limiting [maximum concurrent streams](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-field-config-core-v3-http2protocoloptions-max-concurrent-streams), setting [initial stream window size limit](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-field-config-core-v3-http2protocoloptions-initial-stream-window-size), and configuring action on [headers_with_underscores](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-field-config-core-v3-httpprotocoloptions-headers-with-underscores-action).

[Path normalisation](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-normalize-path) should be enabled to minimise path confusion vulnerabilities. These measures help protect against volumetric threats such as Denial of Service (DoS)nattacks. Utilise custom resources to implement policy attachment, thereby exposing request limit configuration for route types. | +|EGTM-019|EGTM-DP-004|Container Security| There is a risk that replay attacks using stolen or reused JSON Web Tokens (JWTs) can compromise transmission integrity, thereby undermining the confidentiality and integrity of the data plane.

| Transmission integrity is compromised due to replay attacks using stolen or reused JSON Web Tokens (JWTs).

|Medium| Comply with JWT best practices for enhanced security, paying special attention to the use of short-lived tokens, which reduce the window of opportunity for a replay attack. The [exp](https://datatracker.ietf.org/doc/html/rfc7519#page-9) claim can be used to set token expiration times. | +|EGTM-024|EGTM-EG-008|Envoy Gateway| There is a risk of developers getting more privileges than required due to the use of SecurityPolicy, ClientTrafficPolicy, EnvoyPatchPolicy and BackendTrafficPolicy. These resources can be attached to a Gateway resource. Therefore, a developer with permission to deploy them would be able to modify a Gateway configuration by targeting the gateway in the policy manifest. This conflicts with the [Advanced 4 Tier Model](https://gateway-api.sigs.k8s.io/concepts/security-model/#write-permissions-for-advanced-4-tier-model), where developers do not have write permissions on Gateways.

| Excessive developer permissions lead to a misconfiguration and/or unauthorised access.

|Medium| Considering the Tenant C scenario (represented in the Architecture Diagram), if a developer can create SecurityPolicy, ClientTrafficPolicy, EnvoyPatchPolicy or BackendTrafficPolicy objects in namespace C, they would be able to modify a Gateway configuration by attaching the policy to the gateway. In such scenarios, it is recommended to either:

a. Create a separate namespace, where developers have no permissions, > to host tenant C\'s gateway. Note that, due to design decisions, > the > SecurityPolicy/EnvoyPatchPolicy/ClientTrafficPolicy/BackendTrafficPolicy > object can only target resources deployed in the same namespace. > Therefore, having a separate namespace for the gateway would > prevent developers from attaching the policy to the gateway.

b. Forbid the creation of these policies for developers in namespace C.

On the other hand, in scenarios similar to tenants A and B, where a shared gateway namespace is in place, this issue is more limited. Note that in this scenario, developers don\'t have access to the shared gateway namespace.

In addition, it is important to mention that EnvoyPatchPolicy resources can also be attached to GatewayClass resources. This means that, in order to comply with the Advanced 4 Tier model, individuals with the Application Administrator role should not have access to this resource either. | +|EGTM-003|EGTM-EG-001|Envoy Gateway| There is a risk that a threat actor could downgrade the security of proxied connections by configuring a weak set of cipher suites, compromising the confidentiality and integrity of proxied traffic.

| Exploit weak cipher suite configuration to downgrade security of proxied connections.

|Low| Users operating in highly regulated environments may need to tightly control the TLS protocol and associated cipher suites, blocking non-conforming incoming connections to the gateway.

EnvoyProxy bootstrap config can be customised as per the [customise EnvoyProxy](../operations/customize-envoyproxy) documentation. In addition, from v.1.0.0, it is possible to configure common TLS properties for a Gateway or XRoute through the [ClientTrafficPolicy](https://gateway.envoyproxy.io/latest/api/extension_types/#clienttrafficpolicy) object. | +|EGTM-005|EGTM-CP-002|Container Security| Threat actor who has obtained access to Envoy Gateway pod could exploit the lack of AppArmor and Seccomp profiles in the Envoy Gateway deployment to attempt a container breakout, given the presence of an exploitable vulnerability, potentially impacting the confidentiality and integrity of namespace resources.

| Unauthorised syscalls and malicious code running in the Envoy Gateway pod.

|Low| Implement [AppArmor](https://kubernetes.io/docs/tutorials/security/apparmor/) policies by setting \: \ within container.apparmor.security.beta.kubernetes.io (note, this config is set *per container*). Well-defined AppArmor policies may provide greater protection from unknown threats.

Enforce [Seccomp](https://kubernetes.io/docs/tutorials/security/seccomp/) profiles by setting the seccompProfile under securityContext. Ideally, a [fine-grained](https://kubernetes.io/docs/tutorials/security/seccomp/#create-pod-with-a-seccomp-profile-that-only-allows-necessary-syscalls) profile should be used to restrict access to only necessary syscalls, however the \--seccomp-default flag can be set to resort to [RuntimeDefault](https://kubernetes.io/docs/tutorials/security/seccomp/#create-pod-that-uses-the-container-runtime-default-seccomp-profile) which provides a container runtime specific. Example seccomp profiles can be found [here](https://kubernetes.io/docs/tutorials/security/seccomp/#download-profiles).

To further enhance pod security, consider implementing [SELinux](https://en.wikipedia.org/wiki/Security-Enhanced_Linux) via seLinuxOptions for additional syscall attack surface reduction. Setting readOnlyRootFilesystem == true enforces an immutable root filesystem, preventing the addition of malicious binaries to the PATH and increasing the attack cost. Together, these configuration items improve the pods [Security Context](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/). | +|EGTM-006|EGTM-CS-004|Container Security| There is a risk that a threat actor exploits a vulnerability in Envoy Proxy to expose a reverse shell, enabling them to compromise the confidentiality, integrity and availability of tenant data via a secondary attack.

| If an external attacker managed to exploit a vulnerability in Envoy, the presence of a shell would be greatly helpful for the attacker in terms of potentially pivoting, escalating, or establishing some form of persistence.

|Low| By default, Envoy uses a [distroless](https://github.com/GoogleContainerTools/distroless) image since v.0.6.0, which does not ship a shell. Therefore, ensure EnvoyProxy image is up-to-date and patched with the latest stable version.

If using private EnvoyProxy images, use a lightweight EnvoyProxy image without a shell or debugging tool(s) which may be useful for an attacker.

An [AuditPolicy](https://kubernetes.io/docs/tasks/debug/debug-cluster/audit/#audit-policy) (audit.k8s.io/v1beta1) can be configured to record API calls made within your cluster, allowing for identification of malicious traffic and enabling incident response. Requests are recorded based on stages which delineate between the lifecycle stage of the request made (e.g., RequestReceived, ResponseStarted, & ResponseComplete). | +|EGTM-011|EGTM-GW-003|Gateway API| There is a risk that a gateway owner (or someone with the ability to set namespace labels) maliciously or accidentally binds routes across namespace boundaries, potentially compromising the confidentiality and integrity of traffic in a multitenant scenario.

| If a Route Binding within a Gateway Listener is configured based on a custom label, it could allow a malicious internal actor with the ability to label namespaces to change the set of namespaces supported by the Gateway

|Low| Consider the use of custom admission control to restrict what labels can be set on namespaces through tooling such as [Kubewarden](https://kyverno.io/policies/pod-security/), [Kyverno](https://github.com/kubewarden), and [OPA Gatekeeper](https://github.com/open-policy-agent/gatekeeper). Route binding should follow the Kubernetes Gateway API security model, as shown [here](https://gateway-api.sigs.k8s.io/concepts/security-model/#1-route-binding), to connect gateways in different namespaces. | +|EGTM-013|EGTM-GW-005|Gateway API| There is a risk that an unauthorised actor deploys an unauthorised GatewayClass due to GatewayClass namespace validation not being configured, leading to non-compliance with business and security requirements.

| Unauthorised deployment of Gateway resource via GatewayClass template which crosses namespace trust boundaries.

|Low| Leverage GatewayClass namespace validation to limit the namespaces where GatewayClasses can be run through a tool such as using [OPA Gatekeeper](https://github.com/open-policy-agent/gatekeeper). Reference pull request \#[24](https://github.com/open-policy-agent/gatekeeper-library/pull/24) within gatekeeper-library which outlines how to add GatewayClass namespace validation through a GatewayClassNamespaces API resource kind within the constraints.gatekeeper.sh/v1beta1 apiGroup. | +|EGTM-015|EGTM-CS-007|Container Security| There is a risk that threat actors could exploit ServiceAccount tokens for illegitimate authentication, thereby leading to privilege escalation and the undermining of gateway API resources\' integrity, confidentiality, and availability.

| The threat arises from threat actors impersonating the envoy-gateway ServiceAccount through the replay of ServiceAccount tokens, thereby achieving escalated privileges and gaining unauthorised access to Kubernetes resources.

|Low| Limit the creation of ServiceAccounts to only when necessary, specifically refraining from using default service account tokens, especially for high-privilege service accounts. For legacy clusters running Kubernetes version 1.21 or earlier, note that ServiceAccount tokens are long-lived by default. To disable the automatic mounting of the service account token, set automountServiceAccountToken: false in the PodSpec. | +|EGTM-016|EGTM-EG-004|Envoy Gateway| There is a risk that threat actors establish persistence and move laterally through the cluster unnoticed due to limited visibility into access and application-level activity.

| Threat actors establish persistence and move laterally through the cluster unnoticed.

|Low| Configure [access logging](../../../contributions/design/proxy-accesslog) in the EnvoyProxy. Use [ProxyAccessLogFormatType](../../api/extension_types#proxyaccesslogformattype) (Text or JSON) to specify the log format and ensure that the logs are sent to the desired sink types by setting the [ProxyAccessLogSinkType](https://gateway.envoyproxy.io/latest/api/extension_types/#proxyaccesslogsinktype). Make use of [FileEnvoyProxyAccessLog](https://gateway.envoyproxy.io/latest/api/extension_types/#fileenvoyproxyaccesslog) or [OpenTelemetryEnvoyProxyAccessLog](https://gateway.envoyproxy.io/latest/api/extension_types/#opentelemetryenvoyproxyaccesslog) to configure File and OpenTelemetry sinks, respectively. If the settings aren\'t defined, the default format is sent to stdout.

Additionally, consider leveraging a central logging mechanism such as [Fluentd](https://github.com/fluent/fluentd) to enhance visibility into access activity and enable effective incident response (IR). | +|EGTM-017|EGTM-EG-005|Envoy Gateway| There is a risk that an insider misconfigures an envoy gateway component and goes unnoticed due to a low-touch logging configuration (via default) which responsible stakeholders are not aptly aware of or have immediate access to.

| The threat emerges from an insider misconfiguring an Envoy Gateway component without detection.

|Low| Configure the logging level of the Envoy Gateway using the \'level\' field in [EnvoyGatewayLogging](https://gateway.envoyproxy.io/latest/api/extension_types/#envoygatewaylogging). Ensure the appropriate logging levels are set for relevant components such as \'gateway-api\', \'xds-translator\', or \'global-ratelimit\'. If left unspecified, the logging level defaults to \"info\", which may not provide sufficient detail for security monitoring.

Employ a centralised logging mechanism, like [Fluentd](https://github.com/fluent/fluentd), to enhance visibility into application-level activity and to enable efficient incident response. | +|EGTM-021|EGTM-EG-006|Envoy Gateway| There is a risk that the admin interface is exposed without valid business reason, increasing the attack surface.

| Exposed admin interfaces give internal attackers the option to affect production traffic in unauthorised ways, and the option to exploit any vulnerabilities which may be present in the admin interface (e.g. by orchestrating malicious GET requests to the admin interface through CSRF, compromising Envoy Proxy global configuration or shutting off the service entirely (e.g., /quitquitquit).

|Low| The Envoy Proxy admin interface is only exposed to localhost, meaning that it is secure by default. However, due to the risk of misconfiguration, this recommendation is included.

Due to the importance of the admin interface, it is recommended to ensure that Envoy Proxies have not been accidentally misconfigured to expose the admin interface to untrusted networks. | +|EGTM-025 | EGTM-CS-011 | Container Security | The presence of a vulnerability, be it in the kernel or another system component, when coupled with containers running as root, could enable a threat actor to escape the container, thereby compromising the confidentiality, integrity, or availability of cluster resources. | The Envoy Proxy container's root-user configuration can be leveraged by an attacker to escalate privileges, execute a container breakout, and traverse across trust boundaries. | Low | By default, Envoy Gateway deployments do not use root users. Nonetheless, in case a custom image or deployment manifest is to be used, make sure Envoy Proxy pods run as a non-root user with a high UID within the container. Set runAsUser and runAsGroup security context options to specific UIDs (e.g., runAsUser: 1000 & runAsGroup: 3000) to ensure the container operates with the stipulated non-root user and group ID. If using helm chart deployment, define the user and group ID in the values.yaml file or via the command line during helm install / upgrade. | + + +## Attack Trees + +Attack trees offer a methodical way of describing the security of systems, based on varying attack patterns. It's important to approach the review of attack trees from a top-down perspective. The top node, also known as the root node, symbolises the attacker's primary objective. This goal is then broken down into subsidiary aims, each reflecting a different strategy to attain the root objective. This deconstruction persists until reaching the lowest level objectives or 'leaf nodes', which depict attacks that can be directly launched. + +It is essential to note that attack trees presented here are speculative paths for potential exploitation. The Envoy Gateway project is in a continuous development cycle, and as the project evolves, new vulnerabilities may be exposed, or additional controls could be introduced. Therefore, the threats illustrated in the attack trees should be perceived as point-in-time reflections of the project’s current state at the time of writing this threat model. + +### Node ID Schema + +Each node in the attack tree is assigned a unique identifier following the AT#-## schema. This allows easy reference to specific nodes in the attack trees throughout the threat model. The first part of the ID (AT#) signifies the attack tree number, while the second part (##) represents the node number within that tree. + +### Logical Operators + +Logical AND/OR operators are used to represent the relationship between parent and child nodes. An AND operator means that all child nodes must be achieved to satisfy the parent node. An OR operator between a parent node and its child nodes means that any of the child nodes can be achieved to satisfy the parent node. + +### Attack Tree Node Legend + +![AT Legend](/img/AT-legend.png) + +### AT0 + +![AT0](/img/AT0.png) + +### AT1 + +![AT1](/img/AT1.png) + +### AT2 + +![AT2](/img/AT2.png) + +### AT3 + +![AT3](/img/AT3.png) + +### AT4 + +![AT4](/img/AT4.png) + +### AT5 + +![AT5](/img/AT5.png) + +### AT6 + +![AT6](/img/AT6.png) diff --git a/site/content/en/v1.1/tasks/security/tls-cert-manager.md b/site/content/en/v1.1/tasks/security/tls-cert-manager.md new file mode 100644 index 00000000000..d51fa469e8c --- /dev/null +++ b/site/content/en/v1.1/tasks/security/tls-cert-manager.md @@ -0,0 +1,436 @@ +--- +title: "Using cert-manager For TLS Termination" +--- + +This task shows how to set up [cert-manager](https://cert-manager.io/) to automatically create certificates and secrets for use by Envoy Gateway. +It will first show how to enable the self-sign issuer, which is useful to test that cert-manager and Envoy Gateway can talk to each other. +Then it shows how to use [Let's Encrypt's staging environment](https://letsencrypt.org/docs/staging-environment/). +Changing to the Let's Encrypt production environment is straight-forward after that. + +## Prerequisites + +* A Kubernetes cluster and a configured `kubectl`. +* The `helm` command. +* The `curl` command or similar for testing HTTPS requests. +* For the ACME HTTP-01 challenge to work + * your Gateway must be reachable on the public Internet. + * the domain name you use (we use `www.example.com`) must point to the Gateway's external IP(s). + +## Installation + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## Deploying cert-manager + +*This is a summary of [cert-manager Installation with Helm](https://cert-manager.io/docs/installation/helm/).* + +Installing cert-manager is straight-forward, but currently (v1.12) requires setting a feature gate to enable the Gateway API support. + +```console +$ helm repo add jetstack https://charts.jetstack.io +$ helm upgrade --install --create-namespace --namespace cert-manager --set installCRDs=true --set featureGates=ExperimentalGatewayAPISupport=true cert-manager jetstack/cert-manager +``` + +You should now have `cert-manager` running with nothing to do: + +```console +$ kubectl wait --for=condition=Available deployment -n cert-manager --all +deployment.apps/cert-manager condition met +deployment.apps/cert-manager-cainjector condition met +deployment.apps/cert-manager-webhook condition met + +$ kubectl get -n cert-manager deployment +NAME READY UP-TO-DATE AVAILABLE AGE +cert-manager 1/1 1 1 42m +cert-manager-cainjector 1/1 1 1 42m +cert-manager-webhook 1/1 1 1 42m +``` + +## A Self-Signing Issuer + +cert-manager can have any number of *issuer* configurations. +The simplest issuer type is [SelfSigned](https://cert-manager.io/docs/configuration/selfsigned/). +It simply takes the certificate request and signs it with the private key it generates for the TLS Secret. + +``` +Self-signed certificates don't provide any help in establishing trust between certificates. +However, they are great for initial testing, due to their simplicity. +``` + +To install self-signing, run + +```console +$ kubectl apply -f - <}} +{{% tab header="With External LoadBalancer Support" %}} + +You can also test the same functionality by sending traffic to the External IP of the Gateway: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +Curl the example app through the Gateway, e.g. Envoy proxy: + +```shell +curl -v -HHost:passthrough.example.com --resolve "passthrough.example.com:6443:${GATEWAY_HOST}" \ +--cacert example.com.crt https://passthrough.example.com:6443/get +``` + +{{% /tab %}} +{{% tab header="Without LoadBalancer Support" %}} + +Get the name of the Envoy service created the by the example Gateway: + +```shell +export ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}') +``` + +Port forward to the Envoy service: + +```shell +kubectl -n envoy-gateway-system port-forward service/${ENVOY_SERVICE} 6043:6443 & +``` + +Curl the example app through Envoy proxy: + +```shell +curl -v --resolve "passthrough.example.com:6043:127.0.0.1" https://passthrough.example.com:6043 \ +--cacert passthrough.example.com.crt +``` + +{{% /tab %}} +{{< /tabpane >}} + +## Clean-Up + +Follow the steps from the [Quickstart](../../quickstart) to uninstall Envoy Gateway and the example manifest. + +Delete the Secret: + +```shell +kubectl delete secret/server-certs +``` + +## Next Steps + +Checkout the [Developer Guide](../../../contributions/develop) to get involved in the project. diff --git a/site/content/en/v1.1/tasks/security/tls-termination.md b/site/content/en/v1.1/tasks/security/tls-termination.md new file mode 100644 index 00000000000..e4534dd57e1 --- /dev/null +++ b/site/content/en/v1.1/tasks/security/tls-termination.md @@ -0,0 +1,91 @@ +--- +title: "TLS Termination for TCP" +--- + +This task will walk through the steps required to configure TLS Terminate mode for TCP traffic via Envoy Gateway. +This task uses a self-signed CA, so it should be used for testing and demonstration purposes only. + +## Prerequisites + +- OpenSSL to generate TLS assets. + +## Installation + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway. + +## TLS Certificates +Generate the certificates and keys used by the Gateway to terminate client TLS connections. + +Create a root certificate and private key to sign certificates: + +```shell +openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -subj '/O=example Inc./CN=example.com' -keyout example.com.key -out example.com.crt +``` + +Create a certificate and a private key for `www.example.com`: + +```shell +openssl req -out www.example.com.csr -newkey rsa:2048 -nodes -keyout www.example.com.key -subj "/CN=www.example.com/O=example organization" +openssl x509 -req -days 365 -CA example.com.crt -CAkey example.com.key -set_serial 0 -in www.example.com.csr -out www.example.com.crt +``` + +Store the cert/key in a Secret: + +```shell +kubectl create secret tls example-cert --key=www.example.com.key --cert=www.example.com.crt +``` + +Install the TLS Termination for TCP example resources: + +```shell +kubectl apply -f https://raw.githubusercontent.com/envoyproxy/gateway/latest/examples/kubernetes/tls-termination.yaml +``` + +Verify the Gateway status: + +```shell +kubectl get gateway/eg -o yaml +``` + +## Testing + +{{< tabpane text=true >}} +{{% tab header="With External LoadBalancer Support" %}} + +Get the External IP of the Gateway: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +Query the example app through the Gateway: + +```shell +curl -v -HHost:www.example.com --resolve "www.example.com:443:${GATEWAY_HOST}" \ +--cacert example.com.crt https://www.example.com/get +``` + +{{% /tab %}} +{{% tab header="Without LoadBalancer Support" %}} + +Get the name of the Envoy service created the by the example Gateway: + +```shell +export ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}') +``` + +Port forward to the Envoy service: + +```shell +kubectl -n envoy-gateway-system port-forward service/${ENVOY_SERVICE} 8443:443 & +``` + +Query the example app through Envoy proxy: + +```shell +curl -v -HHost:www.example.com --resolve "www.example.com:8443:127.0.0.1" \ +--cacert example.com.crt https://www.example.com:8443/get +``` + +{{% /tab %}} +{{< /tabpane >}} diff --git a/site/content/en/v1.1/tasks/traffic/_index.md b/site/content/en/v1.1/tasks/traffic/_index.md new file mode 100644 index 00000000000..f884ccdfcb0 --- /dev/null +++ b/site/content/en/v1.1/tasks/traffic/_index.md @@ -0,0 +1,5 @@ +--- +title: "Traffic" +weight: 1 +description: This section includes Traffic Management tasks. +--- diff --git a/site/content/en/v1.1/tasks/traffic/backend.md b/site/content/en/v1.1/tasks/traffic/backend.md new file mode 100644 index 00000000000..02de7161fe2 --- /dev/null +++ b/site/content/en/v1.1/tasks/traffic/backend.md @@ -0,0 +1,211 @@ +--- +title: "Backend Routing" +--- + +Envoy Gateway supports routing to native K8s resources such as `Service` and `ServiceImport`. The `Backend` API is a custom Envoy Gateway [extension resource][] that can used in Gateway-API [BackendObjectReference][]. + +## Motivation +The Backend API was added to support several use cases: +- Allowing users to integrate Envoy with services (Ext Auth, Rate Limit, ALS, ...) using Unix Domain Sockets, which are currently not supported by K8s. +- Simplify [routing to cluster-external backends][], which currently requires users to maintain both K8s `Service` and `EndpointSlice` resources. + +## Warning + +Similar to the K8s EndpointSlice API, the Backend API can be misused to allow traffic to be sent to otherwise restricted destinations, as described in [CVE-2021-25740][]. +A Backend resource can be used to: +- Expose a Service or Pod that should not be accessible +- Reference a Service or Pod by a Route without appropriate Reference Grants +- Expose the Envoy Proxy localhost (including the Envoy admin endpoint) + +For these reasons, the Backend API is disabled by default in Envoy Gateway configuration. Envoy Gateway admins are advised to follow [upstream recommendations][] and restrict access to the Backend API using K8s RBAC. + +## Restrictions + +The Backend API is currently supported only in the following BackendReferences: +- [HTTPRoute]: IP and FQDN endpoints +- [Envoy Extension Policy] (ExtProc): IP, FQDN and unix domain socket endpoints + +The Backend API supports attachment the following policies: +- [Backend TLS Policy][] + +Certain restrictions apply on the value of hostnames and addresses. For example, the loopback IP address range and the localhost hostname are forbidden. + +Envoy Gateway does not manage the lifecycle of unix domain sockets referenced by the Backend resource. Envoy Gateway admins are responsible for creating and mounting the sockets into the envoy proxy pod. The latter can be achieved by patching the envoy deployment using the [EnvoyProxy][] resource. + +## Quickstart + +### Prerequisites + +* Follow the steps from the [Quickstart](../../quickstart) task to install Envoy Gateway and the example manifest. + Before proceeding, you should be able to query the example backend using HTTP. + +### Enable Backend + +* By default [Backend][] is disabled. Lets enable it in the [EnvoyGateway][] startup configuration + +* The default installation of Envoy Gateway installs a default [EnvoyGateway][] configuration and attaches it + using a `ConfigMap`. In the next step, we will update this resource to enable Backend. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +* After updating the `ConfigMap`, you will need to restart the `envoy-gateway` deployment so the configuration kicks in + +```shell +kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system +``` + +## Testing + +### Route to External Backend + +* In the following example, we will create a `Backend` resource that routes to httpbin.org:80 and a `HTTPRoute` that references this backend. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Get the Gateway address: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +Send a request and view the response: + +```shell +curl -I -HHost:www.example.com http://${GATEWAY_HOST}/headers +``` + +[Backend]: ../../../api/extension_types#backend +[routing to cluster-external backends]: ./../../tasks/traffic/routing-outside-kubernetes.md +[BackendObjectReference]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.BackendObjectReference +[extension resource]: https://gateway-api.sigs.k8s.io/guides/migrating-from-ingress/#approach-to-extensibility +[CVE-2021-25740]: https://nvd.nist.gov/vuln/detail/CVE-2021-25740 +[upstream recommendations]: https://github.com/kubernetes/kubernetes/issues/103675 +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute +[Envoy Extension Policy]: ../../../api/extension_types#envoyextensionpolicy +[Backend TLS Policy]: https://gateway-api.sigs.k8s.io/api-types/backendtlspolicy/ +[EnvoyProxy]: ../../../api/extension_types#envoyproxy +[EnvoyGateway]: ../../../api/extension_types#envoygateway diff --git a/site/content/en/v1.1/tasks/traffic/circuit-breaker.md b/site/content/en/v1.1/tasks/traffic/circuit-breaker.md new file mode 100644 index 00000000000..9480a86a2cd --- /dev/null +++ b/site/content/en/v1.1/tasks/traffic/circuit-breaker.md @@ -0,0 +1,150 @@ +--- +title: "Circuit Breakers" +--- + +[Envoy circuit breakers] can be used to fail quickly and apply back-pressure in response to upstream service degradation. + +Envoy Gateway supports the following circuit breaker thresholds: +- **Concurrent Connections**: limit the connections that Envoy can establish to the upstream service. When this threshold is met, new connections will not be established, and some requests will be queued until an existing connection becomes available. +- **Concurrent Requests**: limit on concurrent requests in-flight from Envoy to the upstream service. When this threshold is met, requests will be queued. +- **Pending Requests**: limit the pending request queue size. When this threshold is met, overflowing requests will be terminated with a `503` status code. + +Envoy's circuit breakers are distributed: counters are not synchronized across different Envoy processes. The default Envoy and Envoy Gateway circuit breaker threshold values (1024) may be too strict for high-throughput systems. + +Envoy Gateway introduces a new CRD called [BackendTrafficPolicy][] that allows the user to describe their desired circuit breaker thresholds. +This instantiated resource can be linked to a [Gateway][], [HTTPRoute][] or [GRPCRoute][] resource. + +**Note**: There are distinct circuit breaker counters for each `BackendReference` in an `xRoute` rule. Even if a `BackendTrafficPolicy` targets a `Gateway`, each `BackendReference` in that gateway still has separate circuit breaker counter. + +## Prerequisites + +### Install Envoy Gateway + +* Follow the installation step from the [Quickstart](../../quickstart) to install Envoy Gateway and sample resources. + +### Install the hey load testing tool +* The `hey` CLI will be used to generate load and measure response times. Follow the installation instruction from the [Hey project] docs. + +## Test and customize circuit breaker settings + +This example will simulate a degraded backend that responds within 10 seconds by adding the `?delay=10s` query parameter to API calls. The hey tool will be used to generate 100 concurrent requests. + +```shell +hey -n 100 -c 100 -host "www.example.com" http://${GATEWAY_HOST}/?delay=10s +``` + +```console +Summary: + Total: 10.3426 secs + Slowest: 10.3420 secs + Fastest: 10.0664 secs + Average: 10.2145 secs + Requests/sec: 9.6687 + + Total data: 36600 bytes + Size/request: 366 bytes + +Response time histogram: + 10.066 [1] |■■■■ + 10.094 [4] |■■■■■■■■■■■■■■■ + 10.122 [9] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 10.149 [10] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 10.177 [10] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 10.204 [11] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 10.232 [11] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 10.259 [11] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 10.287 [11] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 10.314 [11] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 10.342 [11] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ +``` + +The default circuit breaker threshold (1024) is not met. As a result, requests do not overflow: all requests are proxied upstream and both Envoy and clients wait for 10s. + +In order to fail fast, apply a `BackendTrafficPolicy` that limits concurrent requests to 10 and pending requests to 0. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Execute the load simulation again. + +```shell +hey -n 100 -c 100 -host "www.example.com" http://${GATEWAY_HOST}/?delay=10s +``` + +```console +Summary: + Total: 10.1230 secs + Slowest: 10.1224 secs + Fastest: 0.0529 secs + Average: 1.0677 secs + Requests/sec: 9.8785 + + Total data: 10940 bytes + Size/request: 109 bytes + +Response time histogram: + 0.053 [1] | + 1.060 [89] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 2.067 [0] | + 3.074 [0] | + 4.081 [0] | + 5.088 [0] | + 6.095 [0] | + 7.102 [0] | + 8.109 [0] | + 9.115 [0] | + 10.122 [10] |■■■■ +``` + +With the new circuit breaker settings, and due to the slowness of the backend, only the first 10 concurrent requests were proxied, while the other 90 overflowed. +* Overflowing Requests failed fast, reducing proxy resource consumption. +* Upstream traffic was limited, alleviating the pressure on the degraded service. + +[Envoy Circuit Breakers]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/circuit_breaking +[BackendTrafficPolicy]: ../../../api/extension_types#backendtrafficpolicy +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway/ +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute/ +[GRPCRoute]: https://gateway-api.sigs.k8s.io/api-types/grpcroute/ +[Hey project]: https://github.com/rakyll/hey diff --git a/site/content/en/v1.1/tasks/traffic/client-traffic-policy.md b/site/content/en/v1.1/tasks/traffic/client-traffic-policy.md new file mode 100644 index 00000000000..37d85e5f8e1 --- /dev/null +++ b/site/content/en/v1.1/tasks/traffic/client-traffic-policy.md @@ -0,0 +1,688 @@ +--- +title: "Client Traffic Policy" +--- + +This task explains the usage of the [ClientTrafficPolicy][] API. + + +## Introduction + +The [ClientTrafficPolicy][] API allows system administrators to configure +the behavior for how the Envoy Proxy server behaves with downstream clients. + +## Motivation + +This API was added as a new policy attachment resource that can be applied to Gateway resources and it is meant to hold settings for configuring behavior of the connection between the downstream client and Envoy Proxy listener. It is the counterpart to the [BackendTrafficPolicy][] API resource. + +## Quickstart + +### Prerequisites + +* Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +### Support TCP keepalive for downstream client + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify that ClientTrafficPolicy is Accepted: + +```shell +kubectl get clienttrafficpolicies.gateway.envoyproxy.io -n default +``` + +You should see the policy marked as accepted like this: + +```shell +NAME STATUS AGE +enable-tcp-keepalive-policy Accepted 5s +``` + +Curl the example app through Envoy proxy once again: + +```shell +curl --verbose --header "Host: www.example.com" http://$GATEWAY_HOST/get --next --header "Host: www.example.com" http://$GATEWAY_HOST/get +``` + +You should see the output like this: + +```shell +* Trying 172.18.255.202:80... +* Connected to 172.18.255.202 (172.18.255.202) port 80 (#0) +> GET /get HTTP/1.1 +> Host: www.example.com +> User-Agent: curl/8.1.2 +> Accept: */* +> +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< date: Fri, 01 Dec 2023 10:17:04 GMT +< content-length: 507 +< x-envoy-upstream-service-time: 0 +< server: envoy +< +{ + "path": "/get", + "host": "www.example.com", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/8.1.2" + ], + "X-Envoy-Expected-Rq-Timeout-Ms": [ + "15000" + ], + "X-Envoy-Internal": [ + "true" + ], + "X-Forwarded-For": [ + "172.18.0.2" + ], + "X-Forwarded-Proto": [ + "http" + ], + "X-Request-Id": [ + "4d0d33e8-d611-41f0-9da0-6458eec20fa5" + ] + }, + "namespace": "default", + "ingress": "", + "service": "", + "pod": "backend-58d58f745-2zwvn" +* Connection #0 to host 172.18.255.202 left intact +}* Found bundle for host: 0x7fb9f5204ea0 [serially] +* Can not multiplex, even if we wanted to +* Re-using existing connection #0 with host 172.18.255.202 +> GET /headers HTTP/1.1 +> Host: www.example.com +> User-Agent: curl/8.1.2 +> Accept: */* +> +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< date: Fri, 01 Dec 2023 10:17:04 GMT +< content-length: 511 +< x-envoy-upstream-service-time: 0 +< server: envoy +< +{ + "path": "/headers", + "host": "www.example.com", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/8.1.2" + ], + "X-Envoy-Expected-Rq-Timeout-Ms": [ + "15000" + ], + "X-Envoy-Internal": [ + "true" + ], + "X-Forwarded-For": [ + "172.18.0.2" + ], + "X-Forwarded-Proto": [ + "http" + ], + "X-Request-Id": [ + "9a8874c0-c117-481c-9b04-933571732ca5" + ] + }, + "namespace": "default", + "ingress": "", + "service": "", + "pod": "backend-58d58f745-2zwvn" +* Connection #0 to host 172.18.255.202 left intact +} +``` + +You can see keepalive connection marked by the output in: + +```shell +* Connection #0 to host 172.18.255.202 left intact +* Re-using existing connection #0 with host 172.18.255.202 +``` + +### Enable Proxy Protocol for downstream client + +This example configures Proxy Protocol for downstream clients. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify that ClientTrafficPolicy is Accepted: + +```shell +kubectl get clienttrafficpolicies.gateway.envoyproxy.io -n default +``` + +You should see the policy marked as accepted like this: + +```shell +NAME STATUS AGE +enable-proxy-protocol-policy Accepted 5s +``` + +Try the endpoint without using PROXY protocol with curl: + +```shell +curl -v --header "Host: www.example.com" http://$GATEWAY_HOST/get +``` + +```shell +* Trying 172.18.255.202:80... +* Connected to 172.18.255.202 (172.18.255.202) port 80 (#0) +> GET /get HTTP/1.1 +> Host: www.example.com +> User-Agent: curl/8.1.2 +> Accept: */* +> +* Recv failure: Connection reset by peer +* Closing connection 0 +curl: (56) Recv failure: Connection reset by peer +``` + +Curl the example app through Envoy proxy once again, now sending HAProxy PROXY protocol header at the beginning of the connection with --haproxy-protocol flag: + +```shell +curl --verbose --haproxy-protocol --header "Host: www.example.com" http://$GATEWAY_HOST/get +``` + +You should now expect 200 response status and also see that source IP was preserved in the X-Forwarded-For header. + +```shell +* Trying 172.18.255.202:80... +* Connected to 172.18.255.202 (172.18.255.202) port 80 (#0) +> GET /get HTTP/1.1 +> Host: www.example.com +> User-Agent: curl/8.1.2 +> Accept: */* +> +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< date: Mon, 04 Dec 2023 21:11:43 GMT +< content-length: 510 +< x-envoy-upstream-service-time: 0 +< server: envoy +< +{ + "path": "/get", + "host": "www.example.com", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/8.1.2" + ], + "X-Envoy-Expected-Rq-Timeout-Ms": [ + "15000" + ], + "X-Envoy-Internal": [ + "true" + ], + "X-Forwarded-For": [ + "192.168.255.6" + ], + "X-Forwarded-Proto": [ + "http" + ], + "X-Request-Id": [ + "290e4b61-44b7-4e5c-a39c-0ec76784e897" + ] + }, + "namespace": "default", + "ingress": "", + "service": "", + "pod": "backend-58d58f745-2zwvn" +* Connection #0 to host 172.18.255.202 left intact +} +``` + +### Configure Client IP Detection + +This example configures the number of additional ingress proxy hops from the right side of XFF HTTP headers to trust when determining the origin client's IP address and determines whether or not `x-forwarded-proto` headers will be trusted. Refer to https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers#x-forwarded-for for details. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify that ClientTrafficPolicy is Accepted: + +```shell +kubectl get clienttrafficpolicies.gateway.envoyproxy.io -n default +``` + +You should see the policy marked as accepted like this: + +```shell +NAME STATUS AGE +http-client-ip-detection Accepted 5s +``` + +Open port-forward to the admin interface port: + +```shell +kubectl port-forward deploy/${ENVOY_DEPLOYMENT} -n envoy-gateway-system 19000:19000 +``` + +Curl the admin interface port to fetch the configured value for `xff_num_trusted_hops`: + +```shell +curl -s 'http://localhost:19000/config_dump?resource=dynamic_listeners' \ + | jq -r '.configs[0].active_state.listener.default_filter_chain.filters[0].typed_config + | with_entries(select(.key | match("xff|remote_address|original_ip")))' +``` + +You should expect to see the following: + +```json +{ + "use_remote_address": true, + "xff_num_trusted_hops": 2 +} +``` + +Curl the example app through Envoy proxy: + +```shell +curl -v http://$GATEWAY_HOST/get \ + -H "Host: www.example.com" \ + -H "X-Forwarded-Proto: https" \ + -H "X-Forwarded-For: 1.1.1.1,2.2.2.2" +``` + +You should expect 200 response status, see that `X-Forwarded-Proto` was preserved and `X-Envoy-External-Address` was set to the leftmost address in the `X-Forwarded-For` header: + +```shell +* Trying [::1]:8888... +* Connected to localhost (::1) port 8888 +> GET /get HTTP/1.1 +> Host: www.example.com +> User-Agent: curl/8.4.0 +> Accept: */* +> X-Forwarded-Proto: https +> X-Forwarded-For: 1.1.1.1,2.2.2.2 +> +Handling connection for 8888 +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< date: Tue, 30 Jan 2024 15:19:22 GMT +< content-length: 535 +< x-envoy-upstream-service-time: 0 +< server: envoy +< +{ + "path": "/get", + "host": "www.example.com", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/8.4.0" + ], + "X-Envoy-Expected-Rq-Timeout-Ms": [ + "15000" + ], + "X-Envoy-External-Address": [ + "1.1.1.1" + ], + "X-Forwarded-For": [ + "1.1.1.1,2.2.2.2,10.244.0.9" + ], + "X-Forwarded-Proto": [ + "https" + ], + "X-Request-Id": [ + "53ccfad7-1899-40fa-9322-ddb833aa1ac3" + ] + }, + "namespace": "default", + "ingress": "", + "service": "", + "pod": "backend-58d58f745-8psnc" +* Connection #0 to host localhost left intact +} +``` + +### Enable HTTP Request Received Timeout + +This feature allows you to limit the time taken by the Envoy Proxy fleet to receive the entire request from the client, which is useful in preventing certain clients from consuming too much memory in Envoy +This example configures the HTTP request timeout for the client, please check out the details [here](https://www.envoyproxy.io/docs/envoy/latest/faq/configuration/timeouts#stream-timeouts). + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Curl the example app through Envoy proxy: + +```shell +curl -v http://$GATEWAY_HOST/get \ + -H "Host: www.example.com" \ + -H "Content-Length: 10000" +``` + +You should expect `428` response status after 2s: + +```shell +curl -v http://$GATEWAY_HOST/get \ + -H "Host: www.example.com" \ + -H "Content-Length: 10000" +* Trying 172.18.255.200:80... +* Connected to 172.18.255.200 (172.18.255.200) port 80 +> GET /get HTTP/1.1 +> Host: www.example.com +> User-Agent: curl/8.4.0 +> Accept: */* +> Content-Length: 10000 +> +< HTTP/1.1 408 Request Timeout +< content-length: 15 +< content-type: text/plain +< date: Tue, 27 Feb 2024 07:38:27 GMT +< connection: close +< +* Closing connection +request timeout +``` + +### Configure Client HTTP Idle Timeout + +The idle timeout is defined as the period in which there are no active requests. When the idle timeout is reached the connection will be closed. +For more details see [here](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-field-config-core-v3-httpprotocoloptions-idle-timeout:~:text=...%7D%0A%7D-,idle_timeout,-(Duration)%20The). + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Curl the example app through Envoy proxy: + +```shell +openssl s_client -crlf -connect $GATEWAY_HOST:443 +``` + +You should expect the connection to be closed after 5s. + +You can also check the number of connections closed due to idle timeout by using the following query: + +```shell +envoy_http_downstream_cx_idle_timeout{envoy_http_conn_manager_prefix=""} +``` + +The number of connections closed due to idle timeout should be increased by 1. + + +### Configure Downstream Per Connection Buffer Limit + +This feature allows you to set a soft limit on size of the listener’s new connection read and write buffers. +The size is configured using the `resource.Quantity` format, see examples [here](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#meaning-of-memory). + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +[ClientTrafficPolicy]: ../../../api/extension_types#clienttrafficpolicy +[BackendTrafficPolicy]: ../../../api/extension_types#backendtrafficpolicy diff --git a/site/content/en/v1.1/tasks/traffic/connection-limit.md b/site/content/en/v1.1/tasks/traffic/connection-limit.md new file mode 100644 index 00000000000..aac8437dc45 --- /dev/null +++ b/site/content/en/v1.1/tasks/traffic/connection-limit.md @@ -0,0 +1,137 @@ +--- +title: "Connection Limit" +--- + +The connection limit features allows users to limit the number of concurrently active TCP connections on a [Gateway][] or a [Listener][]. +When the [connection limit][] is reached, new connections are closed immediately by Envoy proxy. It's possible to configure a delay for connection rejection. + +Users may want to limit the number of connections for several reasons: +* Protect resources like CPU and Memory. +* Ensure that different listeners can receive a fair share of global resources. +* Protect from malicious activity like DoS attacks. + +Envoy Gateway introduces a new CRD called [Client Traffic Policy][] that allows the user to describe their desired connection limit settings. +This instantiated resource can be linked to a [Gateway][]. + +The Envoy [connection limit][] implementation is distributed: counters are not synchronized between different envoy proxies. + +When a [Client Traffic Policy][] is attached to a gateway, the connection limit will apply differently based on the +[Listener][] protocol in use: +- HTTP: all HTTP listeners in a [Gateway][] will share a common connection counter, and a limit defined by the policy. +- HTTPS/TLS: each HTTPS/TLS listener will have a dedicated connection counter, and a limit defined by the policy. + + +## Prerequisites + +### Install Envoy Gateway + +* Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the HTTPRoute example manifest. + Before proceeding, you should be able to query the example backend using HTTP. + +### Install the hey load testing tool +* The `hey` CLI will be used to generate load and measure response times. Follow the installation instruction from the [Hey project] docs. + +## Test and customize connection limit settings + +This example we use `hey` to open 10 connections and execute 1 RPS per connection for 10 seconds. + +```shell +hey -c 10 -q 1 -z 10s -host "www.example.com" http://${GATEWAY_HOST}/get +``` + +```console +Summary: + Total: 10.0058 secs + Slowest: 0.0275 secs + Fastest: 0.0029 secs + Average: 0.0111 secs + Requests/sec: 9.9942 + +[...] + +Status code distribution: + [200] 100 responses +``` + +There are no connection limits, and so all 100 requests succeed. + +Next, we apply a limit of 5 connections. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Execute the load simulation again. + +```shell +hey -c 10 -q 1 -z 10s -host "www.example.com" http://${GATEWAY_HOST}/get +``` + +```console +Summary: + Total: 11.0327 secs + Slowest: 0.0361 secs + Fastest: 0.0013 secs + Average: 0.0088 secs + Requests/sec: 9.0640 + +[...] + +Status code distribution: + [200] 50 responses + +Error distribution: + [50] Get "http://localhost:8888/get": EOF +``` + +With the new connection limit, only 5 of 10 connections are established, and so only 50 requests succeed. + + +[Client Traffic Policy]: ../../../api/extension_types#clienttrafficpolicy +[Hey project]: https://github.com/rakyll/hey +[connection limit]: https://www.envoyproxy.io/docs/envoy/latest/configuration/listeners/network_filters/connection_limit_filter +[listener]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Listener +[gateway]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.Gateway diff --git a/site/content/en/v1.1/tasks/traffic/fault-injection.md b/site/content/en/v1.1/tasks/traffic/fault-injection.md new file mode 100644 index 00000000000..d4f536dbb33 --- /dev/null +++ b/site/content/en/v1.1/tasks/traffic/fault-injection.md @@ -0,0 +1,381 @@ +--- +title: "Fault Injection" +--- + +[Envoy fault injection] can be used to inject delays and abort requests to mimic failure scenarios such as service failures and overloads. + +Envoy Gateway supports the following fault scenarios: +- **delay fault**: inject a custom fixed delay into the request with a certain probability to simulate delay failures. +- **abort fault**: inject a custom response code into the response with a certain probability to simulate abort failures. + +Envoy Gateway introduces a new CRD called [BackendTrafficPolicy][] that allows the user to describe their desired fault scenarios. +This instantiated resource can be linked to a [Gateway][], [HTTPRoute][] or [GRPCRoute][] resource. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +For GRPC - follow the steps from the [GRPC Routing](../grpc-routing) example. +Before proceeding, you should be able to query the example backend using HTTP or GRPC. + +### Install the hey load testing tool +* The `hey` CLI will be used to generate load and measure response times. Follow the installation instruction from the [Hey project] docs. + +## Configuration + +Allow requests with a valid faultInjection by creating an [BackendTrafficPolicy][BackendTrafficPolicy] and attaching it to the example HTTPRoute or GRPCRoute. + +### HTTPRoute + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + + +Two HTTPRoute resources were created, one for `/foo` and another for `/bar`. `fault-injection-abort` BackendTrafficPolicy has been created and targeted HTTPRoute foo to abort requests for `/foo`. `fault-injection-delay` BackendTrafficPolicy has been created and targeted HTTPRoute foo to delay `2s` requests for `/bar`. + +Verify the HTTPRoute configuration and status: + +```shell +kubectl get httproute/foo -o yaml +kubectl get httproute/bar -o yaml +``` + +Verify the BackendTrafficPolicy configuration: + +```shell +kubectl get backendtrafficpolicy/fault-injection-50-percent-abort -o yaml +kubectl get backendtrafficpolicy/fault-injection-delay -o yaml +``` + +### GRPCRoute + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +A BackendTrafficPolicy has been created and targeted GRPCRoute yages to abort requests for `yages` service.. + +Verify the GRPCRoute configuration and status: + +```shell +kubectl get grpcroute/yages -o yaml +``` + +Verify the SecurityPolicy configuration: + +```shell +kubectl get backendtrafficpolicy/fault-injection-abort -o yaml +``` + +## Testing + +Ensure the `GATEWAY_HOST` environment variable from the [Quickstart](../../quickstart) is set. If not, follow the +Quickstart instructions to set the variable. + +```shell +echo $GATEWAY_HOST +``` + +### HTTPRoute + +Verify that requests to `foo` route are aborted. + +```shell +hey -n 1000 -c 100 -host "www.example.com" http://${GATEWAY_HOST}/foo +``` + +```console +Status code distribution: + [200] 501 responses + [501] 499 responses +``` + +Verify that requests to `bar` route are delayed. + +```shell +hey -n 1000 -c 100 -host "www.example.com" http://${GATEWAY_HOST}/bar +``` + +```console +Summary: + Total: 20.1493 secs + Slowest: 2.1020 secs + Fastest: 1.9940 secs + Average: 2.0123 secs + Requests/sec: 49.6295 + + Total data: 557000 bytes + Size/request: 557 bytes + +Response time histogram: + 1.994 [1] | + 2.005 [475] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 2.016 [419] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 2.026 [5] | + 2.037 [0] | + 2.048 [0] | + 2.059 [30] |■■■ + 2.070 [0] | + 2.080 [0] | + 2.091 [11] |■ + 2.102 [59] |■■■■■ +``` + +### GRPCRoute + +Verify that requests to `yages`service are aborted. + +```shell +grpcurl -plaintext -authority=grpc-example.com ${GATEWAY_HOST}:80 yages.Echo/Ping +``` + +You should see the below response + +```shell +Error invoking method "yages.Echo/Ping": rpc error: code = Unavailable desc = failed to query for service descriptor "yages.Echo": fault filter abort +``` + +## Clean-Up + +Follow the steps from the [Quickstart](../../quickstart) to uninstall Envoy Gateway and the example manifest. + +Delete the BackendTrafficPolicy: + +```shell +kubectl delete BackendTrafficPolicy/fault-injection-abort +``` + +[Envoy fault injection]: https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/fault_filter.html +[BackendTrafficPolicy]: ../../../api/extension_types#backendtrafficpolicy +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway/ +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute/ +[GRPCRoute]: https://gateway-api.sigs.k8s.io/api-types/grpcroute/ +[Hey project]: https://github.com/rakyll/hey diff --git a/site/content/en/v1.1/tasks/traffic/gateway-address.md b/site/content/en/v1.1/tasks/traffic/gateway-address.md new file mode 100644 index 00000000000..bd87726c139 --- /dev/null +++ b/site/content/en/v1.1/tasks/traffic/gateway-address.md @@ -0,0 +1,68 @@ +--- +title: "Gateway Address" +--- + +The Gateway API provides an optional [Addresses][] field through which Envoy Gateway can set addresses for Envoy Proxy Service. +Depending on the Service Type, the addresses of gateway can be used as: + +- [External IPs](#external-ips) +- [Cluster IP](#cluster-ip) + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. + +## External IPs + +Using the addresses in `Gateway.Spec.Addresses` as the [External IPs][] of Envoy Proxy Service, +this will __require__ the address to be of type `IPAddress` and the [ServiceType][] to be of `LoadBalancer` or `NodePort`. + +The Envoy Gateway deploys Envoy Proxy Service as `LoadBalancer` by default, +so you can set the address of the Gateway directly (the address settings here are for reference only): + +```shell +kubectl patch gateway eg --type=json --patch ' +- op: add + path: /spec/addresses + value: + - type: IPAddress + value: 1.2.3.4 +' +``` + +Verify the Gateway status: + +```shell +kubectl get gateway +``` + +```console +NAME CLASS ADDRESS PROGRAMMED AGE +eg eg 1.2.3.4 True 14m +``` + +Verify the Envoy Proxy Service status: + +```shell +kubectl get service -n envoy-gateway-system +``` + +```console +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +envoy-default-eg-64656661 LoadBalancer 10.96.236.219 1.2.3.4 80:31017/TCP 15m +envoy-gateway ClusterIP 10.96.192.76 18000/TCP 15m +envoy-gateway-metrics-service ClusterIP 10.96.124.73 8443/TCP 15m +``` + +__Note:__ If the `Gateway.Spec.Addresses` is explicitly set, it will be the only addresses that populates the Gateway status. + +## Cluster IP + +Using the addresses in `Gateway.Spec.Addresses` as the [Cluster IP][] of Envoy Proxy Service, +this will __require__ the address to be of type `IPAddress` and the [ServiceType][] to be of `ClusterIP`. + + +[Addresses]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.GatewayAddress +[External IPs]: https://kubernetes.io/docs/concepts/services-networking/service/#external-ips +[Cluster IP]: https://kubernetes.io/docs/concepts/services-networking/service/#type-clusterip +[ServiceType]: ../../../api/extension_types#servicetype diff --git a/site/content/en/v1.1/tasks/traffic/gatewayapi-support.md b/site/content/en/v1.1/tasks/traffic/gatewayapi-support.md new file mode 100644 index 00000000000..779cce6fb12 --- /dev/null +++ b/site/content/en/v1.1/tasks/traffic/gatewayapi-support.md @@ -0,0 +1,120 @@ +--- +title: "Gateway API Support" +--- + +As mentioned in the [system design][] document, Envoy Gateway's managed data plane is configured dynamically through +Kubernetes resources, primarily [Gateway API][] objects. Envoy Gateway supports configuration using the following Gateway API resources. + +## GatewayClass + +A [GatewayClass][] represents a "class" of gateways, i.e. which Gateways should be managed by Envoy Gateway. +Envoy Gateway supports managing __a single__ GatewayClass resource that matches its configured `controllerName` and +follows Gateway API guidelines for [resolving conflicts][] when multiple GatewayClasses exist with a matching +`controllerName`. + +__Note:__ If specifying GatewayClass [parameters reference][], it must refer to an [EnvoyProxy][] resource. + +## Gateway + +When a [Gateway][] resource is created that references the managed GatewayClass, Envoy Gateway will create and manage a +new Envoy Proxy deployment. Gateway API resources that reference this Gateway will configure this managed Envoy Proxy +deployment. + +## HTTPRoute + +A [HTTPRoute][] configures routing of HTTP traffic through one or more Gateways. The following HTTPRoute filters are +supported by Envoy Gateway: + +- `requestHeaderModifier`: [RequestHeaderModifiers][http-filter] + can be used to modify or add request headers before the request is proxied to its destination. +- `responseHeaderModifier`: [ResponseHeaderModifiers][http-filter] + can be used to modify or add response headers before the response is sent back to the client. +- `requestMirror`: [RequestMirrors][http-filter] + configure destinations where the requests should also be mirrored to. Responses to mirrored requests will be ignored. +- `requestRedirect`: [RequestRedirects][http-filter] + configure policied for how requests that match the HTTPRoute should be modified and then redirected. +- `urlRewrite`: [UrlRewrites][http-filter] + allow for modification of the request's hostname and path before it is proxied to its destination. +- `extensionRef`: [ExtensionRefs][] are used by Envoy Gateway to implement extended filters. Currently, Envoy Gateway + supports rate limiting and request authentication filters. For more information about these filters, refer to the + [rate limiting][] and [request authentication][] documentation. + +__Notes:__ +- The only [BackendRef][] kind supported by Envoy Gateway is a [Service][]. Routing traffic to other destinations such + as arbitrary URLs is not possible. +- Only `requestHeaderModifier` and `responseHeaderModifier` filters are currently supported within [HTTPBackendRef][]. + +## TCPRoute + +A [TCPRoute][] configures routing of raw TCP traffic through one or more Gateways. Traffic can be forwarded to the +desired BackendRefs based on a TCP port number. + +__Note:__ A TCPRoute only supports proxying in non-transparent mode, i.e. the backend will see the source IP and port of +the Envoy Proxy instance instead of the client. + +## UDPRoute + +A [UDPRoute][] configures routing of raw UDP traffic through one or more Gateways. Traffic can be forwarded to the +desired BackendRefs based on a UDP port number. + +__Note:__ Similar to TCPRoutes, UDPRoutes only support proxying in non-transparent mode i.e. the backend will see the +source IP and port of the Envoy Proxy instance instead of the client. + +## GRPCRoute + +A [GRPCRoute][] configures routing of [gRPC][] requests through one or more Gateways. They offer request matching by +hostname, gRPC service, gRPC method, or HTTP/2 Header. Envoy Gateway supports the following filters on GRPCRoutes to +provide additional traffic processing: + +- `requestHeaderModifier`: [RequestHeaderModifiers][grpc-filter] + can be used to modify or add request headers before the request is proxied to its destination. +- `responseHeaderModifier`: [ResponseHeaderModifiers][grpc-filter] + can be used to modify or add response headers before the response is sent back to the client. +- `requestMirror`: [RequestMirrors][grpc-filter] + configure destinations where the requests should also be mirrored to. Responses to mirrored requests will be ignored. + +__Notes:__ +- The only [BackendRef][grpc-filter] kind supported by Envoy Gateway is a [Service][]. Routing traffic to other + destinations such as arbitrary URLs is not currently possible. +- Only `requestHeaderModifier` and `responseHeaderModifier` filters are currently supported within [GRPCBackendRef][]. + +## TLSRoute + +A [TLSRoute][] configures routing of TCP traffic through one or more Gateways. However, unlike TCPRoutes, TLSRoutes +can match against TLS-specific metadata. + +## ReferenceGrant + +A [ReferenceGrant][] is used to allow a resource to reference another resource in a different namespace. Normally an +HTTPRoute created in namespace `foo` is not allowed to reference a Service in namespace `bar`. A ReferenceGrant permits +these types of cross-namespace references. Envoy Gateway supports the following ReferenceGrant use-cases: + +- Allowing an HTTPRoute, GRPCRoute, TLSRoute, UDPRoute, or TCPRoute to reference a Service in a different namespace. +- Allowing an HTTPRoute's `requestMirror` filter to include a BackendRef that references a Service in a different + namespace. +- Allowing a Gateway's [SecretObjectReference][] to reference a secret in a different namespace. + +[system design]: ../../../contributions/design/system-design +[Gateway API]: https://gateway-api.sigs.k8s.io/ +[GatewayClass]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.GatewayClass +[parameters reference]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.ParametersReference +[Gateway]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Gateway +[HTTPRoute]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRoute +[Service]: https://kubernetes.io/docs/concepts/services-networking/service/ +[BackendRef]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.BackendRef +[HTTPBackendRef]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPBackendRef +[TCPRoute]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.TCPRoute +[UDPRoute]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.UDPRoute +[GRPCRoute]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.GRPCRoute +[GRPCBackendRef]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.GRPCBackendRef +[gRPC]: https://grpc.io/ +[TLSRoute]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.TLSRoute +[ReferenceGrant]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.ReferenceGrant +[SecretObjectReference]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.SecretObjectReference +[rate limiting]: ../../../contributions/design/rate-limit +[request authentication]: ../security/jwt-authentication +[EnvoyProxy]: ../../../api/extension_types#envoyproxy +[resolving conflicts]: https://gateway-api.sigs.k8s.io/concepts/guidelines/?h=conflict#conflicts +[ExtensionRefs]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRouteFilterType +[grpc-filter]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.GRPCRouteFilter +[http-filter]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRouteFilter diff --git a/site/content/en/v1.1/tasks/traffic/global-rate-limit.md b/site/content/en/v1.1/tasks/traffic/global-rate-limit.md new file mode 100644 index 00000000000..3f1bfa4f301 --- /dev/null +++ b/site/content/en/v1.1/tasks/traffic/global-rate-limit.md @@ -0,0 +1,1312 @@ +--- +title: "Global Rate Limit" +--- + +Rate limit is a feature that allows the user to limit the number of incoming requests to a predefined value based on attributes within the traffic flow. + +Here are some reasons why you may want to implement Rate limits + +* To prevent malicious activity such as DDoS attacks. +* To prevent applications and its resources (such as a database) from getting overloaded. +* To create API limits based on user entitlements. + +Envoy Gateway supports two types of rate limiting: [Global rate limiting][] and [Local rate limiting][]. + +[Global rate limiting][] applies a shared rate limit to the traffic flowing through all the instances of Envoy proxies where it is configured. +i.e. if the data plane has 2 replicas of Envoy running, and the rate limit is 10 requests/second, this limit is shared and will be hit +if 5 requests pass through the first replica and 5 requests pass through the second replica within the same second. + +Envoy Gateway introduces a new CRD called [BackendTrafficPolicy][] that allows the user to describe their rate limit intent. This instantiated resource +can be linked to a [Gateway][], [HTTPRoute][] or [GRPCRoute][] resource. + +**Note:** Limit is applied per route. Even if a [BackendTrafficPolicy][] targets a gateway, each route in that gateway +still has a separate rate limit bucket. For example, if a gateway has 2 routes, and the limit is 100r/s, then each route +has its own 100r/s rate limit bucket. + +## Prerequisites + +### Install Envoy Gateway + +* Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the HTTPRoute example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +### Install Redis + +* The global rate limit feature is based on [Envoy Ratelimit][] which requires a Redis instance as its caching layer. +Lets install a Redis deployment in the `redis-system` namespce. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +### Enable Global Rate limit in Envoy Gateway + +* The default installation of Envoy Gateway installs a default [EnvoyGateway][] configuration and attaches it +using a `ConfigMap`. In the next step, we will update this resource to enable rate limit in Envoy Gateway +as well as configure the URL for the Redis instance used for Global rate limiting. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +* After updating the `ConfigMap`, you will need to restart the `envoy-gateway` deployment so the configuration kicks in + +```shell +kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system +``` + +## Rate Limit Specific User + +Here is an example of a rate limit implemented by the application developer to limit a specific user by matching on a custom `x-user-id` header +with a value set to `one`. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +### HTTPRoute + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The HTTPRoute status should indicate that it has been accepted and is bound to the example Gateway. + +```shell +kubectl get httproute/http-ratelimit -o yaml +``` + +Get the Gateway's address: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +Let's query `ratelimit.example/get` 4 times. We should receive a `200` response from the example Gateway for the first 3 requests +and then receive a `429` status code for the 4th request since the limit is set at 3 requests/Hour for the request which contains the header `x-user-id` +and value `one`. + +```shell +for i in {1..4}; do curl -I --header "Host: ratelimit.example" --header "x-user-id: one" http://${GATEWAY_HOST}/get ; sleep 1; done +``` + +```console +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:31 GMT +content-length: 460 +x-envoy-upstream-service-time: 4 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:32 GMT +content-length: 460 +x-envoy-upstream-service-time: 2 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:33 GMT +content-length: 460 +x-envoy-upstream-service-time: 0 +server: envoy + +HTTP/1.1 429 Too Many Requests +x-envoy-ratelimited: true +date: Wed, 08 Feb 2023 02:33:34 GMT +server: envoy +transfer-encoding: chunked + +``` + +You should be able to send requests with the `x-user-id` header and a different value and receive successful responses from the server. + +```shell +for i in {1..4}; do curl -I --header "Host: ratelimit.example" --header "x-user-id: two" http://${GATEWAY_HOST}/get ; sleep 1; done +``` + +```console +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:34:36 GMT +content-length: 460 +x-envoy-upstream-service-time: 0 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:34:37 GMT +content-length: 460 +x-envoy-upstream-service-time: 0 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:34:38 GMT +content-length: 460 +x-envoy-upstream-service-time: 0 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:34:39 GMT +content-length: 460 +x-envoy-upstream-service-time: 0 +server: envoy + +``` + +## Rate Limit Distinct Users + +Here is an example of a rate limit implemented by the application developer to limit distinct users who can be differentiated based on the +value in the `x-user-id` header. Here, user `one` (recognised from the traffic flow using the header `x-user-id` and value `one`) will be rate limited at 3 requests/hour +and so will user `two` (recognised from the traffic flow using the header `x-user-id` and value `two`). + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +### HTTPRoute + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Lets run the same command again with the header `x-user-id` and value `one` set in the request. We should the first 3 requests succeeding and +the 4th request being rate limited. + +```shell +for i in {1..4}; do curl -I --header "Host: ratelimit.example" --header "x-user-id: one" http://${GATEWAY_HOST}/get ; sleep 1; done +``` + +```console +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:31 GMT +content-length: 460 +x-envoy-upstream-service-time: 4 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:32 GMT +content-length: 460 +x-envoy-upstream-service-time: 2 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:33 GMT +content-length: 460 +x-envoy-upstream-service-time: 0 +server: envoy + +HTTP/1.1 429 Too Many Requests +x-envoy-ratelimited: true +date: Wed, 08 Feb 2023 02:33:34 GMT +server: envoy +transfer-encoding: chunked + +``` + +You should see the same behavior when the value for header `x-user-id` is set to `two` and 4 requests are sent. + +```shell +for i in {1..4}; do curl -I --header "Host: ratelimit.example" --header "x-user-id: two" http://${GATEWAY_HOST}/get ; sleep 1; done +``` + +```console +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:31 GMT +content-length: 460 +x-envoy-upstream-service-time: 4 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:32 GMT +content-length: 460 +x-envoy-upstream-service-time: 2 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:33 GMT +content-length: 460 +x-envoy-upstream-service-time: 0 +server: envoy + +HTTP/1.1 429 Too Many Requests +x-envoy-ratelimited: true +date: Wed, 08 Feb 2023 02:33:34 GMT +server: envoy +transfer-encoding: chunked + +``` + +## Rate Limit All Requests + +This example shows you how to rate limit all requests matching the HTTPRoute rule at 3 requests/Hour by leaving the `clientSelectors` field unset. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +### HTTPRoute + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +```shell +for i in {1..4}; do curl -I --header "Host: ratelimit.example" http://${GATEWAY_HOST}/get ; sleep 1; done +``` + +```console +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:31 GMT +content-length: 460 +x-envoy-upstream-service-time: 4 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:32 GMT +content-length: 460 +x-envoy-upstream-service-time: 2 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:33 GMT +content-length: 460 +x-envoy-upstream-service-time: 0 +server: envoy + +HTTP/1.1 429 Too Many Requests +x-envoy-ratelimited: true +date: Wed, 08 Feb 2023 02:33:34 GMT +server: envoy +transfer-encoding: chunked + +``` + +## Rate Limit Client IP Addresses + +Here is an example of a rate limit implemented by the application developer to limit distinct users who can be differentiated based on their + IP address (also reflected in the `X-Forwarded-For` header). + +Note: EG supports two kinds of rate limit for the IP address: Exact and Distinct. +* Exact means that all IP addresses within the specified Source IP CIDR share the same rate limit bucket. +* Distinct means that each IP address within the specified Source IP CIDR has its own rate limit bucket. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +```shell +for i in {1..4}; do curl -I --header "Host: ratelimit.example" http://${GATEWAY_HOST}/get ; sleep 1; done +``` + +```console +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Tue, 28 Mar 2023 08:28:45 GMT +content-length: 512 +x-envoy-upstream-service-time: 0 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Tue, 28 Mar 2023 08:28:46 GMT +content-length: 512 +x-envoy-upstream-service-time: 0 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Tue, 28 Mar 2023 08:28:48 GMT +content-length: 512 +x-envoy-upstream-service-time: 0 +server: envoy + +HTTP/1.1 429 Too Many Requests +x-envoy-ratelimited: true +date: Tue, 28 Mar 2023 08:28:48 GMT +server: envoy +transfer-encoding: chunked + +``` + +## Rate Limit Jwt Claims + +Here is an example of a rate limit implemented by the application developer to limit distinct users who can be differentiated based on the value of the Jwt claims carried. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Get the JWT used for testing request authentication: + +```shell +TOKEN=$(curl https://raw.githubusercontent.com/envoyproxy/gateway/main/examples/kubernetes/jwt/test.jwt -s) && echo "$TOKEN" | cut -d '.' -f2 - | base64 --decode - +``` + +```shell +TOKEN1=$(curl https://raw.githubusercontent.com/envoyproxy/gateway/main/examples/kubernetes/jwt/with-different-claim.jwt -s) && echo "$TOKEN1" | cut -d '.' -f2 - | base64 --decode - +``` + +### Rate limit by carrying `TOKEN` + +```shell +for i in {1..4}; do curl -I --header "Host: ratelimit.example" --header "Authorization: Bearer $TOKEN" http://${GATEWAY_HOST}/foo ; sleep 1; done +``` + +```console +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Mon, 12 Jun 2023 12:00:25 GMT +content-length: 561 +x-envoy-upstream-service-time: 0 +server: envoy + + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Mon, 12 Jun 2023 12:00:26 GMT +content-length: 561 +x-envoy-upstream-service-time: 0 +server: envoy + + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Mon, 12 Jun 2023 12:00:27 GMT +content-length: 561 +x-envoy-upstream-service-time: 0 +server: envoy + + +HTTP/1.1 429 Too Many Requests +x-envoy-ratelimited: true +date: Mon, 12 Jun 2023 12:00:28 GMT +server: envoy +transfer-encoding: chunked + +``` + +### No Rate Limit by carrying `TOKEN1` + +```shell +for i in {1..4}; do curl -I --header "Host: ratelimit.example" --header "Authorization: Bearer $TOKEN1" http://${GATEWAY_HOST}/foo ; sleep 1; done +``` + +```console +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Mon, 12 Jun 2023 12:02:34 GMT +content-length: 556 +x-envoy-upstream-service-time: 0 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Mon, 12 Jun 2023 12:02:35 GMT +content-length: 556 +x-envoy-upstream-service-time: 0 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Mon, 12 Jun 2023 12:02:36 GMT +content-length: 556 +x-envoy-upstream-service-time: 1 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Mon, 12 Jun 2023 12:02:37 GMT +content-length: 556 +x-envoy-upstream-service-time: 0 +server: envoy + +``` + +### (Optional) Editing Kubernetes Resources settings for the Rate Limit Service + +* The default installation of Envoy Gateway installs a default [EnvoyGateway][] configuration and provides the initial rate +limit kubernetes resources settings. such as `replicas` is 1, requests resources cpu is `100m`, memory is `512Mi`. the others +like container `image`, `securityContext`, `env` and pod `annotations` and `securityContext` can be modified by modifying the `ConfigMap`. + +* `tls.certificateRef` set the client certificate for redis server TLS connections. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +* After updating the `ConfigMap`, you will need to restart the `envoy-gateway` deployment so the configuration kicks in + +```shell +kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system +``` + +[Global Rate Limiting]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_features/global_rate_limiting +[Local rate limiting]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_features/local_rate_limiting +[BackendTrafficPolicy]: ../../../api/extension_types#backendtrafficpolicy +[Envoy Ratelimit]: https://github.com/envoyproxy/ratelimit +[EnvoyGateway]: ../../api/extension_types#envoygateway +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway/ +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute/ +[GRPCRoute]: https://gateway-api.sigs.k8s.io/api-types/grpcroute/ diff --git a/site/content/en/v1.1/tasks/traffic/grpc-routing.md b/site/content/en/v1.1/tasks/traffic/grpc-routing.md new file mode 100644 index 00000000000..7c41b54c885 --- /dev/null +++ b/site/content/en/v1.1/tasks/traffic/grpc-routing.md @@ -0,0 +1,272 @@ +--- +title: "GRPC Routing" +--- + +The [GRPCRoute][] resource allows users to configure gRPC routing by matching HTTP/2 traffic and forwarding it to backend gRPC servers. +To learn more about gRPC routing, refer to the [Gateway API documentation][]. + +## Prerequisites + +Follow the steps from the [Quickstart](../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## Installation + +Install the gRPC routing example resources: + +```shell +kubectl apply -f https://raw.githubusercontent.com/envoyproxy/gateway/latest/examples/kubernetes/grpc-routing.yaml +``` + +The manifest installs a [GatewayClass][], [Gateway][], a Deployment, a Service, and a GRPCRoute resource. +The GatewayClass is a cluster-scoped resource that represents a class of Gateways that can be instantiated. + +__Note:__ Envoy Gateway is configured by default to manage a GatewayClass with +`controllerName: gateway.envoyproxy.io/gatewayclass-controller`. + +## Verification + +Check the status of the GatewayClass: + +```shell +kubectl get gc --selector=example=grpc-routing +``` + +The status should reflect "Accepted=True", indicating Envoy Gateway is managing the GatewayClass. + +A Gateway represents configuration of infrastructure. When a Gateway is created, [Envoy proxy][] infrastructure is +provisioned or configured by Envoy Gateway. The `gatewayClassName` defines the name of a GatewayClass used by this +Gateway. Check the status of the Gateway: + +```shell +kubectl get gateways --selector=example=grpc-routing +``` + +The status should reflect "Ready=True", indicating the Envoy proxy infrastructure has been provisioned. The status also +provides the address of the Gateway. This address is used later to test connectivity to proxied backend services. + +Check the status of the GRPCRoute: + +```shell +kubectl get grpcroutes --selector=example=grpc-routing -o yaml +``` + +The status for the GRPCRoute should surface "Accepted=True" and a `parentRef` that references the example Gateway. +The `example-route` matches any traffic for "grpc-example.com" and forwards it to the "yages" Service. + +## Testing the Configuration + +Before testing GRPC routing to the `yages` backend, get the Gateway's address. + +```shell +export GATEWAY_HOST=$(kubectl get gateway/example-gateway -o jsonpath='{.status.addresses[0].value}') +``` + +Test GRPC routing to the `yages` backend using the [grpcurl][] command. + +```shell +grpcurl -plaintext -authority=grpc-example.com ${GATEWAY_HOST}:80 yages.Echo/Ping +``` + +You should see the below response + +```shell +{ + "text": "pong" +} +``` + +Envoy Gateway also supports [gRPC-Web][] requests for this configuration. The below `curl` command can be used to send a grpc-Web request with over HTTP/2. You should receive the same response seen in the previous command. + +The data in the body `AAAAAAA=` is a base64 encoded representation of an empty message (data length 0) that the Ping RPC accepts. + +```shell +curl --http2-prior-knowledge -s ${GATEWAY_HOST}:80/yages.Echo/Ping -H 'Host: grpc-example.com' -H 'Content-Type: application/grpc-web-text' -H 'Accept: application/grpc-web-text' -XPOST -d'AAAAAAA=' | base64 -d +``` + +## GRPCRoute Match +The `matches` field can be used to restrict the route to a specific set of requests based on GRPC's service and/or method names. +It supports two match types: `Exact` and `RegularExpression`. + +### Exact + +`Exact` match is the default match type. + +The following example shows how to match a request based on the service and method names for `grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo`, +as well as a match for all services with a method name `Ping` which matches `yages.Echo/Ping` in our deployment. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the GRPCRoute status: + +```shell +kubectl get grpcroutes --selector=example=grpc-routing -o yaml +``` + +Test GRPC routing to the `yages` backend using the [grpcurl][] command. + +```shell +grpcurl -plaintext -authority=grpc-example.com ${GATEWAY_HOST}:80 yages.Echo/Ping +``` + +### RegularExpression + +The following example shows how to match a request based on the service and method names +with match type `RegularExpression`. It matches all the services and methods with pattern +`/.*.Echo/Pin.+`, which matches `yages.Echo/Ping` in our deployment. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the GRPCRoute status: + +```shell +kubectl get grpcroutes --selector=example=grpc-routing -o yaml +``` + +Test GRPC routing to the `yages` backend using the [grpcurl][] command. + +```shell +grpcurl -plaintext -authority=grpc-example.com ${GATEWAY_HOST}:80 yages.Echo/Ping +``` + +[GRPCRoute]: https://gateway-api.sigs.k8s.io/api-types/grpcroute/ +[Gateway API documentation]: https://gateway-api.sigs.k8s.io/ +[GatewayClass]: https://gateway-api.sigs.k8s.io/api-types/gatewayclass/ +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway/ +[Envoy proxy]: https://www.envoyproxy.io/ +[grpcurl]: https://github.com/fullstorydev/grpcurl +[gRPC-Web]: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md#protocol-differences-vs-grpc-over-http2 diff --git a/site/content/en/v1.1/tasks/traffic/http-redirect.md b/site/content/en/v1.1/tasks/traffic/http-redirect.md new file mode 100644 index 00000000000..b3177e89263 --- /dev/null +++ b/site/content/en/v1.1/tasks/traffic/http-redirect.md @@ -0,0 +1,399 @@ +--- +title: "HTTP Redirects" +--- + +The [HTTPRoute][] resource can issue redirects to clients or rewrite paths sent upstream using filters. Note that +HTTPRoute rules cannot use both filter types at once. Currently, Envoy Gateway only supports __core__ +[HTTPRoute filters][] which consist of `RequestRedirect` and `RequestHeaderModifier` at the time of this writing. To +learn more about HTTP routing, refer to the [Gateway API documentation][]. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTPS. + +## Redirects + +Redirects return HTTP 3XX responses to a client, instructing it to retrieve a different resource. A +[`RequestRedirect` filter][req_filter] instructs Gateways to emit a redirect response to requests that match the rule. +For example, to issue a permanent redirect (301) from HTTP to HTTPS, configure `requestRedirect.statusCode=301` and +`requestRedirect.scheme="https"`: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +__Note:__ `301` (default) and `302` are the only supported statusCodes. + +The HTTPRoute status should indicate that it has been accepted and is bound to the example Gateway. + +```shell +kubectl get httproute/http-to-https-filter-redirect -o yaml +``` + +Get the Gateway's address: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +Querying `redirect.example/get` should result in a `301` response from the example Gateway and redirecting to the +configured redirect hostname. + +```console +$ curl -L -vvv --header "Host: redirect.example" "http://${GATEWAY_HOST}/get" +... +< HTTP/1.1 301 Moved Permanently +< location: https://www.example.com/get +... +``` + +If you followed the steps in the [Secure Gateways](../security/secure-gateways) task, you should be able to curl the redirect +location. + +## HTTP --> HTTPS + +Listeners expose the TLS setting on a per domain or subdomain basis. TLS settings of a listener are applied to all domains that satisfy the hostname criteria. + +Create a root certificate and private key to sign certificates: + +```shell +openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -subj '/CN=example.com' -keyout CA.key -out CA.crt +openssl req -out example.com.csr -newkey rsa:2048 -nodes -keyout tls.key -subj "/CN=example.com" +``` + +Generate a self-signed wildcard certificate for `example.com` with `*.example.com` extension + +```shell +cat <}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Check for any TLS certificate issues on the gateway. + +```bash +kubectl -n default describe gateway eg +``` + +Create two HTTPRoutes and attach them to the HTTP and HTTPS listeners using the [sectionName][] field. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Curl the example app through http listener: + +```bash +curl --verbose --header "Host: www.example.com" http://$GATEWAY_HOST/get +``` + +Curl the example app through https listener: + +```bash +curl -v -H 'Host:www.example.com' --resolve "www.example.com:443:$GATEWAY_HOST" \ +--cacert CA.crt https://www.example.com:443/get +``` + + +## Path Redirects + +Path redirects use an HTTP Path Modifier to replace either entire paths or path prefixes. For example, the HTTPRoute +below will issue a 302 redirect to all `path.redirect.example` requests whose path begins with `/get` to `/status/200`. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The HTTPRoute status should indicate that it has been accepted and is bound to the example Gateway. + +```shell +kubectl get httproute/http-filter-path-redirect -o yaml +``` + +Querying `path.redirect.example` should result in a `302` response from the example Gateway and a redirect location +containing the configured redirect path. + +Query the `path.redirect.example` host: + +```shell +curl -vvv --header "Host: path.redirect.example" "http://${GATEWAY_HOST}/get" +``` + +You should receive a `302` with a redirect location of `http://path.redirect.example/status/200`. + +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute/ +[HTTPRoute filters]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.HTTPRouteFilter +[Gateway API documentation]: https://gateway-api.sigs.k8s.io/ +[req_filter]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.HTTPRequestRedirectFilter +[sectionName]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.CommonRouteSpec diff --git a/site/content/en/v1.1/tasks/traffic/http-request-headers.md b/site/content/en/v1.1/tasks/traffic/http-request-headers.md new file mode 100644 index 00000000000..7bc709c49c6 --- /dev/null +++ b/site/content/en/v1.1/tasks/traffic/http-request-headers.md @@ -0,0 +1,449 @@ +--- +title: "HTTP Request Headers" +--- + +The [HTTPRoute][] resource can modify the headers of a request before forwarding it to the upstream service. HTTPRoute +rules cannot use both filter types at once. Currently, Envoy Gateway only supports __core__ [HTTPRoute filters][] which +consist of `RequestRedirect` and `RequestHeaderModifier` at the time of this writing. To learn more about HTTP routing, +refer to the [Gateway API documentation][]. + +A [`RequestHeaderModifier` filter][req_filter] instructs Gateways to modify the headers in requests that match the rule +before forwarding the request upstream. Note that the `RequestHeaderModifier` filter will only modify headers before the +request is sent from Envoy to the upstream service and will not affect response headers returned to the downstream +client. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## Adding Request Headers + +The `RequestHeaderModifier` filter can add new headers to a request before it is sent to the upstream. If the request +does not have the header configured by the filter, then that header will be added to the request. If the request already +has the header configured by the filter, then the value of the header in the filter will be appended to the value of the +header in the request. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + + +The HTTPRoute status should indicate that it has been accepted and is bound to the example Gateway. + +```shell +kubectl get httproute/http-headers -o yaml +``` + +Get the Gateway's address: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +Querying `headers.example/get` should result in a `200` response from the example Gateway and the output from the +example app should indicate that the upstream example app received the header `add-header` with the value: +`something,foo` + +```console +$ curl -vvv --header "Host: headers.example" "http://${GATEWAY_HOST}/get" --header "add-header: something" +... +> GET /get HTTP/1.1 +> Host: headers.example +> User-Agent: curl/7.81.0 +> Accept: */* +> add-header: something +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< content-length: 474 +< x-envoy-upstream-service-time: 0 +< server: envoy +< +... + "headers": { + "Accept": [ + "*/*" + ], + "Add-Header": [ + "something", + "foo" + ], +... +``` + +## Setting Request Headers + +Setting headers is similar to adding headers. If the request does not have the header configured by the filter, then it +will be added, but unlike [adding request headers](#adding-request-headers) which will append the value of the header if +the request already contains it, setting a header will cause the value to be replaced by the value configured in the +filter. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Querying `headers.example/get` should result in a `200` response from the example Gateway and the output from the +example app should indicate that the upstream example app received the header `add-header` with the original value +`something` replaced by `foo`. + +```console +$ curl -vvv --header "Host: headers.example" "http://${GATEWAY_HOST}/get" --header "set-header: something" +... +> GET /get HTTP/1.1 +> Host: headers.example +> User-Agent: curl/7.81.0 +> Accept: */* +> add-header: something +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< content-length: 474 +< x-envoy-upstream-service-time: 0 +< server: envoy +< + "headers": { + "Accept": [ + "*/*" + ], + "Set-Header": [ + "foo" + ], +... +``` + +## Removing Request Headers + +Headers can be removed from a request by simply supplying a list of header names. + +Setting headers is similar to adding headers. If the request does not have the header configured by the filter, then it +will be added, but unlike [adding request headers](#adding-request-headers) which will append the value of the header if +the request already contains it, setting a header will cause the value to be replaced by the value configured in the +filter. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Querying `headers.example/get` should result in a `200` response from the example Gateway and the output from the +example app should indicate that the upstream example app received the header `add-header`, but the header +`remove-header` that was sent by curl was removed before the upstream received the request. + +```console +$ curl -vvv --header "Host: headers.example" "http://${GATEWAY_HOST}/get" --header "add-header: something" --header "remove-header: foo" +... +> GET /get HTTP/1.1 +> Host: headers.example +> User-Agent: curl/7.81.0 +> Accept: */* +> add-header: something +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< content-length: 474 +< x-envoy-upstream-service-time: 0 +< server: envoy +< + + "headers": { + "Accept": [ + "*/*" + ], + "Add-Header": [ + "something" + ], +... +``` + +## Combining Filters + +Headers can be added/set/removed in a single filter on the same HTTPRoute and they will all perform as expected + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute/ +[HTTPRoute filters]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRouteFilter +[Gateway API documentation]: https://gateway-api.sigs.k8s.io/ +[req_filter]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPHeaderFilter diff --git a/site/content/en/v1.1/tasks/traffic/http-request-mirroring.md b/site/content/en/v1.1/tasks/traffic/http-request-mirroring.md new file mode 100644 index 00000000000..f22ef51da36 --- /dev/null +++ b/site/content/en/v1.1/tasks/traffic/http-request-mirroring.md @@ -0,0 +1,447 @@ +--- +title: "HTTPRoute Request Mirroring" +--- + +The [HTTPRoute][] resource allows one or more [backendRefs][] to be provided. Requests will be routed to these upstreams. It is possible to divide the traffic between these backends using [Traffic Splitting][], but it is also possible to mirror requests to another Service instead. Request mirroring is accomplished using Gateway API's [HTTPRequestMirrorFilter][] on the `HTTPRoute`. + +When requests are made to a `HTTPRoute` that uses a `HTTPRequestMirrorFilter`, the response will never come from the `backendRef` defined in the filter. Responses from the mirror `backendRef` are always ignored. + +## Installation + +Follow the steps from the [Quickstart][] to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## Mirroring the Traffic + +Next, create a new `Deployment` and `Service` to mirror requests to. The following example will use +a second instance of the application deployed in the quickstart. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} +Then create an `HTTPRoute` that uses a `HTTPRequestMirrorFilter` to send requests to the original +service from the quickstart, and mirror request to the service that was just deployed. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The HTTPRoute status should indicate that it has been accepted and is bound to the example Gateway. + +```shell +kubectl get httproute/http-mirror -o yaml +``` + +Get the Gateway's address: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +Querying `backends.example/get` should result in a `200` response from the example Gateway and the output from the +example app should indicate which pod handled the request. There is only one pod in the deployment for the example app +from the quickstart, so it will be the same on all subsequent requests. + +```console +$ curl -v --header "Host: backends.example" "http://${GATEWAY_HOST}/get" +... +> GET /get HTTP/1.1 +> Host: backends.example +> User-Agent: curl/7.81.0 +> Accept: */* +> add-header: something +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< content-length: 474 +< x-envoy-upstream-service-time: 0 +< server: envoy +< +... + + "namespace": "default", + "ingress": "", + "service": "", + "pod": "backend-79665566f5-s589f" +... +``` + +Check the logs of the pods and you will see that the original deployment and the new deployment each got a request: + +```shell +$ kubectl logs deploy/backend && kubectl logs deploy/backend-2 +... +Starting server, listening on port 3000 (http) +Echoing back request made to /get to client (10.42.0.10:41566) +Starting server, listening on port 3000 (http) +Echoing back request made to /get to client (10.42.0.10:45096) +``` + +## Multiple BackendRefs + +When an `HTTPRoute` has multiple `backendRefs` and an `HTTPRequestMirrorFilter`, traffic splitting will still behave the same as it normally would for the main `backendRefs` while the `backendRef` of the `HTTPRequestMirrorFilter` will continue receiving mirrored copies of the incoming requests. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +## Multiple HTTPRequestMirrorFilters + +Multiple `HTTPRequestMirrorFilters` are not supported on the same `HTTPRoute` `rule`. When attempting to do so, the admission webhook will reject the configuration. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +```console +Error from server: error when creating "STDIN": admission webhook "validate.gateway.networking.k8s.io" denied the request: spec.rules[0].filters: Invalid value: "RequestMirror": cannot be used multiple times in the same rule +``` + +[Quickstart]: ../../quickstart/ +[Traffic Splitting]: ../http-traffic-splitting/ +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute/ +[backendRefs]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.BackendRef +[HTTPRequestMirrorFilter]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRequestMirrorFilter diff --git a/site/content/en/v1.1/tasks/traffic/http-response-headers.md b/site/content/en/v1.1/tasks/traffic/http-response-headers.md new file mode 100644 index 00000000000..60121674b00 --- /dev/null +++ b/site/content/en/v1.1/tasks/traffic/http-response-headers.md @@ -0,0 +1,446 @@ +--- +title: "HTTP Response Headers" +--- + +The [HTTPRoute][] resource can modify the headers of a response before responding it to the downstream service. To learn +more about HTTP routing, refer to the [Gateway API documentation][]. + +A [`ResponseHeaderModifier` filter][req_filter] instructs Gateways to modify the headers in responses that match the +rule before responding to the downstream. Note that the `ResponseHeaderModifier` filter will only modify headers before +the response is returned from Envoy to the downstream client and will not affect request headers forwarding to the +upstream service. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## Adding Response Headers + +The `ResponseHeaderModifier` filter can add new headers to a response before it is sent to the upstream. If the response +does not have the header configured by the filter, then that header will be added to the response. If the response +already has the header configured by the filter, then the value of the header in the filter will be appended to the +value of the header in the response. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The HTTPRoute status should indicate that it has been accepted and is bound to the example Gateway. + +```shell +kubectl get httproute/http-headers -o yaml +``` + +Get the Gateway's address: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +Querying `headers.example/get` should result in a `200` response from the example Gateway and the output from the +example app should indicate that the downstream client received the header `add-header` with the value: `foo` + +```console +$ curl -vvv --header "Host: headers.example" "http://${GATEWAY_HOST}/get" -H 'X-Echo-Set-Header: X-Foo: value1' +... +> GET /get HTTP/1.1 +> Host: headers.example +> User-Agent: curl/7.81.0 +> Accept: */* +> X-Echo-Set-Header: X-Foo: value1 +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< content-length: 474 +< x-envoy-upstream-service-time: 0 +< server: envoy +< x-foo: value1 +< add-header: foo +< +... + "headers": { + "Accept": [ + "*/*" + ], + "X-Echo-Set-Header": [ + "X-Foo: value1" + ] +... +``` + +## Setting Response Headers + +Setting headers is similar to adding headers. If the response does not have the header configured by the filter, then it +will be added, but unlike [adding response headers](#adding-response-headers) which will append the value of the header +if the response already contains it, setting a header will cause the value to be replaced by the value configured in the +filter. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Querying `headers.example/get` should result in a `200` response from the example Gateway and the output from the +example app should indicate that the downstream client received the header `set-header` with the original value `value1` +replaced by `foo`. + +```console +$ curl -vvv --header "Host: headers.example" "http://${GATEWAY_HOST}/get" -H 'X-Echo-Set-Header: set-header: value1' +... +> GET /get HTTP/1.1 +> Host: headers.example +> User-Agent: curl/7.81.0 +> Accept: */* +> X-Echo-Set-Header: set-header: value1 +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< content-length: 474 +< x-envoy-upstream-service-time: 0 +< server: envoy +< set-header: foo +< + "headers": { + "Accept": [ + "*/*" + ], + "X-Echo-Set-Header": [ + "set-header": value1" + ] +... +``` + +## Removing Response Headers + +Headers can be removed from a response by simply supplying a list of header names. + +Setting headers is similar to adding headers. If the response does not have the header configured by the filter, then it +will be added, but unlike [adding response headers](#adding-response-headers) which will append the value of the header +if the response already contains it, setting a header will cause the value to be replaced by the value configured in the +filter. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Querying `headers.example/get` should result in a `200` response from the example Gateway and the output from the +example app should indicate that the header `remove-header` that was sent by curl was removed before the upstream +received the response. + +```console +$ curl -vvv --header "Host: headers.example" "http://${GATEWAY_HOST}/get" -H 'X-Echo-Set-Header: remove-header: value1' +... +> GET /get HTTP/1.1 +> Host: headers.example +> User-Agent: curl/7.81.0 +> Accept: */* +> X-Echo-Set-Header: remove-header: value1 +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< content-length: 474 +< x-envoy-upstream-service-time: 0 +< server: envoy +< + + "headers": { + "Accept": [ + "*/*" + ], + "X-Echo-Set-Header": [ + "remove-header": value1" + ] +... +``` + +## Combining Filters + +Headers can be added/set/removed in a single filter on the same HTTPRoute and they will all perform as expected + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute/ +[Gateway API documentation]: https://gateway-api.sigs.k8s.io/ +[req_filter]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPHeaderFilter diff --git a/site/content/en/v1.1/tasks/traffic/http-routing.md b/site/content/en/v1.1/tasks/traffic/http-routing.md new file mode 100644 index 00000000000..aba57adc9b2 --- /dev/null +++ b/site/content/en/v1.1/tasks/traffic/http-routing.md @@ -0,0 +1,302 @@ +--- +title: "HTTP Routing" +--- + +The [HTTPRoute][] resource allows users to configure HTTP routing by matching HTTP traffic and forwarding it to +Kubernetes backends. Currently, the only supported backend supported by Envoy Gateway is a Service resource. This task +shows how to route traffic based on host, header, and path fields and forward the traffic to different Kubernetes +Services. To learn more about HTTP routing, refer to the [Gateway API documentation][]. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## Installation + +Install the HTTP routing example resources: + +```shell +kubectl apply -f https://raw.githubusercontent.com/envoyproxy/gateway/latest/examples/kubernetes/http-routing.yaml +``` + +The manifest installs a [GatewayClass][], [Gateway][], four Deployments, four Services, and three HTTPRoute resources. +The GatewayClass is a cluster-scoped resource that represents a class of Gateways that can be instantiated. + +__Note:__ Envoy Gateway is configured by default to manage a GatewayClass with +`controllerName: gateway.envoyproxy.io/gatewayclass-controller`. + +## Verification + +Check the status of the GatewayClass: + +```shell +kubectl get gc --selector=example=http-routing +``` + +The status should reflect "Accepted=True", indicating Envoy Gateway is managing the GatewayClass. + +A Gateway represents configuration of infrastructure. When a Gateway is created, [Envoy proxy][] infrastructure is +provisioned or configured by Envoy Gateway. The `gatewayClassName` defines the name of a GatewayClass used by this +Gateway. Check the status of the Gateway: + +```shell +kubectl get gateways --selector=example=http-routing +``` + +The status should reflect "Ready=True", indicating the Envoy proxy infrastructure has been provisioned. The status also +provides the address of the Gateway. This address is used later to test connectivity to proxied backend +services. + +The three HTTPRoute resources create routing rules on the Gateway. In order to receive traffic from a Gateway, +an HTTPRoute must be configured with `parentRefs` which reference the parent Gateway(s) that it should be attached to. +An HTTPRoute can match against a [single set of hostnames][spec]. These hostnames are matched before any other matching +within the HTTPRoute takes place. Since `example.com`, `foo.example.com`, and `bar.example.com` are separate hosts with +different routing requirements, each is deployed as its own HTTPRoute - `example-route, ``foo-route`, and `bar-route`. + +Check the status of the HTTPRoutes: + +```shell +kubectl get httproutes --selector=example=http-routing -o yaml +``` + +The status for each HTTPRoute should surface "Accepted=True" and a `parentRef` that references the example Gateway. +The `example-route` matches any traffic for "example.com" and forwards it to the "example-svc" Service. + +## Testing the Configuration + +Before testing HTTP routing to the `example-svc` backend, get the Gateway's address. + +```shell +export GATEWAY_HOST=$(kubectl get gateway/example-gateway -o jsonpath='{.status.addresses[0].value}') +``` + +Test HTTP routing to the `example-svc` backend. + +```shell +curl -vvv --header "Host: example.com" "http://${GATEWAY_HOST}/" +``` + +A `200` status code should be returned and the body should include `"pod": "example-backend-*"` indicating the traffic +was routed to the example backend service. If you change the hostname to a hostname not represented in any of the +HTTPRoutes, e.g. "www.example.com", the HTTP traffic will not be routed and a `404` should be returned. + +The `foo-route` matches any traffic for `foo.example.com` and applies its routing rules to forward the traffic to the +"foo-svc" Service. Since there is only one path prefix match for `/login`, only `foo.example.com/login/*` traffic will +be forwarded. Test HTTP routing to the `foo-svc` backend. + +```shell +curl -vvv --header "Host: foo.example.com" "http://${GATEWAY_HOST}/login" +``` + +A `200` status code should be returned and the body should include `"pod": "foo-backend-*"` indicating the traffic +was routed to the foo backend service. Traffic to any other paths that do not begin with `/login` will not be matched by +this HTTPRoute. Test this by removing `/login` from the request. + +```shell +curl -vvv --header "Host: foo.example.com" "http://${GATEWAY_HOST}/" +``` + +The HTTP traffic will not be routed and a `404` should be returned. + +Similarly, the `bar-route` HTTPRoute matches traffic for `bar.example.com`. All traffic for this hostname will be +evaluated against the routing rules. The most specific match will take precedence which means that any traffic with the +`env:canary` header will be forwarded to `bar-svc-canary` and if the header is missing or not `canary` then it'll be +forwarded to `bar-svc`. Test HTTP routing to the `bar-svc` backend. + +```shell +curl -vvv --header "Host: bar.example.com" "http://${GATEWAY_HOST}/" +``` + +A `200` status code should be returned and the body should include `"pod": "bar-backend-*"` indicating the traffic +was routed to the foo backend service. + +Test HTTP routing to the `bar-canary-svc` backend by adding the `env: canary` header to the request. + +```shell +curl -vvv --header "Host: bar.example.com" --header "env: canary" "http://${GATEWAY_HOST}/" +``` + +A `200` status code should be returned and the body should include `"pod": "bar-canary-backend-*"` indicating the +traffic was routed to the foo backend service. + +### JWT Claims Based Routing + +Users can route to a specific backend by matching on JWT claims. +This can be achieved, by defining a SecurityPolicy with a jwt configuration that does the following +* Converts jwt claims to headers, which can be used for header based routing +* Sets the recomputeRoute field to `true`. This is required so that the incoming request matches on a fallback/catch all route where the JWT can be authenticated, the claims from the JWT can be converted to headers, and then the route match can be recomputed to match based on the updated headers. + +For this feature to work please make sure +* you have a fallback route rule defined, the backend for this route rule can be invalid. +* The SecurityPolicy is applied to both the fallback route as well as the route with the claim header matches, to avoid spoofing. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Get the JWT used for testing request authentication: + +```shell +TOKEN=$(curl https://raw.githubusercontent.com/envoyproxy/gateway/main/examples/kubernetes/jwt/test.jwt -s) && echo "$TOKEN" | cut -d '.' -f2 - | base64 --decode - +``` + +Test routing to the `foo-svc` backend by specifying a JWT Token with a claim `name: John Doe`. + +```shell +curl -sS -H "Host: foo.example.com" -H "Authorization: Bearer $TOKEN" "http://${GATEWAY_HOST}/login" | jq .pod +"foo-backend-6df8cc6b9f-fmwcg" +``` + +Get another JWT used for testing request authentication: + +```shell +TOKEN=$(curl https://raw.githubusercontent.com/envoyproxy/gateway/main/examples/kubernetes/jwt/with-different-claim.jwt -s) && echo "$TOKEN" | cut -d '.' -f2 - | base64 --decode - +``` + +Test HTTP routing to the `bar-svc` backend by specifying a JWT Token with a claim `name: Tom`. + +```shell +curl -sS -H "Host: bar.example.com" -H "Authorization: Bearer $TOKEN" "http://${GATEWAY_HOST}/" | jq .pod +"bar-backend-6688b8944c-s8htr" +``` + +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute/ +[Gateway API documentation]: https://gateway-api.sigs.k8s.io/ +[GatewayClass]: https://gateway-api.sigs.k8s.io/api-types/gatewayclass/ +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway/ +[Envoy proxy]: https://www.envoyproxy.io/ +[spec]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRouteSpec diff --git a/site/content/en/v1.1/tasks/traffic/http-timeouts.md b/site/content/en/v1.1/tasks/traffic/http-timeouts.md new file mode 100644 index 00000000000..1eb9beabb24 --- /dev/null +++ b/site/content/en/v1.1/tasks/traffic/http-timeouts.md @@ -0,0 +1,199 @@ +--- +title: "HTTP Timeouts" +--- + +The default request timeout is set to 15 seconds in Envoy Proxy. +The [HTTPRouteTimeouts][] resource allows users to configure request timeouts for an [HTTPRouteRule][]. +This task shows you how to configure timeouts. + +The [HTTPRouteTimeouts][] supports two kinds of timeouts: +- **request**: Request specifies the maximum duration for a gateway to respond to an HTTP request. +- **backendRequest**: BackendRequest specifies a timeout for an individual request from the gateway to a backend. + +__Note:__ The Request duration must be >= BackendRequest duration + +## Installation + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## Verification + +backend has the ability to delay responses; we use it as the backend to control response time. + +### request timeout +We configure the backend to delay responses by 3 seconds, then we set the request timeout to 4 seconds. Envoy Gateway will successfully respond to the request. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +```shell +curl --header "Host: timeout.example.com" http://${GATEWAY_HOST}/?delay=3s -I +``` + +```console +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Mon, 04 Mar 2024 02:34:21 GMT +content-length: 480 +``` + +Then we set the request timeout to 2 seconds. In this case, Envoy Gateway will respond with a timeout. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +```shell +curl --header "Host: timeout.example.com" http://${GATEWAY_HOST}/?delay=3s -v +``` + +```console +* Trying 127.0.0.1:80... +* Connected to 127.0.0.1 (127.0.0.1) port 80 +> GET /?delay=3s HTTP/1.1 +> Host: timeout.example.com +> User-Agent: curl/8.6.0 +> Accept: */* +> + + +< HTTP/1.1 504 Gateway Timeout +< content-length: 24 +< content-type: text/plain +< date: Mon, 04 Mar 2024 02:35:03 GMT +< +* Connection #0 to host 127.0.0.1 left intact +upstream request timeout +``` + +[HTTPRouteTimeouts]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRouteTimeouts +[HTTPRouteRule]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRouteRule diff --git a/site/content/en/v1.1/tasks/traffic/http-traffic-splitting.md b/site/content/en/v1.1/tasks/traffic/http-traffic-splitting.md new file mode 100644 index 00000000000..06e4a236589 --- /dev/null +++ b/site/content/en/v1.1/tasks/traffic/http-traffic-splitting.md @@ -0,0 +1,527 @@ +--- +title: "HTTPRoute Traffic Splitting" +--- + +The [HTTPRoute][] resource allows one or more [backendRefs][] to be provided. Requests will be routed to these upstreams +if they match the rules of the HTTPRoute. If an invalid backendRef is configured, then HTTP responses will be returned +with status code `500` for all requests that would have been sent to that backend. + +## Installation + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## Single backendRef + +When a single backendRef is configured in a HTTPRoute, it will receive 100% of the traffic. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The HTTPRoute status should indicate that it has been accepted and is bound to the example Gateway. + +```shell +kubectl get httproute/http-headers -o yaml +``` + +Get the Gateway's address: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +Querying `backends.example/get` should result in a `200` response from the example Gateway and the output from the +example app should indicate which pod handled the request. There is only one pod in the deployment for the example app +from the quickstart, so it will be the same on all subsequent requests. + +```console +$ curl -vvv --header "Host: backends.example" "http://${GATEWAY_HOST}/get" +... +> GET /get HTTP/1.1 +> Host: backends.example +> User-Agent: curl/7.81.0 +> Accept: */* +> add-header: something +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< content-length: 474 +< x-envoy-upstream-service-time: 0 +< server: envoy +< +... + "namespace": "default", + "ingress": "", + "service": "", + "pod": "backend-79665566f5-s589f" +... +``` + +## Multiple backendRefs + +If multiple backendRefs are configured, then traffic will be split between the backendRefs equally unless a weight is +configured. + +First, create a second instance of the example app from the quickstart: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Then create an HTTPRoute that uses both the app from the quickstart and the second instance that was just created + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Querying `backends.example/get` should result in `200` responses from the example Gateway and the output from the +example app that indicates which pod handled the request should switch between the first pod and the second one from the +new deployment on subsequent requests. + +```console +$ curl -vvv --header "Host: backends.example" "http://${GATEWAY_HOST}/get" +... +> GET /get HTTP/1.1 +> Host: backends.example +> User-Agent: curl/7.81.0 +> Accept: */* +> add-header: something +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< content-length: 474 +< x-envoy-upstream-service-time: 0 +< server: envoy +< +... + "namespace": "default", + "ingress": "", + "service": "", + "pod": "backend-75bcd4c969-lsxpz" +... +``` + +## Weighted backendRefs + +If multiple backendRefs are configured and an un-even traffic split between the backends is desired, then the `weight` +field can be used to control the weight of requests to each backend. If weight is not configured for a backendRef it is +assumed to be `1`. + +The [weight field in a backendRef][backendRefs] controls the distribution of the traffic split. The proportion of +requests to a single backendRef is calculated by dividing its `weight` by the sum of all backendRef weights in the +HTTPRoute. The weight is not a percentage and the sum of all weights does not need to add up to 100. + +The HTTPRoute below will configure the gateway to send 80% of the traffic to the backend service, and 20% to the +backend-2 service. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +## Invalid backendRefs + +backendRefs can be considered invalid for the following reasons: + +- The `group` field is configured to something other than `""`. Currently, only the core API group (specified by + omitting the group field or setting it to an empty string) is supported +- The `kind` field is configured to anything other than `Service`. Envoy Gateway currently only supports Kubernetes + Service backendRefs +- The backendRef configures a service with a `namespace` not permitted by any existing ReferenceGrants +- The `port` field is not configured or is configured to a port that does not exist on the Service +- The named Service configured by the backendRef cannot be found + +Modifying the above example to make the backend-2 backendRef invalid by using a port that does not exist on the Service +will result in 80% of the traffic being sent to the backend service, and 20% of the traffic receiving an HTTP response +with status code `500`. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Querying `backends.example/get` should result in `200` responses 80% of the time, and `500` responses 20% of the time. + +```console +$ curl -vvv --header "Host: backends.example" "http://${GATEWAY_HOST}/get" +> GET /get HTTP/1.1 +> Host: backends.example +> User-Agent: curl/7.81.0 +> Accept: */* +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 500 Internal Server Error +< server: envoy +< content-length: 0 +< +``` + +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute/ +[backendRefs]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.BackendRef diff --git a/site/content/en/v1.1/tasks/traffic/http-urlrewrite.md b/site/content/en/v1.1/tasks/traffic/http-urlrewrite.md new file mode 100644 index 00000000000..0ebb7595c22 --- /dev/null +++ b/site/content/en/v1.1/tasks/traffic/http-urlrewrite.md @@ -0,0 +1,405 @@ +--- +title: "HTTP URL Rewrite" +--- + +[HTTPURLRewriteFilter][] defines a filter that modifies a request during forwarding. At most one of these filters may be +used on a Route rule. This MUST NOT be used on the same Route rule as a HTTPRequestRedirect filter. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## Rewrite URL Prefix Path + +You can configure to rewrite the prefix in the url like below. In this example, any curls to +`http://${GATEWAY_HOST}/get/xxx` will be rewritten to `http://${GATEWAY_HOST}/replace/xxx`. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The HTTPRoute status should indicate that it has been accepted and is bound to the example Gateway. + +```shell +kubectl get httproute/http-filter-url-rewrite -o yaml +``` + +Get the Gateway's address: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +Querying `http://${GATEWAY_HOST}/get/origin/path` should rewrite to +`http://${GATEWAY_HOST}/replace/origin/path`. + +```console +$ curl -L -vvv --header "Host: path.rewrite.example" "http://${GATEWAY_HOST}/get/origin/path" +... +> GET /get/origin/path HTTP/1.1 +> Host: path.rewrite.example +> User-Agent: curl/7.85.0 +> Accept: */* +> + +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< date: Wed, 21 Dec 2022 11:03:28 GMT +< content-length: 503 +< x-envoy-upstream-service-time: 0 +< server: envoy +< +{ + "path": "/replace/origin/path", + "host": "path.rewrite.example", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/7.85.0" + ], + "X-Envoy-Expected-Rq-Timeout-Ms": [ + "15000" + ], + "X-Envoy-Original-Path": [ + "/get/origin/path" + ], + "X-Forwarded-Proto": [ + "http" + ], + "X-Request-Id": [ + "fd84b842-9937-4fb5-83c7-61470d854b90" + ] + }, + "namespace": "default", + "ingress": "", + "service": "", + "pod": "backend-6fdd4b9bd8-8vlc5" +... +``` + +You can see that the `X-Envoy-Original-Path` is `/get/origin/path`, but the actual path is `/replace/origin/path`. + +## Rewrite URL Full Path + +You can configure to rewrite the fullpath in the url like below. In this example, any request sent to +`http://${GATEWAY_HOST}/get/origin/path/xxxx` will be rewritten to +`http://${GATEWAY_HOST}/force/replace/fullpath`. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The HTTPRoute status should indicate that it has been accepted and is bound to the example Gateway. + +```shell +kubectl get httproute/http-filter-url-rewrite -o yaml +``` + +Querying `http://${GATEWAY_HOST}/get/origin/path/extra` should rewrite the request to +`http://${GATEWAY_HOST}/force/replace/fullpath`. + +```console +$ curl -L -vvv --header "Host: path.rewrite.example" "http://${GATEWAY_HOST}/get/origin/path/extra" +... +> GET /get/origin/path/extra HTTP/1.1 +> Host: path.rewrite.example +> User-Agent: curl/7.85.0 +> Accept: */* +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< date: Wed, 21 Dec 2022 11:09:31 GMT +< content-length: 512 +< x-envoy-upstream-service-time: 0 +< server: envoy +< +{ + "path": "/force/replace/fullpath", + "host": "path.rewrite.example", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/7.85.0" + ], + "X-Envoy-Expected-Rq-Timeout-Ms": [ + "15000" + ], + "X-Envoy-Original-Path": [ + "/get/origin/path/extra" + ], + "X-Forwarded-Proto": [ + "http" + ], + "X-Request-Id": [ + "8ab774d6-9ffa-4faa-abbb-f45b0db00895" + ] + }, + "namespace": "default", + "ingress": "", + "service": "", + "pod": "backend-6fdd4b9bd8-8vlc5" +... +``` + +You can see that the `X-Envoy-Original-Path` is `/get/origin/path/extra`, but the actual path is +`/force/replace/fullpath`. + +## Rewrite Host Name + +You can configure to rewrite the hostname like below. In this example, any requests sent to +`http://${GATEWAY_HOST}/get` with `--header "Host: path.rewrite.example"` will rewrite host into `envoygateway.io`. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The HTTPRoute status should indicate that it has been accepted and is bound to the example Gateway. + +```shell +kubectl get httproute/http-filter-url-rewrite -o yaml +``` + +Querying `http://${GATEWAY_HOST}/get` with `--header "Host: path.rewrite.example"` will rewrite host into +`envoygateway.io`. + +```console +$ curl -L -vvv --header "Host: path.rewrite.example" "http://${GATEWAY_HOST}/get" +... +> GET /get HTTP/1.1 +> Host: path.rewrite.example +> User-Agent: curl/7.85.0 +> Accept: */* +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< date: Wed, 21 Dec 2022 11:15:15 GMT +< content-length: 481 +< x-envoy-upstream-service-time: 0 +< server: envoy +< +{ + "path": "/get", + "host": "envoygateway.io", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/7.85.0" + ], + "X-Envoy-Expected-Rq-Timeout-Ms": [ + "15000" + ], + "X-Forwarded-Host": [ + "path.rewrite.example" + ], + "X-Forwarded-Proto": [ + "http" + ], + "X-Request-Id": [ + "39aa447c-97b9-45a3-a675-9fb266ab1af0" + ] + }, + "namespace": "default", + "ingress": "", + "service": "", + "pod": "backend-6fdd4b9bd8-8vlc5" +... +``` + +You can see that the `X-Forwarded-Host` is `path.rewrite.example`, but the actual host is `envoygateway.io`. + +[HTTPURLRewriteFilter]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPURLRewriteFilter diff --git a/site/content/en/v1.1/tasks/traffic/http3.md b/site/content/en/v1.1/tasks/traffic/http3.md new file mode 100644 index 00000000000..af95ab8ba57 --- /dev/null +++ b/site/content/en/v1.1/tasks/traffic/http3.md @@ -0,0 +1,136 @@ +--- +title: "HTTP3" +--- + +This task will help you get started using HTTP3 using EG. +This task uses a self-signed CA, so it should be used for testing and demonstration purposes only. + +## Prerequisites + +- OpenSSL to generate TLS assets. + +## Installation + +Follow the steps from the [Quickstart](../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## TLS Certificates + +Generate the certificates and keys used by the Gateway to terminate client TLS connections. + +Create a root certificate and private key to sign certificates: + +```shell +openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -subj '/O=example Inc./CN=example.com' -keyout example.com.key -out example.com.crt +``` + +Create a certificate and a private key for `www.example.com`: + +```shell +openssl req -out www.example.com.csr -newkey rsa:2048 -nodes -keyout www.example.com.key -subj "/CN=www.example.com/O=example organization" +openssl x509 -req -days 365 -CA example.com.crt -CAkey example.com.key -set_serial 0 -in www.example.com.csr -out www.example.com.crt +``` + +Store the cert/key in a Secret: + +```shell +kubectl create secret tls example-cert --key=www.example.com.key --cert=www.example.com.crt +``` + +Update the Gateway from the Quickstart to include an HTTPS listener that listens on port `443` and references the +`example-cert` Secret: + +```shell +kubectl patch gateway eg --type=json --patch ' + - op: add + path: /spec/listeners/- + value: + name: https + protocol: HTTPS + port: 443 + tls: + mode: Terminate + certificateRefs: + - kind: Secret + group: "" + name: example-cert + ' +``` + +Apply the following ClientTrafficPolicy to enable HTTP3 + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the Gateway status: + +```shell +kubectl get gateway/eg -o yaml +``` + +## Testing + +{{< tabpane text=true >}} +{{% tab header="With External LoadBalancer Support" %}} + +Get the External IP of the Gateway: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +Query the example app through the Gateway: + +The below example uses a custom docker image with custom `curl` binary with built-in http3. + +```shell +docker run --net=host --rm ghcr.io/macbre/curl-http3 curl -kv --http3 -HHost:www.example.com --resolve "www.example.com:443:${GATEWAY_HOST}" https://www.example.com/get +``` + +{{% /tab %}} +{{% tab header="Without LoadBalancer Support" %}} + +It is not possible at the moment to port-forward UDP protocol in kubernetes service +check out https://github.com/kubernetes/kubernetes/issues/47862. +Hence we need external loadbalancer to test this feature out. + +{{% /tab %}} +{{< /tabpane >}} diff --git a/site/content/en/v1.1/tasks/traffic/load-balancing.md b/site/content/en/v1.1/tasks/traffic/load-balancing.md new file mode 100644 index 00000000000..90a816e7bc3 --- /dev/null +++ b/site/content/en/v1.1/tasks/traffic/load-balancing.md @@ -0,0 +1,922 @@ +--- +title: "Load Balancing" +--- + +[Envoy load balancing][] is a way of distributing traffic between multiple hosts within a single upstream cluster +in order to effectively make use of available resources. + +Envoy Gateway supports the following load balancing policies: + +- **Round Robin**: a simple policy in which each available upstream host is selected in round robin order. +- **Random**: load balancer selects a random available host. +- **Least Request**: load balancer uses different algorithms depending on whether hosts have the same or different weights. +- **Consistent Hash**: load balancer implements consistent hashing to upstream hosts. + +Envoy Gateway introduces a new CRD called [BackendTrafficPolicy][] that allows the user to describe their desired load balancing polices. +This instantiated resource can be linked to a [Gateway][], [HTTPRoute][] or [GRPCRoute][] resource. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +For better testing the load balancer, you can add more hosts in upstream cluster by increasing the replicas of one deployment: + +```shell +kubectl patch deployment backend -n default -p '{"spec": {"replicas": 4}}' +``` + +### Install the hey load testing tool + +Install the `Hey` CLI tool, this tool will be used to generate load and measure response times. + +Follow the installation instruction from the [Hey project] docs. + +## Round Robin + +This example will create a Load Balancer with Round Robin policy via [BackendTrafficPolicy][]. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The `hey` tool will be used to generate 100 concurrent requests. + +```shell +hey -n 100 -c 100 -host "www.example.com" http://${GATEWAY_HOST}/round +``` + +```console +Summary: + Total: 0.0487 secs + Slowest: 0.0440 secs + Fastest: 0.0181 secs + Average: 0.0307 secs + Requests/sec: 2053.1676 + + Total data: 50500 bytes + Size/request: 505 bytes + +Response time histogram: + 0.018 [1] |■■ + 0.021 [2] |■■■■ + 0.023 [10] |■■■■■■■■■■■■■■■■■■■■■■ + 0.026 [16] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.028 [7] |■■■■■■■■■■■■■■■■ + 0.031 [10] |■■■■■■■■■■■■■■■■■■■■■■ + 0.034 [17] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.036 [18] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.039 [11] |■■■■■■■■■■■■■■■■■■■■■■■■ + 0.041 [6] |■■■■■■■■■■■■■ + 0.044 [2] |■■■■ +``` + +As a result, you can see all available upstream hosts receive traffics evenly. + +```shell +kubectl get pods -l app=backend --no-headers -o custom-columns=":metadata.name" | while read -r pod; do echo "$pod: received $(($(kubectl logs $pod | wc -l) - 2)) requests"; done +``` + +```console +backend-69fcff487f-2gfp7: received 26 requests +backend-69fcff487f-69g8c: received 25 requests +backend-69fcff487f-bqwpr: received 24 requests +backend-69fcff487f-kbn8l: received 25 requests +``` + +You should note that this results may vary, the output here is for reference purpose only. + +## Random + +This example will create a Load Balancer with Random policy via [BackendTrafficPolicy][]. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The `hey` tool will be used to generate 1000 concurrent requests. + +```shell +hey -n 1000 -c 100 -host "www.example.com" http://${GATEWAY_HOST}/random +``` + +```console +Summary: + Total: 0.2624 secs + Slowest: 0.0851 secs + Fastest: 0.0007 secs + Average: 0.0179 secs + Requests/sec: 3811.3020 + + Total data: 506000 bytes + Size/request: 506 bytes + +Response time histogram: + 0.001 [1] | + 0.009 [421] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.018 [219] |■■■■■■■■■■■■■■■■■■■■■ + 0.026 [118] |■■■■■■■■■■■ + 0.034 [64] |■■■■■■ + 0.043 [73] |■■■■■■■ + 0.051 [41] |■■■■ + 0.060 [22] |■■ + 0.068 [19] |■■ + 0.077 [13] |■ + 0.085 [9] |■ +``` + +As a result, you can see all available upstream hosts receive traffics randomly. + +```shell +kubectl get pods -l app=backend --no-headers -o custom-columns=":metadata.name" | while read -r pod; do echo "$pod: received $(($(kubectl logs $pod | wc -l) - 2)) requests"; done +``` + +```console +backend-69fcff487f-bf6lm: received 246 requests +backend-69fcff487f-gwmqk: received 256 requests +backend-69fcff487f-mzngr: received 230 requests +backend-69fcff487f-xghqq: received 268 requests +``` + +You should note that this results may vary, the output here is for reference purpose only. + +## Least Request + +This example will create a Load Balancer with Least Request policy via [BackendTrafficPolicy][]. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The `hey` tool will be used to generate 100 concurrent requests. + +```shell +hey -n 100 -c 100 -host "www.example.com" http://${GATEWAY_HOST}/least +``` + +```console +Summary: + Total: 0.0489 secs + Slowest: 0.0479 secs + Fastest: 0.0054 secs + Average: 0.0297 secs + Requests/sec: 2045.9317 + + Total data: 50500 bytes + Size/request: 505 bytes + +Response time histogram: + 0.005 [1] |■■ + 0.010 [1] |■■ + 0.014 [8] |■■■■■■■■■■■■■■■ + 0.018 [6] |■■■■■■■■■■■ + 0.022 [11] |■■■■■■■■■■■■■■■■■■■■ + 0.027 [7] |■■■■■■■■■■■■■ + 0.031 [15] |■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.035 [13] |■■■■■■■■■■■■■■■■■■■■■■■■ + 0.039 [22] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.044 [12] |■■■■■■■■■■■■■■■■■■■■■■ + 0.048 [4] |■■■■■■■ +``` + +As a result, you can see all available upstream hosts receive traffics randomly, +and host `backend-69fcff487f-6l2pw` receives fewer requests than others. + +```shell +kubectl get pods -l app=backend --no-headers -o custom-columns=":metadata.name" | while read -r pod; do echo "$pod: received $(($(kubectl logs $pod | wc -l) - 2)) requests"; done +``` + +```console +backend-69fcff487f-59hvs: received 24 requests +backend-69fcff487f-6l2pw: received 19 requests +backend-69fcff487f-ktsx4: received 30 requests +backend-69fcff487f-nqxc7: received 27 requests +``` + +If you send one more requests to the `${GATEWAY_HOST}/least`, you can tell that host `backend-69fcff487f-6l2pw` is very likely +to get the attention of load balancer and receive this request. + +```console +backend-69fcff487f-59hvs: received 24 requests +backend-69fcff487f-6l2pw: received 20 requests +backend-69fcff487f-ktsx4: received 30 requests +backend-69fcff487f-nqxc7: received 27 requests +``` + +You should note that this results may vary, the output here is for reference purpose only. + +## Consistent Hash + +This example will create a Load Balancer with Consistent Hash policy via [BackendTrafficPolicy][]. + +The underlying consistent hash algorithm that Envoy Gateway utilise is [Maglev][], and it can derive hash from following aspects: + +- **SourceIP** +- **Header** +- **Cookie** + +They are also the supported value as consistent hash type. + +### Source IP + +This example will create a Load Balancer with Source IP based Consistent Hash policy. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The `hey` tool will be used to generate 100 concurrent requests. + +```shell +hey -n 100 -c 100 -host "www.example.com" http://${GATEWAY_HOST}/source +``` + +```console +Summary: + Total: 0.0539 secs + Slowest: 0.0500 secs + Fastest: 0.0198 secs + Average: 0.0340 secs + Requests/sec: 1856.5666 + + Total data: 50600 bytes + Size/request: 506 bytes + +Response time histogram: + 0.020 [1] |■■ + 0.023 [5] |■■■■■■■■■■■ + 0.026 [12] |■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.029 [16] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.032 [11] |■■■■■■■■■■■■■■■■■■■■■■■■ + 0.035 [7] |■■■■■■■■■■■■■■■■ + 0.038 [8] |■■■■■■■■■■■■■■■■■■ + 0.041 [18] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.044 [15] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.047 [4] |■■■■■■■■■ + 0.050 [3] |■■■■■■■ +``` + +As a result, you can see all traffics are routed to only one upstream host, since the client that send requests +has the same source IP. + +```shell +kubectl get pods -l app=backend --no-headers -o custom-columns=":metadata.name" | while read -r pod; do echo "$pod: received $(($(kubectl logs $pod | wc -l) - 2)) requests"; done +``` + +```console +backend-69fcff487f-grzkj: received 0 requests +backend-69fcff487f-n4d8w: received 100 requests +backend-69fcff487f-tb7zx: received 0 requests +backend-69fcff487f-wbzpg: received 0 requests +``` + +You can try different client to send out these requests, the upstream host that receives traffics may vary. + +### Header + +This example will create a Load Balancer with Header based Consistent Hash policy. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The `hey` tool will be used to generate 100 concurrent requests. + +```shell +hey -n 100 -c 100 -host "www.example.com" -H "FooBar: 1.2.3.4" http://${GATEWAY_HOST}/header +``` + +```console +Summary: + Total: 0.0579 secs + Slowest: 0.0510 secs + Fastest: 0.0323 secs + Average: 0.0431 secs + Requests/sec: 1728.6064 + + Total data: 53800 bytes + Size/request: 538 bytes + +Response time histogram: + 0.032 [1] |■■ + 0.034 [3] |■■■■■■ + 0.036 [1] |■■ + 0.038 [1] |■■ + 0.040 [7] |■■■■■■■■■■■■■■ + 0.042 [20] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.044 [20] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.045 [20] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.047 [16] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.049 [9] |■■■■■■■■■■■■■■■■■■ + 0.051 [2] |■■■■ +``` + +As a result, you can see all traffics are routed to only one upstream host, since the header of all requests are the same. + +```shell +kubectl get pods -l app=backend --no-headers -o custom-columns=":metadata.name" | while read -r pod; do echo "$pod: received $(($(kubectl logs $pod | wc -l) - 2)) requests"; done +``` + +```console +backend-69fcff487f-dvt9r: received 0 requests +backend-69fcff487f-f8qdl: received 100 requests +backend-69fcff487f-gnpm4: received 0 requests +backend-69fcff487f-t2pgm: received 0 requests +``` + +You can try to add different header to these requests, and the upstream host that receives traffics may vary. +The following output happens when you use `hey` to send another 100 requests with header `FooBar: 5.6.7.8`. + +```console +backend-69fcff487f-dvt9r: received 0 requests +backend-69fcff487f-f8qdl: received 100 requests +backend-69fcff487f-gnpm4: received 100 requests +backend-69fcff487f-t2pgm: received 0 requests +``` + +### Cookie + +This example will create a Load Balancer with Cookie based Consistent Hash policy. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +By sending 10 request with `curl` to the `${GATEWAY_HOST}/cookie`, you can see that all requests got routed to only +one upstream host, since they have same cookie setting. + +```shell +for i in {1..10}; do curl -I --header "Host: www.example.com" --cookie "FooBar=1.2.3.4" http://${GATEWAY_HOST}/cookie ; sleep 1; done +``` + +```shell +kubectl get pods -l app=backend --no-headers -o custom-columns=":metadata.name" | while read -r pod; do echo "$pod: received $(($(kubectl logs $pod | wc -l) - 2)) requests"; done +``` + +```console +backend-69fcff487f-5dxz9: received 0 requests +backend-69fcff487f-gpvl2: received 0 requests +backend-69fcff487f-pglgv: received 10 requests +backend-69fcff487f-qxr74: received 0 requests +``` + +You can try to set different cookie to these requests, the upstream host that receives traffics may vary. +The following output happens when you use `curl` to send another 10 requests with cookie `FooBar: 5.6.7.8`. + +```console +backend-69fcff487f-dvt9r: received 0 requests +backend-69fcff487f-f8qdl: received 0 requests +backend-69fcff487f-gnpm4: received 10 requests +backend-69fcff487f-t2pgm: received 10 requests +``` + +If the cookie has not been set in one request, Envoy Gateway will auto-generate a cookie for this request +according to the `ttl` and `attributes` field. + +In this example, the following cookie will be generated (see `set-cookie` header in response) if sending a request without cookie: + +```shell +curl -v --header "Host: www.example.com" http://${GATEWAY_HOST}/cookie +``` + +```console +> GET /cookie HTTP/1.1 +> Host: www.example.com +> User-Agent: curl/7.74.0 +> Accept: */* +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< date: Fri, 19 Jul 2024 16:49:57 GMT +< content-length: 458 +< set-cookie: FooBar="88358b9442700c56"; Max-Age=60; SameSite=Strict; HttpOnly +< +{ + "path": "/cookie", + "host": "www.example.com", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/7.74.0" + ], + "X-Envoy-Internal": [ + "true" + ], + "X-Forwarded-For": [ + "10.244.0.1" + ], + "X-Forwarded-Proto": [ + "http" + ], + "X-Request-Id": [ + "1adeaaf7-d45c-48c8-9a4d-eadbccb2fd50" + ] + }, + "namespace": "default", + "ingress": "", + "service": "", + "pod": "backend-69fcff487f-5dxz9" +``` + + +[Envoy load balancing]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/load_balancing/overview +[BackendTrafficPolicy]: ../../../api/extension_types#backendtrafficpolicy +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway/ +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute/ +[GRPCRoute]: https://gateway-api.sigs.k8s.io/api-types/grpcroute/ +[Hey project]: https://github.com/rakyll/hey +[Maglev]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/load_balancing/load_balancers#maglev diff --git a/site/content/en/v1.1/tasks/traffic/local-rate-limit.md b/site/content/en/v1.1/tasks/traffic/local-rate-limit.md new file mode 100644 index 00000000000..37fb5590a44 --- /dev/null +++ b/site/content/en/v1.1/tasks/traffic/local-rate-limit.md @@ -0,0 +1,414 @@ +--- +title: "Local Rate Limit" +--- + +Rate limit is a feature that allows the user to limit the number of incoming requests to a predefined value based on attributes within the traffic flow. + +Here are some reasons why you may want to implement Rate limits + +* To prevent malicious activity such as DDoS attacks. +* To prevent applications and its resources (such as a database) from getting overloaded. +* To create API limits based on user entitlements. + +Envoy Gateway supports two types of rate limiting: [Global rate limiting][] and [Local rate limiting][]. + +[Local rate limiting][] applies rate limits to the traffic flowing through a single instance of Envoy proxy. This means +that if the data plane has 2 replicas of Envoy running, and the rate limit is 10 requests/second, each replica will allow +10 requests/second. This is in contrast to [Global Rate Limiting][] which applies rate limits to the traffic flowing through +all instances of Envoy proxy. + +Envoy Gateway introduces a new CRD called [BackendTrafficPolicy][] that allows the user to describe their rate limit intent. +This instantiated resource can be linked to a [Gateway][], [HTTPRoute][] or [GRPCRoute][] resource. + +**Note:** Limit is applied per route. Even if a [BackendTrafficPolicy][] targets a gateway, each route in that gateway +still has a separate rate limit bucket. For example, if a gateway has 2 routes, and the limit is 100r/s, then each route +has its own 100r/s rate limit bucket. + +## Prerequisites + +### Install Envoy Gateway + +* Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the HTTPRoute example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## Rate Limit Specific User + +Here is an example of a rate limit implemented by the application developer to limit a specific user by matching on a custom `x-user-id` header +with a value set to `one`. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +### HTTPRoute + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +The HTTPRoute status should indicate that it has been accepted and is bound to the example Gateway. + +```shell +kubectl get httproute/http-ratelimit -o yaml +``` + +Get the Gateway's address: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +Let's query `ratelimit.example/get` 4 times. We should receive a `200` response from the example Gateway for the first 3 requests +and then receive a `429` status code for the 4th request since the limit is set at 3 requests/Hour for the request which contains the header `x-user-id` +and value `one`. + +```shell +for i in {1..4}; do curl -I --header "Host: ratelimit.example" --header "x-user-id: one" http://${GATEWAY_HOST}/get ; sleep 1; done +``` + +```console +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:31 GMT +content-length: 460 +x-envoy-upstream-service-time: 4 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:32 GMT +content-length: 460 +x-envoy-upstream-service-time: 2 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:33 GMT +content-length: 460 +x-envoy-upstream-service-time: 0 +server: envoy + +HTTP/1.1 429 Too Many Requests +x-envoy-ratelimited: true +date: Wed, 08 Feb 2023 02:33:34 GMT +server: envoy +transfer-encoding: chunked + +``` + +You should be able to send requests with the `x-user-id` header and a different value and receive successful responses from the server. + +```shell +for i in {1..4}; do curl -I --header "Host: ratelimit.example" --header "x-user-id: two" http://${GATEWAY_HOST}/get ; sleep 1; done +``` + +```console +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:34:36 GMT +content-length: 460 +x-envoy-upstream-service-time: 0 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:34:37 GMT +content-length: 460 +x-envoy-upstream-service-time: 0 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:34:38 GMT +content-length: 460 +x-envoy-upstream-service-time: 0 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:34:39 GMT +content-length: 460 +x-envoy-upstream-service-time: 0 +server: envoy + +``` + +## Rate Limit All Requests + +This example shows you how to rate limit all requests matching the HTTPRoute rule at 3 requests/Hour by leaving the `clientSelectors` field unset. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +### HTTPRoute + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +```shell +for i in {1..4}; do curl -I --header "Host: ratelimit.example" http://${GATEWAY_HOST}/get ; sleep 1; done +``` + +```console +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:31 GMT +content-length: 460 +x-envoy-upstream-service-time: 4 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:32 GMT +content-length: 460 +x-envoy-upstream-service-time: 2 +server: envoy + +HTTP/1.1 200 OK +content-type: application/json +x-content-type-options: nosniff +date: Wed, 08 Feb 2023 02:33:33 GMT +content-length: 460 +x-envoy-upstream-service-time: 0 +server: envoy + +HTTP/1.1 429 Too Many Requests +x-envoy-ratelimited: true +date: Wed, 08 Feb 2023 02:33:34 GMT +server: envoy +transfer-encoding: chunked + +``` + +**Note:** Local rate limiting does not support `distinct` matching. If you want to rate limit based on distinct values, +you should use [Global Rate Limiting][]. + +[Global Rate Limiting]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_features/global_rate_limiting +[Local rate limiting]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_features/local_rate_limiting +[BackendTrafficPolicy]: ../../../api/extension_types#backendtrafficpolicy +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway/ +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute/ +[GRPCRoute]: https://gateway-api.sigs.k8s.io/api-types/grpcroute/ diff --git a/site/content/en/v1.1/tasks/traffic/multicluster-service.md b/site/content/en/v1.1/tasks/traffic/multicluster-service.md new file mode 100644 index 00000000000..d0fd7a83fb1 --- /dev/null +++ b/site/content/en/v1.1/tasks/traffic/multicluster-service.md @@ -0,0 +1,86 @@ +--- +title: "Multicluster Service Routing" +--- + +The Multicluster Service API ServiceImport object can be used as part of the GatewayAPI backendRef for configuring routes. For more information about multicluster service API follow [sig documentation](https://multicluster.sigs.k8s.io/concepts/multicluster-services-api/). + +We will use [Submariner project](https://github.com/submariner-io/submariner) for setting up the multicluster environment for exporting the service to be routed from peer clusters. + +## Setting KIND clusters and installing Submariner. + +- We will be using KIND clusters to demonstrate this example. + +```shell +git clone https://github.com/submariner-io/submariner-operator +cd submariner-operator +make clusters +``` + +Note: remain in submariner-operator directory for the rest of the steps in this section + +- Install subctl: + +```shell +curl -Ls https://get.submariner.io | VERSION=v0.14.6 bash +``` + +- Set up multicluster service API and submariner for cross cluster traffic using ServiceImport + +```shell +subctl deploy-broker --kubeconfig output/kubeconfigs/kind-config-cluster1 --globalnet +subctl join --kubeconfig output/kubeconfigs/kind-config-cluster1 broker-info.subm --clusterid cluster1 --natt=false +subctl join --kubeconfig output/kubeconfigs/kind-config-cluster2 broker-info.subm --clusterid cluster2 --natt=false +``` + +Once the above steps are done and all the pods are up in both the clusters. We are ready for installing envoy gateway. + +## Install EnvoyGateway + +Install the Gateway API CRDs and Envoy Gateway in cluster1: + +```shell +helm install eg oci://docker.io/envoyproxy/gateway-helm --version v0.0.0-latest -n envoy-gateway-system --create-namespace --kubeconfig output/kubeconfigs/kind-config-cluster1 +``` + +Wait for Envoy Gateway to become available: + +```shell +kubectl wait --timeout=5m -n envoy-gateway-system deployment/envoy-gateway --for=condition=Available --kubeconfig output/kubeconfigs/kind-config-cluster1 +``` + +## Install Application + +Install the backend application in cluster2 and export it through subctl command. + +```shell +kubectl apply -f https://raw.githubusercontent.com/envoyproxy/gateway/latest/examples/kubernetes/application.yaml --kubeconfig output/kubeconfigs/kind-config-cluster2 +subctl export service backend --namespace default --kubeconfig output/kubeconfigs/kind-config-cluster2 +``` + +## Create Gateway API Objects + +Create the Gateway API objects GatewayClass, Gateway and HTTPRoute in cluster1 to set up the routing. + +```shell +kubectl apply -f https://raw.githubusercontent.com/envoyproxy/gateway/latest/examples/kubernetes/multicluster-service.yaml --kubeconfig output/kubeconfigs/kind-config-cluster1 +``` + +## Testing the Configuration + +Get the name of the Envoy service created the by the example Gateway: + +```shell +export ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}') +``` + +Port forward to the Envoy service: + +```shell +kubectl -n envoy-gateway-system port-forward service/${ENVOY_SERVICE} 8888:80 & +``` + +Curl the example app through Envoy proxy: + +```shell +curl --verbose --header "Host: www.example.com" http://localhost:8888/get +``` diff --git a/site/content/en/v1.1/tasks/traffic/retry.md b/site/content/en/v1.1/tasks/traffic/retry.md new file mode 100644 index 00000000000..4de8f604f96 --- /dev/null +++ b/site/content/en/v1.1/tasks/traffic/retry.md @@ -0,0 +1,147 @@ +--- +title: Retry +--- + +A retry setting specifies the maximum number of times an Envoy proxy attempts to connect to a service if the initial call fails. Retries can enhance service availability and application performance by making sure that calls don’t fail permanently because of transient problems such as a temporarily overloaded service or network. The interval between retries prevents the called service from being overwhelmed with requests. + +Envoy Gateway supports the following retry settings: +- **NumRetries**: is the number of retries to be attempted. Defaults to 2. +- **RetryOn**: specifies the retry trigger condition. +- **PerRetryPolicy**: is the retry policy to be applied per retry attempt. + +Envoy Gateway introduces a new CRD called [BackendTrafficPolicy](../../../api/extension_types#backendtrafficpolicy) that allows the user to describe their desired retry settings. This instantiated resource can be linked to a [Gateway](https://gateway-api.sigs.k8s.io/api-types/gateway/), [HTTPRoute](https://gateway-api.sigs.k8s.io/api-types/httproute/) or [GRPCRoute](https://gateway-api.sigs.k8s.io/api-types/grpcroute/) resource. + +**Note**: There are distinct circuit breaker counters for each `BackendReference` in an `xRoute` rule. Even if a `BackendTrafficPolicy` targets a `Gateway`, each `BackendReference` in that gateway still has separate circuit breaker counter. + +## Prerequisites + +Follow the installation step from the [Quickstart](../../quickstart) to install Envoy Gateway and sample resources. + +## Test and customize retry settings + +Before applying a `BackendTrafficPolicy` with retry setting to a route, let's test the default retry settings. + +```shell +curl -v -H "Host: www.example.com" "http://${GATEWAY_HOST}/status/500" +``` + +It will return `500` response immediately. + +```console +* Trying 172.18.255.200:80... +* Connected to 172.18.255.200 (172.18.255.200) port 80 +> GET /status/500 HTTP/1.1 +> Host: www.example.com +> User-Agent: curl/8.4.0 +> Accept: */* +> +< HTTP/1.1 500 Internal Server Error +< date: Fri, 01 Mar 2024 15:12:55 GMT +< content-length: 0 +< +* Connection #0 to host 172.18.255.200 left intact +``` + +Let's create a `BackendTrafficPolicy` with a retry setting. + +The request will be retried 5 times with a 100ms base interval and a 10s maximum interval. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Execute the test again. + +```shell +curl -v -H "Host: www.example.com" "http://${GATEWAY_HOST}/status/500" +``` + +It will return `500` response after a few while. + +```console +* Trying 172.18.255.200:80... +* Connected to 172.18.255.200 (172.18.255.200) port 80 +> GET /status/500 HTTP/1.1 +> Host: www.example.com +> User-Agent: curl/8.4.0 +> Accept: */* +> +< HTTP/1.1 500 Internal Server Error +< date: Fri, 01 Mar 2024 15:15:53 GMT +< content-length: 0 +< +* Connection #0 to host 172.18.255.200 left intact +``` + +Let's check the stats to see the retry behavior. + +```shell +egctl x stats envoy-proxy -n envoy-gateway-system -l gateway.envoyproxy.io/owning-gateway-name=eg,gateway.envoyproxy.io/owning-gateway-namespace=default | grep "envoy_cluster_upstream_rq_retry{envoy_cluster_name=\"httproute/default/backend/rule/0\"}" +``` + +You will expect to see the stats. + +```console +envoy_cluster_upstream_rq_retry{envoy_cluster_name="httproute/default/backend/rule/0"} 5 +``` diff --git a/site/content/en/v1.1/tasks/traffic/routing-outside-kubernetes.md b/site/content/en/v1.1/tasks/traffic/routing-outside-kubernetes.md new file mode 100644 index 00000000000..7382b9cb78d --- /dev/null +++ b/site/content/en/v1.1/tasks/traffic/routing-outside-kubernetes.md @@ -0,0 +1,168 @@ +--- +title: "Routing outside Kubernetes" +--- + +Routing to endpoints outside the Kubernetes cluster where Envoy Gateway and its corresponding Envoy Proxy fleet is +running is a common use case. This can be achieved by: +- defining FQDN addresses in a [EndpointSlice][] (covered in this document) +- defining a [Backend][] resource, as described in the [Backend Task][]. + +## Installation + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## Configuration + +Define a Service and EndpointSlice that represents https://httpbin.org + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Update the [Gateway][] to include a TLS Listener on port 443 + +```shell +kubectl patch gateway eg --type=json --patch ' + - op: add + path: /spec/listeners/- + value: + name: tls + protocol: TLS + port: 443 + tls: + mode: Passthrough + ' +``` + +Add a [TLSRoute][] that can route incoming traffic to the above backend that we created + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Get the Gateway address: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +Send a request and view the response: + +```shell +curl -I -HHost:httpbin.org --resolve "httpbin.org:443:${GATEWAY_HOST}" https://httpbin.org/ +``` + +[EndpointSlice]: https://kubernetes.io/docs/concepts/services-networking/endpoint-slices/ +[Backend]: ../../api/extension_types#backend +[Backend Task]: ./backend.md +[Gateway]: https://gateway-api.sigs.k8s.io/api-types/gateway/ +[TLSRoute]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.TLSRoute diff --git a/site/content/en/v1.1/tasks/traffic/tcp-routing.md b/site/content/en/v1.1/tasks/traffic/tcp-routing.md new file mode 100644 index 00000000000..d36f145e266 --- /dev/null +++ b/site/content/en/v1.1/tasks/traffic/tcp-routing.md @@ -0,0 +1,483 @@ +--- +title: "TCP Routing" +--- + +[TCPRoute][] provides a way to route TCP requests. When combined with a Gateway listener, it can be used to forward +connections on the port specified by the listener to a set of backends specified by the TCPRoute. To learn more about +HTTP routing, refer to the [Gateway API documentation][]. + +## Installation + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## Configuration + +In this example, we have one Gateway resource and two TCPRoute resources that distribute the traffic with the following +rules: + +All TCP streams on port `8088` of the Gateway are forwarded to port 3001 of `foo` Kubernetes Service. +All TCP streams on port `8089` of the Gateway are forwarded to port 3002 of `bar` Kubernetes Service. +In this example two TCP listeners will be applied to the Gateway in order to route them to two separate backend +TCPRoutes, note that the protocol set for the listeners on the Gateway is TCP: + +Install the GatewayClass and a `tcp-gateway` Gateway first. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Install two services `foo` and `bar`, which are bound to `backend-1` and `backend-2`. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Install two TCPRoutes `tcp-app-1` and `tcp-app-2` with different `sectionName`: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +In the above example we separate the traffic for the two separate backend TCP Services by using the sectionName field in +the parentRefs: + +``` yaml +spec: + parentRefs: + - name: tcp-gateway + sectionName: foo +``` + +This corresponds directly with the name in the listeners in the Gateway: + +``` yaml + listeners: + - name: foo + protocol: TCP + port: 8088 + - name: bar + protocol: TCP + port: 8089 +``` + +In this way each TCPRoute "attaches" itself to a different port on the Gateway so that the `foo` service +is taking traffic for port `8088` from outside the cluster and `bar` service takes the port `8089` traffic. + +Before testing, please get the tcp-gateway Gateway's address first: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/tcp-gateway -o jsonpath='{.status.addresses[0].value}') +``` + +You can try to use nc to test the TCP connections of envoy gateway with different ports, and you can see them succeeded: + +```shell +nc -zv ${GATEWAY_HOST} 8088 + +nc -zv ${GATEWAY_HOST} 8089 +``` + +You can also try to send requests to envoy gateway and get responses as shown below: + +```shell +curl -i "http://${GATEWAY_HOST}:8088" + +HTTP/1.1 200 OK +Content-Type: application/json +X-Content-Type-Options: nosniff +Date: Tue, 03 Jan 2023 10:18:36 GMT +Content-Length: 267 + +{ + "path": "/", + "host": "xxx.xxx.xxx.xxx:8088", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/7.85.0" + ] + }, + "namespace": "default", + "ingress": "", + "service": "foo", + "pod": "backend-1-c6c5fb958-dl8vl" +} +``` + +You can see that the traffic routing to `foo` service when sending request to `8088` port. + +```shell +curl -i "http://${GATEWAY_HOST}:8089" + +HTTP/1.1 200 OK +Content-Type: application/json +X-Content-Type-Options: nosniff +Date: Tue, 03 Jan 2023 10:19:28 GMT +Content-Length: 267 + +{ + "path": "/", + "host": "xxx.xxx.xxx.xxx:8089", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/7.85.0" + ] + }, + "namespace": "default", + "ingress": "", + "service": "bar", + "pod": "backend-2-98fcff498-hcmgb" +} +``` + +You can see that the traffic routing to `bar` service when sending request to `8089` port. + +[TCPRoute]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.TCPRoute +[Gateway API documentation]: https://gateway-api.sigs.k8s.io/ diff --git a/site/content/en/v1.1/tasks/traffic/udp-routing.md b/site/content/en/v1.1/tasks/traffic/udp-routing.md new file mode 100644 index 00000000000..b9d8e379282 --- /dev/null +++ b/site/content/en/v1.1/tasks/traffic/udp-routing.md @@ -0,0 +1,170 @@ +--- +title: "UDP Routing" +--- + +The [UDPRoute][] resource allows users to configure UDP routing by matching UDP traffic and forwarding it to Kubernetes +backends. This task will use CoreDNS example to walk you through the steps required to configure UDPRoute on Envoy +Gateway. + +__Note:__ UDPRoute allows Envoy Gateway to operate as a non-transparent proxy between a UDP client and server. The lack +of transparency means that the upstream server will see the source IP and port of the Gateway instead of the client. +For additional information, refer to Envoy's [UDP proxy documentation][]. + +## Prerequisites + +Follow the steps from the [Quickstart](../../quickstart) to install Envoy Gateway and the example manifest. +Before proceeding, you should be able to query the example backend using HTTP. + +## Installation + +Install CoreDNS in the Kubernetes cluster as the example backend. The installed CoreDNS is listening on +UDP port 53 for DNS lookups. + +```shell +kubectl apply -f https://raw.githubusercontent.com/envoyproxy/gateway/latest/examples/kubernetes/udp-routing-example-backend.yaml +``` + +Wait for the CoreDNS deployment to become available: + +```shell +kubectl wait --timeout=5m deployment/coredns --for=condition=Available +``` + +Update the Gateway from the Quickstart to include a UDP listener that listens on UDP port `5300`: + +```shell +kubectl patch gateway eg --type=json --patch ' + - op: add + path: /spec/listeners/- + value: + name: coredns + protocol: UDP + port: 5300 + allowedRoutes: + kinds: + - kind: UDPRoute + ' +``` + +Verify the Gateway status: + +```shell +kubectl get gateway/eg -o yaml +``` + +## Configuration + +Create a UDPRoute resource to route UDP traffic received on Gateway port 5300 to the CoredDNS backend. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the UDPRoute status: + +```shell +kubectl get udproute/coredns -o yaml +``` + +## Testing + +Get the External IP of the Gateway: + +```shell +export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}') +``` + +Use `dig` command to query the dns entry foo.bar.com through the Gateway. + +```shell +dig @${GATEWAY_HOST} -p 5300 foo.bar.com +``` + +You should see the result of the dns query as the below output, which means that the dns query has been successfully +routed to the backend CoreDNS. + +Note: 49.51.177.138 is the resolved address of GATEWAY_HOST. + +```bash +; <<>> DiG 9.18.1-1ubuntu1.1-Ubuntu <<>> @49.51.177.138 -p 5300 foo.bar.com +; (1 server found) +;; global options: +cmd +;; Got answer: +;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 58125 +;; flags: qr aa rd; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 3 +;; WARNING: recursion requested but not available + +;; OPT PSEUDOSECTION: +; EDNS: version: 0, flags:; udp: 1232 +; COOKIE: 24fb86eba96ebf62 (echoed) +;; QUESTION SECTION: +;foo.bar.com. IN A + +;; ADDITIONAL SECTION: +foo.bar.com. 0 IN A 10.244.0.19 +_udp.foo.bar.com. 0 IN SRV 0 0 42376 . + +;; Query time: 1 msec +;; SERVER: 49.51.177.138#5300(49.51.177.138) (UDP) +;; WHEN: Fri Jan 13 10:20:34 UTC 2023 +;; MSG SIZE rcvd: 114 +``` + +## Clean-Up + +Follow the steps from the [Quickstart](../../quickstart) to uninstall Envoy Gateway. + +Delete the CoreDNS example manifest and the UDPRoute: + +```shell +kubectl delete deploy/coredns +kubectl delete service/coredns +kubectl delete cm/coredns +kubectl delete udproute/coredns +``` + +## Next Steps + +Checkout the [Developer Guide](../../../contributions/develop) to get involved in the project. + +[UDPRoute]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1alpha2.UDPRoute +[UDP proxy documentation]: https://www.envoyproxy.io/docs/envoy/latest/configuration/listeners/udp_filters/udp_proxy diff --git a/site/content/zh/_index.md b/site/content/zh/_index.md index 505cb9ee68a..d3d0380c202 100644 --- a/site/content/zh/_index.md +++ b/site/content/zh/_index.md @@ -3,7 +3,7 @@ title: Envoy Gateway --- {{< blocks/cover title="欢迎访问 Envoy Gateway!" image_anchor="top" height="full" >}} -
+ 开始使用 diff --git a/site/content/zh/latest/api/extension_types.md b/site/content/zh/latest/api/extension_types.md index 6548b6b7bbb..4dc4ccd890c 100644 --- a/site/content/zh/latest/api/extension_types.md +++ b/site/content/zh/latest/api/extension_types.md @@ -755,7 +755,7 @@ _Appears in:_ | Field | Type | Required | Description | | --- | --- | --- | --- | -| `type` | _[ConsistentHashType](#consistenthashtype)_ | true | ConsistentHashType defines the type of input to hash on. Valid Type values are "SourceIP" or "Header". | +| `type` | _[ConsistentHashType](#consistenthashtype)_ | true | ConsistentHashType defines the type of input to hash on. Valid Type values are
"SourceIP",
"Header",
"Cookie". | | `header` | _[Header](#header)_ | false | Header configures the header hash policy when the consistent hash type is set to Header. | | `cookie` | _[Cookie](#cookie)_ | false | Cookie configures the cookie hash policy when the consistent hash type is set to Cookie. | | `tableSize` | _integer_ | false | The table size for consistent hashing, must be prime number limited to 5000011. | diff --git a/site/content/zh/latest/install/gateway-addons-helm-api.md b/site/content/zh/latest/install/gateway-addons-helm-api.md index 591af36c35e..448aa91e504 100644 --- a/site/content/zh/latest/install/gateway-addons-helm-api.md +++ b/site/content/zh/latest/install/gateway-addons-helm-api.md @@ -40,6 +40,7 @@ An Add-ons Helm chart for Envoy Gateway | fluent-bit.config.service | string | `"[SERVICE]\n Daemon Off\n Flush {{ .Values.flush }}\n Log_Level {{ .Values.logLevel }}\n Parsers_File parsers.conf\n Parsers_File custom_parsers.conf\n HTTP_Server On\n HTTP_Listen 0.0.0.0\n HTTP_Port {{ .Values.metricsPort }}\n Health_Check On\n"` | | | fluent-bit.enabled | bool | `true` | | | fluent-bit.fullnameOverride | string | `"fluent-bit"` | | +| fluent-bit.image.repository | string | `"fluent/fluent-bit"` | | | fluent-bit.podAnnotations."fluentbit.io/exclude" | string | `"true"` | | | fluent-bit.podAnnotations."prometheus.io/path" | string | `"/api/v1/metrics/prometheus"` | | | fluent-bit.podAnnotations."prometheus.io/port" | string | `"2020"` | | diff --git a/site/hugo.toml b/site/hugo.toml index 58f35b567d6..01c5f3aca03 100644 --- a/site/hugo.toml +++ b/site/hugo.toml @@ -253,7 +253,7 @@ enable = true [[menu.main]] name = "Documentation" weight = -102 - url = "/v1.0.2" + url = "/docs" # i18n for Chinese [[languages.zh.menu.main]] @@ -278,25 +278,29 @@ enable = true url = "/latest" [[params.versions]] - version = "v1.0.2" - url = "/v1.0.2" + version = "v1.0" + url = "/docs" [[params.versions]] - version = "v0.6.0" - url = "/v0.6.0" + version = "v0.6" + url = "/v0.6" [[params.versions]] - version = "v0.5.0" - url = "/v0.5.0" + version = "v0.5" + url = "/v0.5" [[params.versions]] - version = "v0.4.0" - url = "/v0.4.0" + version = "v0.4" + url = "/v0.4" [[params.versions]] - version = "v0.3.0" - url = "/v0.3.0" + version = "v0.3" + url = "/v0.3" [[params.versions]] - version = "v0.2.0" - url = "/v0.2.0" + version = "v0.2" + url = "/v0.2" + +[[params.versions]] + version = "v1.1" + url = "/v1.1" diff --git a/site/layouts/shortcodes/boilerplate.html b/site/layouts/shortcodes/boilerplate.html index 393892bd0ec..b120a8d1b14 100644 --- a/site/layouts/shortcodes/boilerplate.html +++ b/site/layouts/shortcodes/boilerplate.html @@ -4,7 +4,7 @@ {{- /* check if the page is under versioned path */ -}} {{- $url_prefix := (index (split $.Page.File.Dir "/") 0) -}} -{{- if or (eq $url_prefix "latest") (strings.HasPrefix $url_prefix "v1.") -}} +{{- if or (eq $url_prefix "docs") (eq $url_prefix "latest") (strings.HasPrefix $url_prefix "v1.") -}} {{- $boilerplates_path = printf "/%s/boilerplates" $url_prefix -}} {{- end -}} diff --git a/site/package.json b/site/package.json index 499d153e7b5..2ea4bc1f1ae 100644 --- a/site/package.json +++ b/site/package.json @@ -32,7 +32,7 @@ }, "devDependencies": { "autoprefixer": "^10.4.14", - "hugo-extended": "0.123.8", - "postcss-cli": "^10.1.0" + "hugo-extended": "0.128.0", + "postcss-cli": "^11.0.0" } } diff --git a/site/static/img/envoy-gateway-resources-dashboard.png b/site/static/img/envoy-gateway-resources-dashboard.png deleted file mode 100644 index 8b7128658bbbf00d094400ac59cedc247639ada6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 130568 zcmZU)WmH>T)GbVlyR{?~cPJ91#ickDic}~RfBc8UJ@@qfy-8SN^hi@k96_#${819;9<6*g;BQoLK_qaI8QeIbogwav3 zKc-VRpzX=^?3LfhO}ETv%wuk6g!F3f{0e-7+3-5vB6zWRveLZ2^B4VkZ}!K}Npqxm z;H~BIvXxPT75!jexz!ub_hcLqG%6(j=O>2lQ~Re8amxeI%P=*-{~7-OcZdOV#fWSQ zD*^qw4Svl%gT?>v14%g49Qy%V;{VTZw+P3m`v08ZzbzOAKYQ~;@+Rm1b!rmB_t;pG zYo6x+*?HIFr|sK@$0iR`Q!diQtACnk*c&yKyq}2b7w3uHf!QA4O~(1H7?XUR&fZLs z_P-{qLid*$4Q@Yu@0#$XzB=k5l<38~cY4DCJr^0`)xS*Yh*-*ea=3o`?0nEjL%;hS zW#Y4EZ=Ps7Hh{by1$@5pKJIe$Ql*tb=_aeRY)#Z9Q39WI zUL3K#tJ;*BxS(Wa4W_EvuG;}@TMYCKRJ{`>Hbsw{SFw7+W6oO5#%_u)g-cz3sc@Oc zTsSVY|N7@P|M+M85#sK8r%5phv&T%PEqV&%!O>~NSoP-0QXO!t$p1VVJF1(EM|Dio zUptD*-u(_B%vk@)f3z<#{iR`h`Bs1O-s>Mh~S+?2s_lJ&~+)B5$q)Ts5PH^viCMl~g-ush?(789IK_)7fN%8^E<9(^_>%XlC zA}UtL)Ne!85*tFbj5Qmh{GvBd*LP z+O_3~>US1w@GFOr)Z^Dt@CV_WFPl4)ExW$`cK$=l)tX8Ll!qA*$#Rj~{rzlcSh~NH zC+nXFCM?=Ic%nje-a9>;K4Or6fQ8ulo7h-;{1R1lc>CwGw9xa0?@ah&E}_{!u7Vve zv3M?q8NpSp%ID-c3I-F`wMxr1Z(l;5z7`J7AI#}svRB>68LlEU(_XG8s2T?{cGM}7Z*ZOJNo1CpZ9dsV`&4Ld zWToR^8&>x*1)I%e8Txc+?G?ln9@e!7vopMzG;<BPxKRxKB0?Xxs>VXj{_RXfyDus6!^1K=TACj64$i zDP7O6BQ~@jdt8r7U7A6dhQ8r8Z6FNE%h;0(v8iaUAC~pnH{tDNbR3GKP=>bOMJx1E zW+}g-&2J_T+tTzD*N@1Uz^XDxMc*G_&pnppXxo$1a8 zhE-od^*hrG5W}IE{RGC*L1z0AMZ`BfIf-V?oQwyCf6SMt;JYxU`-L-m)1a&n&C<1PIm8Y9@G$T4R4e8pHQebSkFZ zA^^@$u^R1@<{^3D`qjs$^b_S%)9)HEtD#-Zg5{?lj&~Kl4>5ea?iF(5zM~TPaPL~L*?YFvpwHW)=BQQ z%%+WR_}wb{Uu~5zm}_}H_rk0S{(Px%u2-UwMCxmav)iMT+%-1_Du6u{C&()R6=Dud@ zSEKVP-4A&tziR88(({hbFg0$z{FR?4b9)~U)mU)A)Ufq;7+zXn_qM_&A*Zg&P&276 z9@Nx10=JB;b6BuyxU}>S^OaPjUfj8Q!LAxZAPiL%e0xkvPV%N@b^P`wMx^9{`S(7Q zo^+2SI51l7Sjx4zO{=_#VbvzptmRz9)Tdw8x0B%nCPFEFCS?)YpwPaa9z>;!oiBgo z8@PAO;^W~pj+!P#yX(eKXm41R4zRM(GoQSEAEb9dLf6hk7xCoO9$lF0JAa%-ZZf5Q z?2dYI8VC+LI-K%BLl^b$wYMf z|G193L%_KR$2D+N@WayS4*%0wsi8SF{i>yut%*r<{K3U?MqY||oW6@M7&S)q`qi=sV zCqRiNoIp#?IwJ2-{yk@qj|1^DnJHRgV={^hkII9RJi^dCyp%+YHr_-qVn*pZ1Ey!YR)QfiynM?%!`z95LF_yXK zkYV^=J-OiWOX?B%o%=bm*FwMER8+}{6H~HoNz&7bW|}pJ6p(|aL2F?qEv34-RttfY zm9L6geKAk}BEr{PHj@#v5!w=Eucj1TQ&A2!)#g@`hK^rHooat(Kvl!F+>kn26 z%9OI@F&`FfyND@}yITZQ37DHjO&gXUUA*RCzrZ3kY?^ zgx*Mz-`*Uv)wC4c7Lki^AulIxjxo2h$NE=`vJw1Q5oeuCK?PToqu+Lhq~oHNI!{;J zLUcCxSOQJGNKctpyLtc>xRRdvQn~~eQ?+9SpZOm~3eA|#=?uHxtU9IbO?q-D)0pzb zk&gXYg|SEaQ%6HTILK8#dH+;YBwO15TRh9!(}kwA*uHSV$t6pjjB+2%2F*fn?PT+b z)-4684Hw*XQV*_uChR@md0MwEiFF{)P`1AB&<83AvEb>QJ1}a&;u2BsXXZ=B*iOFP1!75II9QK0E%I zt@|OTyNHEvn?4Qte5qdbv{jPiI#ymPVV{^&Di*3-tUu)|_~^*wibaSqW4~R8_!_w4 zyYNuztote@AMkZVFn4pTLUgo;JnMN!#LGf)F(dc)`>Y<1?A~&%^NfzFpfq-MmBht^a2RI)?3&Rr7b2sbz`QWJiFJ;~JZ67si^ zd~vJXb#0Zk?YH4;&ujUWvt0Dor%ZY8XzS~aXn3W0Fma3300wBhpjpjC@2O~4e8MIkHYV;-dy<@w#LjFD9G$u(tGU;8 z+S;leALN1ajq%%Brw-nGbzznp{cf_#3B$j@1zr(x3C6r|{g`Y{J>)K3Yu z=dDW^&^0wIU%V!286Anw=D!EU+6&L2kKHKbSdH3Xs51&sz=a$9`{HK9{FIqg?ozE? zODoPJt2EzY;#%wSv(h#tDoJrd_&vV;=d<|716+GUPo;TDg3W#zdRYh=2UQ`hmF$nC zk@)_677>9byQ!vZ!|wuvBbwQ7tqhdgy!S?zzBKHAo}zAyzBZ2^V3;ilM(2kFk0jt8 zM%#NbBcjEz{nrD~%8AWJn7df<`>h1-?Zo=lE9~&3=zuElfKV&xgS8fJDAwZH0&Y9j zXSJz!sC9zTJr{93)A}Y*%Jw^8tZ=`{*SO*dFxXoL4!*$UtmhYefy}1**hjI`%95zA zoW~Ychjb;l9z{c#gVUVc!S;MspFg;iBP8zQfARXFah-{FaA3kt-(>`+nfh0YkA5D? zqP=8ieE^t2sAdKt zbR=6c&#V+ML{RWTL&4e@%e2(+eom&M{Z%A&Na<32+<->DA8)CmTqhy>$u-L8(MYD&*TbX^UEpYsKT{!dCgqegHZugbv%ocb$aW;#z5` zW}gr^C0yqVrT;cmd#ZHSl%VJVQz}>rxRfw$+@$kmcMts_gx=k*dHY}GTf$dwXfsO4 z$8^#&?~gto)390T?j(YN1$+dR;kmvCwy6UPC)cP)a=?4K)xrhl2< zzQ4Vi3U!(J4X@S~gx3aIT&2c?$fZJEx)_42Z*P#HF5@=v8gtM3Iql34t6;QCgGdu6 z*{XJuMEy*bT|tXq*KOBJTP{H8`z}C;^)+(V$_8{kkA(aXzhMQGNUY842Wi(-rkPmn zx{MOS$p%-&h`Jy&y%x1ww5mzi>Kss9Ssnf~*BsKnB z=Gm+ehF*H8atbP|Y};F&^W)O1Od{S;NI_sSIH+HCXROE~A8rY`sPfF$4w6>|#xFCe za9h2!0AhG>!^ZI*4^hOus%DwE@A$nGLyh{0ra(+&V7?fkjrxeyRd?3EU@pVi7$LK8 zZHtvhj5CMc0!)sdK|#8MBJJ}G1@L){+}H}Rcf?9)7h#0N+^*iHCG2>D^J!b;$_sTP z>%dq=kf2rzIC_6kDM|LEI7qRTdtZrJejv#%7BS(0UF1CSRTEz%CD|1;GT+10dMUCG zbWI2S+jiQ`LLt`AWljk$Vb^0F*WsI;5pgLe+81)yexdZ>PKm1TPXuhV2ThpGh)3-# zlaocl=*U1TC1wo8bD?%`NnwNG)Yn69`KpiK96K?489(&3{T3%8>NhQRGM(|(?3o|t zx?m3M>*k@F?1gu@S{|5R`(kRrjxFR!r@&v_=gP5OHo!9IpaZnqMIGuWwQ%)pQ8n+0 zhnnKyn$wM(3PQL+acvNrR*OGosmT^tEeFvF(EEs@1=GCkOc7d(#}>3LbAlkqcFpOI zbn!CNiZGu`mL!ed1wcD=5Ql`LS1Ayes!sy^7lL-e_m4VBPu5;lTvB|)r~b7IBS-%> z4Q*4noS)0z_e8>X62-R@axf=yY&Dnrp}nqc*H%hwnwS5W+y)mmg%iyk{*`-rB}BuT zZwJQxwzs6@g^~k`S=}q?ZcYb&nJOf$Me?kY7}|ZiI!H#${i<*|J@Lo1RqXcWU`{3Z zzI?ihlH~dsR}azV9#Q4DX{wqsU)uK%JWo#wj#6(f86(3-vEYBNby1t~kF{txFaNIB zj@@~+FUI%cq}KA^7pL8h=uie0&xW@1jZ$+ZsWBvH5lrLAI%KUXbNA;jxBlRmiGzzT zQny!qTDL=NKGEtfNXzIChQt$0gF_r2t1`BVlG>b^%9~|w&mkT+rRs`8Vt@7J{o@pF zSBVXqE#`DUp=&jxw6#%5%zNHTcTuoqrS}rne%sW*c&x}ZFt}p)1t4UyzFzWTF=;S==ZkslTwjQ!_1?G zc+PM%#PGHxWRq#|t(jv*d;4&Oe)bs?iUbvPW!C6bha@P{J?eUzX#qjXUuG0o%!g+# z`Bznv@{L@3f%-c8K+Uz)uSW^PvuoYuYZN7S>_CccO-y`i1to{TAdg!txI*y|7ba=z z-d7c2`!)`%$q%YI29j8?rYCfW(n{7+s|cXE#2-qBUYHP?>#jMj7H7NWvfD1KSTR`HRckL#n?+;mW zdJw`hv92w4P5aR+WiJ_<5m@VbcO$RH6YoJ>^p%Y*JrO+M=~U}vQP}rr%V!*tBiRko zk1sWD^W+cpzRJf)Mp)qjjW0f#moIJZ}<__?#%a!5vS`@bB8BYZo~PyJ_GZIK>W1o(VON z9i4Z6-Ic$MWqudB{=x^@rhno28S+QrR54xZ-X}Bz#$-|AROtY!shW&W7P)8eXudsR_p7S{@pNP|cvw1G zUi4n{aA*~(GE^@|b_$^}EkErB(LmH;rn^4Cs(HtSgx3nWVdUr5SJ;C)a16Zpnb0ts z!Dl|~xoV9OB~22Ou#xMX-VH{)XP9^jFL;!WszO*CiZP-z6HS?C{%V`X!n2osE{^=o zd;f|AB?S)=sQ2phBt+k5ZfJp#C-dTOU@AeOxV&IobZCIBPJBO`KlcD^2VRfL4B6=H zUxHs&w6*V2^+?L5-zdweNg``nH;q`i_S2(vJPCT`i@aNdpeU_qv$@Z0smO6^s+( z9je)z4}zyXV?7vb(roAW3f&&^HUDiFp7Zeu`-h<4k!-2C*xYQ_yh8c$mWzq3G+*hp z1Kns?{&WlM*PS)&Tr(!mHgqa<=0Do4iAeh9{Y3FA$mn@CMJ}0*Sca6Xv+NGkvj{R2 zU=bX4$cZw=B9_u(13IVruIogbd#^l2jsmu(>oPN=*XjDXQ0%=jX@aa6yK}cp^(jkc zCL_16xoTJrtZ4bWrpK&PAKF;kh%d+l7Xpq>ZHzfSE%}{9*o|d9>$kOm{q1Z_sxr8| zj~R^M7IFfhLU1&&zeh$uPE4DZYi;}JQ%zQd z-rM%|`sn#AovCGQX`*PwD0ZriF)y>k7jYuXiNpND%!|@9e#vIu!@o}kezDDIqx5|> z4|<*4eDM=ZD5N1C5gf(roL0FjBt`SJO5Slae<+yD^OYU$+XI$$zYXo~gn#@fEp}Rl zYuz{cJ+ryjjgOg}V?A-IMvi=5K{Vcr{WG>;@$SvMb{vYCE6c#0qT{saA|KdB5E{_8 z;%eZWP#*ToHOqt|DT5-R`*;RBoXl zP)^e9`N7A+giTPZ?I)b9zpm_eIU*t8I0%kSmfQDN<|5+|qIsjXPyzM@zxG`^A{IVV%7tQA1amjM7T?IMp2LpOw%AKTqdRR}OQ0q;vk8jK=qv*sq zpTb=%B)n9keVH_^9m3{+q^=*Xi1Z+NLGk3fzKWKB9LR4w!;aQtANy%tk=;7#-oU`W zqrp6h0W$pXNmW~rIa-1zi1II;NK4wCo{;Gx=XEUE?T+fvpF4C`|M=&CNq%?p`(m^I zrucc6f6}PmX!YRU(RWt)tEb}J0(=@GfeDty5`8^}BY!d-?&a$X8kw{Q7YbtcGU&v^ZOlF93-niyj&P+t6MwFyA-YRi0Q&*;w2Ng(?Ae&JF zjUKuB;o>sueVqMn+&=eVdR_c+!gl0Ss~@H#$|Uwa4?s4saT?{Aa^>9>8P21k~#earQU zY5-uEBDk^bkHtGtc6im`zjkF!FN+iGJ=7=%Dzjw{X!|*!Aoagc0qj z^y=HhU;BL0vVy_u@kW2s3dqSjCZ9DAbEG_EI+TbuzHX?W+(m1_@NI2&W?tA#3@Z3# z=Naj^^WN-EneS`gnDLmMiM`KO2(?pf%~0RU70Xsuw?j-dZ23Y{?KbPT z`*p!vC6i&dv?xige=kctKC|U_e1g2^Nt2>P-eK=YE^EZo6Pj~7`9bH)r^&*#g7iBn z1PFZ(7&(eMh!bi7w6IPRbL~;=V(s#QU*_`#56O#MBvcK{VS-j{l$?1+?8w#$`OKe?6Tjd;9A0gzdHpb8~EAdi%$1uYI0B>#jyB@{S3Cqneav zF*jFkrXO%pJ~LqW$&#!81$F-Z&K&$0-q3m&>eAtDG1rGvtg*OWDX6~&+*XcHg{&hE zF`YT>hcTJ9izktrbk3H&Iny15ERG24QuBG5eSU*NRRwM-ZwgPoS zzt45W5DaS%(+acv^ONpK|4KTBYF;xY+*u>>YgkuS{}40M*wx{QM(kU+>1=7*#NG#6EWl=V! zG6!Y=+pNHJt`*DYDt{Cp8x!AnHX@pMQL?;kJcIA(fCbfFQd^wHkz)G?4y^;g7NL_K zZQ`lI#DgZ$ZzYPr=$a>Q9?MrGGC!RKmg1xWiy2PJrpG_)cdKl38N4>%6qL8v;`31A zzYJJ=#HVpeg~_kP(oC(emQvS{_2oCZ%J%6Pn?rro!%hPqWgR_V*wu%{H_UxDtX~<( z3HH~xg8cCA!{{!r-&nfI=KoM@;u#m9Tx&a10H-|QXto5JwO*0UwWAttREU|orKjUR zWnNCYz(dKGantRvCZNvusaVMd1>^KF8fbED`-}R4}R1U$r&b5z- zmRRHhTG0=~h-f2cPGdx+dqX}PSnWr^n1fdFzJ0*7-8#ZI?nS@;ke=U1FlOr#pdzfF zBZ*{L7HW8%A|3#Vw9sg&GIe(AD&LZ?_t(Id@tZ)&{s5Q8YiX(A{6t8bZp2Xc}e}2Zn!-=U8?4 zH~T{5=xaC1;2SkG!Q3l1sZT}e8L-8xhe#>9B+d8{G@4^uTw@t&?uQgH^{xXI^bTg- zMAxJ~aqnDRE@2Lb5pz2&M)}xTFu{oXKqJp{gCod@!o%%y9niLU-?-#yvcg6vNOCLG z6)N#3o`Sf`6U5Tecf5KkA#C}cMK`MOmaDj7R)$0?mA224fotrOe=+X-^p4$sR!R-w zYjz(!M#{Xl_wq0EKV)@yX&tc_gDNv=tx0+rb(}{}`|;7JTyzjLXrMR$1&U9vCY4XO z?iK8bN8(sWriLAlY>W>lS!8ohhwg&)YXikGL|C9-x^w79&d;N_w34Age+yl+R;1N; zpGg;iIN~aKy}l=z2S=ZI`)R*&i?M&}+}LrszOjRey;%Pxl68T#Qn&~Vn1`h0tylE1 zQ5!|*b#rijpb-zD1K&1|minbpN#MQ@rs`*J2WJNrM;!tPPw9ymiRjlM&)%eDM#PH* z+l=xgb~jE=YDDoQy4#O@7J-qwL{9(Ffr@+RTYcoz7|Y;GQX0;?s&aoIcP&;+vqZxn z3Z-R^-B|G-{$KT|>1#qF@IzVyIns!THztkZ`-k_FIG=UJ!WWHGqMt#?hanQbdX76jQk3dcm-`S7 zbk?~)U>6;~k;lAD;7lYvy$7Q}D!@v`J)bvdfB(<5;KSnkhgS{Ij?UW~$s98Kxj&D< zyS&fG?6bWX0gba-|k~77S%0XEe1;uB>V;9fMue7Th{I{4El#kC zT;&gGloV%)$2K^$tF673k!o{H`b@fBNDYtyw9&2{+`me1yFFi0xE~n`3OWEVd7V%t zz2j(JubR4b{;hvGO_W>L6@n`boguckT0rjkZcyd4t)N9Jq`7yaZQ`P#!Ka1H_ruLH^T6I;g7vgT0&%La8NKUxOQd=E#dVP-^cSCml z3TiXO&p}XfT0n{;FR9|^PBnm~bJuD6P!+ji!gc#Q_>BgDk_HSUr~en`7QNm{3jm^V zp%=t94BZd*yKI+W`M?$~*~*vq+S5@QNvEhLYQ`NtWFgc>Ki~T8=jiXQ2dHGF-auVj zo1mWHaP~hy%4ks=+upBc;Q?x197yP%Rubf%%J2oHRFef!0o^fhgc>$@r;}eae)ixK zBoj?eui|ucXG7^If8<=!JwpLAgl*xEP7(SH%#ILiw5#^qlf#Ni+T|C{n>i#niYzcBoBdhVkC zv}{S&=Hbi>JoBrbYEoAu@hV@T(!>x8bG3nt9*X5s-m%M;sTCVrvA@Z>I;82X8Oj@VfI zvr&Xa{T*Z1UnCsUtlY;adu6P|myP!N>oC7Y&o}H25!BmVBj;mTZpZ24JVV+(2B38T<-m2$WRk}a` z$nwO?ic~Q1R9+1MWtHn^S!8KG@+uWRSAIRy1kiojA}8yHs`EXgz;~s}=;!2&qInNB zWImF%wHdXm)kH2k*!L=}Lj-tX@hTH!j$G1K&{gMkp14*02>KBp7%_Ky5>LAbqC?4nM7>2JbUEZ23p0HCelmgrTGN8Es9C(3*pS8me zvfp%7yAOTi9G-&7mQq_U-%)WewbvrEdF^qi3`P@uC0qu%!V!o0 z2NLsA-7ov6S~*nUYt}9Sz2M3r3-e&Lziu!lEPqi1XdF4I!~?lyFVBUMy68|0#uK z#si(Fo&JmTUq(KVy{UfRjn>qV@1;%jn`Pq?1E-N6bjk*A%VVT&=;AL(K_E$U?hk}0 zv0dV^qN!&99D_)EH!=RNB1yBLMLQ9oJJRw%FIT>6C#e=0M}%@-?ECXKwmkBg^10}= zKdD6HA3V(=^JcFEq}j928p~D&VO9F=qRS*<2s~~T!lJAIlra|Zw28V8L?m)e8lGRT zH5H=l#X&~Mhkpw>;c}F6nCQg(sx)H?H^SzmbCrz*A<6~l00zzxKCFHt|~F>tQ)}{IKI$}r>#)jommITm@8Gt#yd{` zpSdYN%-1JmlqZB5<*syc=Aqa@X_~NZ-}zETSFOWn<}Hj|kk^vD%Im35NbAJjb2+D2Y^${&3oDnt7nlz|5S3=@oe%I?mA6#I5}*dfRZ8LU5dJqmm{8qpWD3%kLO zzHE%Ck{Pk;)t?@?(v!(HK~6`JKAG0iePZ3ocmwr1agnbtW0-jjX#U%&CfVyGG@}+! z;wU&KKt4*ST#U({`;DsoEfQ2IHJG5^OUtiTPhol%d%d%CTbGl6)du35ge>~ZUEo5e z5f;~7nKUh{VN26&w^+B!OQl;UxsY?}q&R;Y)AG$~l8-Qx14SD_MJ^2nOjY%&HYRZYMknDDnEg%>xHnT5_M`?H||M7wwq!D!zBboR0nKy+PM}P^W8Nrv-I9cV|)!DTK4%cxy)(g3nv^Qt=81-D)(Kb&$Av2ZgBGT8XzQP@b#Xma-YV z?GH((N?cLs@XNc(T+0RDVh2wWpH1apc+?JP9;zdFZpWUea81f^9Re$vY zox+x*7Mn_S@IxJNEF%zmj|c^@yR(J=v2gu*|J#N0hq*Wsydj~EY$i28ZgpX645f6r z$vbGY44UHu!D!?YF*Acn1TBxKN3+z%9shDv;`{tUbUi>VorXwT>$Ub27S&q3+37Ex zW0+6N=uU?nv(YXGm|y&1lNZYDQY8r&ah{06&&=Fidxdm|C?$Si-U9xX_e+71JKmL~ ze*LA>qARJHZ0MCE9-*|`7RwatiB&vwfb^z^E$7fOv`Y-pMO^hMNA``ERjlYNwfh+t z!|37)N78}D4Kl0cFD@?ZBR=j-j6TQ>a8jdeYbo$8MUh{cikRC92L!YZ#XE7$M#Ruu z&`_D>9v{&TMEU*wwM2ttgOS4wXuy6nT-5}lgedFPv~1Sjo$YE#ZvHbyIYYZaSM{)Ts40*M4J>ntgvVgzTQF#;Dt z$!~#PR~gO~cND+$C$TRyLNvZQ*7C)1BJHI*8ureccGg;m#5I^20Jga4YsLhTrhOEI zxc=izFdNRO)XM)#)l&O3mr%#W$2Ec&Z6oa&avOF?lt7}iHSnjY%q}i@IBK6+RSD8% zxJ}oAugcPu3J`V_V~2dyX+LKPuMr)Mlw6mQa_BQ0)rl+N*E}DC&XX7SAJh0baOFqF zD9r*5?=D}5iRgvp#BC}YsxH>#idUJ7?L+qie;ZUx@HsVDr_pOD-lRF+Kv&Mc z?9VV~094ED>Uo!F7f>9%qc|B>2MVnCIOI|t$%jSasxTi{SQRw01q8hj&RphawLCjy zsl0m#oq{?u+_qB7w|JwNw`=61k>7t-@(MW8DX1!->A|q-mm2xu@@VZ$|ECuBX^-ZV%qay|HVVFpAwtck%KRDwGdK|7!}}qAtnfvdv6gd zGX0uG_Vh!v_z4wiUK**XZj+zjN?hQ3L)PHreqeoSnJR8YG8eQ!&$_;sk~?tqxG z)@R&LZZ$rvd{)HCHumUc~LN7NR@8w=a%1Z_qIn!d2@zoDW)sEZ1&arNoJx0psYv ztLr`11$2sr1D)G++M|(g9HogyBb~$pL#C4v%@uKWU&8E0WumUUq1!}o;?zTa;t*NR zn^Zet%3>Yx*op3tu2eZlN2{Q@Fwr=e_g_KgTIHrxZ`QQMZ{Stxmn zk^}w)!Fqgf_u;g^@9Z~j(fV@3hC=u+W2}OID29?>FqKAyo8e}2)eoBn#X zzQp`AV)Oo5q;{(!hvWd;V2kJNDiKOOuNXnsIaOOk_GZ}a}=zhzc|^9-tWowjOBf%+q^y* zeYHLSCCIQSYQ4Qo#aAe}Dky1=Ia@AN!a+UTT+b4M)!g-7O6Gb87iBu%@uefaoIJ8# zTU#UE_Dcr~uk_(tpvU&VQDqP!3F~{uFZj7R$w9`m;omb&U4y6da+GA3*+Z+^PiB1p z55{6ej`-}iDxDbztv$d54R_{5zZNVFhg<9Snfbh+S0VQHE^{|b0A4LFlM3zcuT&a6 z&Z+_G+ETCik|wAP)ky-67`LUIYZE*=kK^gV4iL`gG2B)aECi%Pin*pdHO-okDl}=b zwh*d&kuf4c>9ocdl}%#Uv}L0vP21Cb-rj2DI&)2k0_9ATtXv%V!V(n|FGvP495Aat zm1~T_Rt^(4Bw(S``m;}it>&R;0kMqkH#_tx`d&?zk)^7ppQ%9dP+Lw@*$-yp zPZB9lV*O!Up{cBPoL>?SJnSLg=)wGa;nc7jKV;8+lW*jD^7tm=DT=y}VytV$rV87& zxzuWo;;};-Syv-%@n>Pm4>euBRn&mP#9t}NM`vBDDR{WyAsKm9MQu{+$vE5pBWX@p zr}dS+IvBN!NobL3I+mwaIijvsbg}x-*j>(yTv9}7jo9gvY0$K-?mlTCJ(eZ4q>p|? zdsnn~a7f&|_Uxra+?Uo2TeG*!#jnyfJ2BHKy0hIxones*Q7jP)16gq;Be6j1)??Qv z8umQ+M#q*EZR@d9@|3*@P}@;qQu;`ev#XJT<*giV_QR(dhm!qy&JDk)W~*tyG_nE6 zh5f<6L(FfM(3;Z48~DkN(#>FG2IJcxyF0(rDtzH&mkoQDz28n}Mwax_ytz4>3JrWs zF&KA<+0(ZkU2j9)+QZKll&;@_A+E=$hLB_GB&ZczHhS?go(;aSXn~OmY{qjzJSTvX z$3{JN?WFgSpl4vTITGr~SE~*Exk<nuBysP z$+A^_IW(&-E8&rU<#Qae_>iC4TR$)AMpaS8{H**ATZ#CH`iuGBEPnE_uWqPd5I_1} ztEjYHAZh)H|Avg<^CA7W%=g%<{Yua*=;1y-udc)l{q5r$J>F!7I|!~8$ommTCFrs! zfXk-?s9soC5awME;opHu&dGU67XulU8PaA>HXmvPb20R<3Z_g5u9U1c&9! zo(9E06YrXt?#wd)RuPksNV${k3Ab&gPq+na-$yIQQs1TPF(nU053XuP`I=1`w$g`A zZ$hwJnBTsklao-EZ@XI`_BL9sI|j_}H!O~99I&02;blAtKn?5f9U?6H6Bzj0>0iQI zMQM~p@pPKCr9SIR7NIY8Dq8!w%=eowNdY>n!Mi=J0Q$lK=~CKf3_{J_MGB%P?%#1R zj^A_peiMuSr=BgFDT8gyBt}7+3x9of5#w&l$>!(@${MD6%vl~&we+7mSD(k_W|13g z=V~aEveQKGV#pZnCt1aL@iAL^H&!*`eq!Av>|(8_Kc2iRIa^MMul}G_C)ka;d~lad z4Pc%&SARR%e6)#{da##Z_yH5F-%^y9Fz8N3-}8yRTQ;S2S=5}yB+IZ{=xa2q|Dx0f zbctDtHAeql^sr5Zd*j2PNPDH5G z8$8hvUX!Yy;d-%)Q@U`o@)x?mg8^xoh4P~O5YFFy7ca+n*G90bL5e>#Pxv_>|B&`b zTJmEz4@Cus6#UI!%W)9c{-TvRko^zLZ0#?#on9BiD9ZlYI%1imv>>SVmnoceb0~y_KyAy1FWIbV^`{U!?a)qJJ0jW z7~`io0ok2% z|2=)?km5TjJ;X0$S}+bmRLitv9yO)*)v+svr#~eHV;Kk9(?+3nZ+7N zDM^>*y-~UjNwo*bAPQwQ0hLx&xfFes4FNtXg7QI%iHCyaCuru=g}Y`h=ypIVb(Ha= zh>}HqKux-X{%kIU8=rN5Dbf&JoI~2sa)u}J^HsMoRBF=4zFq)%XEs%%86>N2FmU7? zyK596F_g5#=V%@(fOqriXYawtefo+84)aN-om|6h5r#w3tI|<8Q=u^#@X1LN9^8C-8>BWW@Kf#*f=*`9{$fuc7wj{E*A`aZs1LFl_ypxHZo zZF{~twKS6?=rcGD_Z^Ky5?kE7TC9^rPTM2Q&$1OR_bp&M-8%6LmFuUIw`F^m zFN$??&7T4kvWz2ZWS#Ny<*7FDp}&BhV#yMvpZmRj9LQa#h_RrQVIoO7szCMfLd}-&*0j;pPxl zDnkN_@{Th~gj=&+f`B0Ei(rvcHB{V2tT!5(aDa4|Kb1nV><34@QOo4fF}i$yUmkD} zE7og82cZFH{q_6zui;|NHpm2u(@L$D_*UY|m}IDUvb1KRx!|Ehx7Qg*tt)OT&W{k= z$a@M=Zk?YV`=_Y4uE?v9FEL(psFbREp4T7Ie%%$%1FnS5dNEXe~1)0MZ8)hB9oDhtT6)>O>kyg>AQsnowQ{z0_WaGS^WF_|ShVMrlp)!*d>o#p zfqRH}JON64S74RN|M!9|wF35&8#i*1_Qz$$17ae8QN>u4ClU(UQ| za`S5|n)azIW@Gr1ZSxq2j!oWm5c10riC-pK0ujfzALV{%$4eIHyGut=&XSt+086h{ z!t`0hx*|NqehR#8B)ZkCT$UQU!{k(=i=7IzC;rMo)-!b}(h^O)_z!byhZ^rZvq1&H ziJ}ZAUdn3gQsp`eHmw|4qBcCaNmeSM#?{U*%!A$M+eUqJeNn@wm(?~MmBpcl)i(Y$ zcl{cDEvK8NwNBH-_i2P1Z?_tbelQ>wD&^Y*Up8)N&BVjz1%n^lHjO5VyKB49hBA{ud3pCAAJ->=My*w zji_owHuas-GBno;4XW&BKt}lqbm;;p-FH*pQhd0mnR0W@_Y1Qr_E5M(eyLAo|D)_I!=l{UzQ2_&DV6S0ltH>dU??e(kVYD$1cojN z3F&SSknV;dMnXEIyJP6?0p`8j&wlQE-}`>Pyx%yE;acmuR-Ws>&hvM&FlGWlhW2na zXR27Uq@y;63R4@QIMkDYFi5ujr>dVGrz^|Cd{Y^ATd^)lrbC(;J*Ejtp_gUzUON8V zfNtsYqQP*3NoDq8K26|W^C%m$I&cYi)HJS>>3_am&)mzB$U%SbY*olM^U9w1aIZOiueX980Ul4lyKh z+ax8LCp_0Jd^=dRcc#Z25+QQUZ9w(eUbi`49@bnbu*RIgY@F7?Shn6GrMGfvT27yD@LxCfN6H-yHB zKRDSGmtRx0@rJyi^etux9zjUu*=p@|T40lWk#7w4lW6YOs}wKha*Qyqg__?qQ@;PG zU1&rATluEw72VYjcuX^YmxHf7=tRnU#G;$)o*-g=oc9>NYVcEeN@?TPtsZVZk^==M z8YLH6yOT>oj@;rsKVA4+{6eSNYTMOQNuhv54$C8#kBzImy$(5viHfe%xp{J}nkjfW z@y3!AG4ORB6vIs1(Xjrf6xD^-EL6Df`{36vvz{fS)UP|qzu5{U{bO9rM^$;niD?3QVpEO>9kBUxl2ix3J<9zzHTt>f3jnLG_^xL{4q3%-=I16lz8h?as zdsI=e8xbtlaZ*p^bL#o1UtqI%ZoV~0Qz1@ME}<9uj_JIKe__v4B{*w--0Di`_GzDt z&w7S(0{eRGJ6?=ohA~~D+D37hY|Y{2j)l%6y`uGqcV)5-8s$&GHEhUJc#YA>9_A~vSGPqJ zKtg$TO@(0cKYb@BxO#~ODXH?F-VfovL|v~exQX;xYH@h%5x$qb`|C0A#3se@y? zRsQm>EXvwr)YMTf>@_9Y0J@~bvwnowkl1!EyQKZbi-SwO+y=;XxVfQW_hT1ttxt<+ ziPxj3r1rRK?IviVA!Th9#&v|NcNs%}@^7$%HcXON`Z$L@tB^XZFPnB?k%DaiG(h|N z+X}uTysx{R#0FQ4?=qN-KNr~TVKeV8^RrnsRg_32-r;o-n6g3l%761H?9t8@)=A}w zf-oPZP4R<6{8)sLS;1{?7}K^_uDX>`XC_y@F;QTR-mR zbUWQNh$6v?76wrXt6_C8r9~-jX=Oxw@|4aL9oG zOdMCU-!Bvw`^o6SEt%UbA*d^X@l1cutzD$Y9NLrfp(2QBF$RVMOj)DD=%GptQbhk1 zEkD1n*L_m-VAaeX5G81EwH~YY<%|(33wi!R0upp6Ii6k0UOqI<3@#$kKuYYf2bjN< z&nJ0uUBgGqKF?Ru+D_z}4n04yyAgZ^0xsIEBrT* zkm#*Ji53ei`f`b z#_KN{`H^xaHZ_~}DYTZ%%kkPzn8qiEolPU|FtpBgzPWUwY^g?92>%NK0063v5phhZ zvo%K>hdMgkK}h;)Bn_V)#}H^-1zdn6oc00L$Fg+bn=sEE$`=y?MyS1 z8#DIL0tgU(C-?IM_05_~_s_F&*k%6?SNgLUCVFHB=ng|`zb3mMgw3vPr0~Cm|Iha< z5udQJKLsbUibI}rt>i9$`HdC&tNP!6u@e0Z1vNDdB>lYwe!tzZ$hWs6Obn%4RYL9Z zZK1zM``;c^dkkQ&fVh>K*>iHu_yI~1J$+nlVB3k^;nEIqM6Rn(ELQBtnfXgoqP z+MHP64(iAp{YEhS?d4VO#qlktDx0W0ZbL5oMZGXI_n~-fIUkiM7h<$p;>Ifmwz#;8 zr9nKZm`hA@BGP|*6!@BKPd}x1<=(^;+!h6Q-*oCT64F)29*KUBp>nX;&@GBfk;KO_ z`OilrJlXlLez9tlx>N0rHNoz_d0grzsFo&#vNbWJQKAQ76#autrsn!c;AKbARDqQj z0+)6)H*sSfa5EUiVJG_Ac>A-py%<`y9D@IgUN#zK05No{}P~_KdN9 z|F?<*2sSe58T7ERS)@W);#tkL(%r*L4c9aX*-8y#pAvf3vBpXNdaNC)TacKx8bZpwr!L|+VMEd{!AYkk~tis8bcVGEpY&B(L4Uo8M zKJx5tyyM$IJ|FkH$tL(XVP&nTT~o`dUc4ayzXpG@9IdG}HJ|!AD;LLCmF--t-jr2q zIrfwD-K@snn+p8%n}Sp#dbqacx%PSK8#A8%o+5qznYWUUfyrU>lB>x~bR^fbSScY^ zRme7x+lICeMZH9QJOcC8ZF)1^f6TEsgNxuTzlwJ}P0}mtuN74=zl4IQ^dQmIt-`VY zfZMS?!y3tzc`(A%azsC3s!TtUOV9A1ox#d_EtP0Xq<4YA2vsxrO-}oFYk;KydYRYb z?J6Yg|Mj4@zO@)QTV6`~KgW!k8o(n9l7$rZ$O3Egzsn2$+6VdyLi+-YNYn0vuOJL6 z-QGw1N56tQPDCv;*XrPk*=e6`zLn>xjl z>CH>gu=wJ#Iv|*hZYSxyPdz}X_aBqsNxT<5)C3?5_Nf}`2zH^K2r34HbUgU1n@k@KRMGVZ|wSw6Cz;__QQhbC!`{Sn!90e0|_>e z!3KEoIeh3aW}z1JgqfA`a;(QW>7b6CMmyXqgbiOSQ_`6fs)^-E~TlbcS?M z1-xyZG@2|)c11RcDD1mX z?zO5_5X~EXJM{=*8N}0+Af_^ zFDt}j%j*PHptpKC;#RwSw&^^(4q=4pl4?r%S&k*$l_c884DOAC<#VtpNV zljeJ0pOr7@xzf>0>~Pm+bb~_595bDS+@saYr#Lg8#YO=4vj;7Qmi-0VgWm2xvMxGk zs6L3eKRPN%42df)*2gcJ&^tc4d`)6@vfVkyKQkH~!vWW}b}XLU9ODy6h^n_>$5a&O}Ib@@#H~ zl(a7@Lpj)>k#*Zs_hZZ@H+|CV-VU z1L8C1YQlYL*GO6ka!*~vFdm>Co1ARGszQoeYu27NFHh}?*`}+Pz57XCOYsX%cZK|R^qISY^a`0z%4qVK1jPI@^LgZ3F=&ndirn}i} zVwX2^_gVPzJB{pquS+pwfr*bEO}A&CC52bf?2U)!rG5{CEhDu4NRjh13g^K*A?u!B zIi>n{%ojiWhuy%q@^OT0rx!w*b4Lu3j?v$}{mp)}DQ0%<_C95;-B27HbIa|HbnDkR z0i+-?kNbt-%M$xo4eH*}0(*G_*1azzV!_@^*7%}lX)Wg$=B-l-{TftzKqT@R_d&B& zD4VutVJHZbPWN&K2!}kUusss?x*D0{y$J|$i)J6o%QU6&yuuO7Y;HY0AOR7nm&L3( zeL|uP-zQil7m&&DTdVsK$Sp?1P{MdCH^4egDZ_^*&XFKzIlf#5G ziBS&%-}^{R1zEH~--pMy2sa`aVZwaLAd~(9c?uIdH*E*$*yrL%$ly|&B9X}3&Py*h z09#_y5zp83VYJYZUCjJ`MqU^9Mhv-xzrSvue=W+3f=L!AH*LnSpLxod{32Fv-JB}dW$MpyK4`p@ zD^{(ypLHDluML{%D+E|4Mw?|v(Asjeri?|Z0)i9Xkdd|W_4hPJ&R%U~YAfwaC+HPt zoeLk(3$5Jp-8PW{3x-r8u$7m(U9RcXSj@Vqz0_7K-ta?LVO#?(*ybBtlcYAzl-LJl zfo9VnBN)7EP@kCwdFScRSQJ24)+LaoOV;#_oMt+61Pb$PbB_)+%y{-zndwD16QcP7 z5h$GZRm0i0qC|I3Gy6x4Rm<;FCoE?tF5L~cU-h8^o6D}L)LTKz85z)Yh4Abz%X&nk zTxD(vII);-ac31vK8#M3;HUWa9yhqbX1@cSHLW{A7E!_yeR}{?3qQ$-pVjRZ3&xy% zcVvBJy|*!y&^hEdum%8?=F&)x_X1~RS1@)oBPJzfcdeEmuCH$}@u+EGxmNXXW#QNm zB|;i-NvYG?Q&t29QV$uIcEX?~ag!yriqzgbunil5*b!s5T3~q?7K%8DWmY zI}S1(=FfXJf@Ys@UheCycM(AsejtMG3fsKt6~sGN7NI*6Zryk?jPIxBA2=AQ8&g92 z&lww|4A~yd4Kg)9)B?IHAh&h)5&wh~en7bKrr6p;d5ySeaaCTA@!qI0ySk?$N)^UK z-m^+t$GN6c)&LHK`+#4#?>1|)4*8fwom*6m6`?WYohAT$e5t4e?1o$h}J7 zF4ifR(;hLM*a-)Xj~8f(@xkH&zpN8pw?1nU&EQS(D_X@-7~n;zJO3Os`UNu;FfjMz ziz>mh05a(GtZdG+p60#g_?rfj%}lQ>cu?+GkK;*0_CK)JPvTox0ULAks!BIj1KHQ5 zdlg{^S;|`i1KSg9wl~q*QG9*cF|$6zs#)z4KVoA z?2-j)bq8KUEN2``?96<)J&NahPK^Ea0t75)5R=Vw_N_tKD(u8lo@?8<84mt3KWKYs z6DaGK%~h!HjcUnVK8d5M1bbM-pC-u1(|M1cShJ-22?4W=mHz6b?6R8u@_9 zW!vsy^G)&322WT}?Q_!hL z|xzS}dkalP)KWX(D zx)Pf^*Yp15a`pe}nb+|}$&=o{AgRuSF1+U{u(|bGSY=4Dx8WLS#&(8h*8_E6JbY<3 z?%bzBt{7IST7K1%?l5BxDjNSdKY~UIj_@Vsa|ic+uQiw(FGd>0vLh8C^CYcVB8b9q zo5cm@lH1WdJHVz2A5D3hJ9D>n>a`S4ey%gO^^?7+fpb5_=Suo=KQie+0yCObW(YdP zV322ku%l>NZ+UhH(;i-9cMuOKNL4wd?$@CX3yv2x*0|E=1eQJC_Z2g%*MmJpW?o9V zmRx+u?mXRNq14J5>%CDI-M2=ODc`5xCH!UZsAyRvu5-wZFr{^m8tjE~njaWhjn!3$ z3w{if8L)2NbS_pCnEMg~1elCDN~Kosr@O#vp66$*(96K?CH3BFx%@n*wrO`Tv1>g~ z2Fz5&#lSPbM8M?1qo}Q5Q*QoPzi=Ncv&?O^I5Z^SVi`AuFXEMLIBD0=tMB2JO69mG z*J_7LD2&MizPmld$3I$|84GMN(WJK?T~EFMqVf>>?BFZL-IwvdmSUF@asO$p&g^E( zPwef0_{Z(WhG)+*w^A3UY)5-@^a+FsSa<4otZXn8>H=*rw9=XNJVVL{cARdTwZjE9 z`}K`W#OEGpG=5CL;jp<;tN+loTxwdyo_WtY4d<$%GEp*Uz>1GqNMvHe$T-|7q;~RBXVu;+Mkk`>S6zw6IB;c4W zE+v+JpS7{_XbBp1$ zv%QF|cDuop`g7wK>O-tY9)N3CD?f;bZ8pjgdbwq1tempij@d4wDP8OkPibI=Ysgq5 zgoF!q5)Taz6j|;ILF%<@DK^h>M)xLL4B|Emd2f5xN&$=A&GN*%5zbb%c#3-m*+Sif7lW^Y-A^LyL5 z)s*GB$3ogGv+PhQGWBAz5x(9VR-OM#!npu0!$1O+ujsL>gtBoe^5yIi;YS`&V_6Du ze+$A3UpZ;7Pf9z|r?)h|C>(U>x8tYp#sb!*&nf#44VRMn2XEcv6PNgRn3TAj>{7;; zdXxbxwy8#Z*v2@@U=Pge^d7M+bnIs!y}!%`_CP#%r9sQ)<61 zS1U$y+b#-bmj+vmW~WaSE_FUqwtTkJ;C>FB%u_VCT7bsZ5^Y3kq?ahpfhG%7LLc&F z|6K$RK2c7Vf}*mj4^@Q~O`OD%73e=xm!bxG8cwYH(EdA^#~_9E#YYyEds^UDq3j0rZ(@Q56eN1W6P78XGfgDXV!A_4WU3VZLrhe|9SsHn9%eC*c=02jCq4K}{=4$o zYcY3K-+&$<_xGx&w0UU>+;OnzYJPbfwp@a!9zW@P=!{nR;;Yg7&a!73YL5xFvq2tu zR+$S!i-@>g!KriP?QQpAa@Z>_u5JP&Su@TcXEWM}d_{cx46Jo)rA^vNo9iWe4R$J|m>53jI)M%UQRaeOZSt1Ta-zl2rz zZ=jPisrP(u7v1aUFdb(2OCB|sWa)olVI|;1kuX9HllhVi+8d)U|D|1PYlr7gobZb~ zqM3-C=P$WDc(kU6y=qb_y+mVwp3{gqz-F(bwXplIo0rZRKlA9NJpWp2b;#rs_9q{q z>-DyFfU8ke+Pt(zx%e9DIfDhJ;+ulG*MEJ%8&#M+m(^Hn=tP++EG!AjkOEO*w{!g0 zYuneYkK0*?r@|4|`s&uu8pp9*P~MwQ#+3~-qiTPZKwr4jaIKXD8tkWJ|KBgd=?Yc& zE5`~rTr%&kX3egO{@*+!1=0WNEH&PL^#%{ozgoI4k6s~B{9nH$2mY%(Q&qyeP}cwL zK(e5}{m_;tPyDYE!e0No-og)K8}sj><9&s8B%7#wQL#GhUmgHh>1U^(lF{2*3v53j zop#XHwfEie`u9|0SKQoeGmzQKB1Dnv`^}PdL5Fc(JL|lxwaV%9odP?&7P>o*kv0`R zYC<1`;0h#;uyV?wzy(*I=a)N-xjgTo&n)3hLSQ$6@^?+r`NYnOFa&K@7;=g2`~_`U~{N=6z*l8h|B)S zFtc(hd|1rrjL~Yk#x&%$Y32ienVA5&YU}wSh(mGQkioe_>YDh(5|_4UXyor@3#^z!ub2qrwL!h!G#REKzW(Tk6>F{)Nn1WV zE=<&erh|UXH5;Ov<_RtEf-j%mm|leYN9@g>_c?Ew)2|Qj3!P&wPQ9E8XT$mWriGzK z$~*PZ{iB(DMS{qk`IU@cU^iZ~Tkc&k3vi)L5`KEqCD!bYwbeo$zj0=)xy2~`61Cfz zo5x;A;;l>7s|Eva_~*;`qK%Zgmq(ZKskT#N;>S=4oOyRP<}(vzxb|j%bagb#5g)Plix04v z4z=&3bJyfnMIfE72vG|b9UAvpnaeM84F{5Q8MQRtSD6`M`4mqQu#RwnoC}7q+Y{Jc zOn=#Nw00q)v(2Li$Gcnf>Zri0;j&?aB;o1HKSwK1^jA*3o>c8kl9{&Q)(g)JUEcNl zOl1E-3(|fGuHbOf7pP<|n7iZ-|Gb+=<9&-C)ExCdr7(VD8&l})y@7i?n?d6wUctB* zXE@Hssp|eo+cWd~1B6etlsxEpyyWTKlif0x@S{6?q%96ef1~?*Mn3d4qT{W=@pbt{ zd+6bn5cb^Kb?wfq6W_cc?EVI3vzhiY_v|fm+mBZY;%BSjd`mWPJ+noO&7VKO-`)p36p+ zS2vy}&fH49uvz(8Y%@CEC~h<}N)sF~CjV7gJ>ba8p;v59Ri@|rSH=iDvEua9S7_N0 zkvA`I(^S;UTkZ{MZSV7KeD=^_1~E_(8p`8!LP>gIY)`L zKErfPXD3!RZQ1Ga<4AHydbm8k%JW9bX z;!Pdfrls)YHp%V;PCY^AZOcq_#DV8Mo7JzYnjfEkL9TgaD`;Gg3aj$^JBDxSenI3S z@?ppinI;@5lr*d42dR{iN5Y=UxbKX0FVf@F->}6e>U$3Z<9sOd3Gvqvw~$kogW)B` zqxj+JjH~1kosd&{qcZ5`Q@bE8GlQ2Lzb9TWxw2F$D3khjR`hl&iA^8c<%CIfPQu`RMjhL${%Rlpj!RQp16b@l>bacIx?q1}Uaig5|=lA&9nW$9?K z-9!2Ew)avuW^Pj1B4^!8HUG;S5m82Ny{!r)F#S6+JiO~SGRV`=)cSHpNPB|Z60D&C zHzS91SE6no*N>8k5NVUCo(YsqCU=;o#yaj5?^gwsGPxK9yzPhk&67Yfk#}*EkQvvz zr-4YM;m_vc)?y)&#D+O6mQMaXaj~8CyZ}X**+F_`aWk$aAIsB)(tX|PUL`w!7V<{nYJGJ!;ar)sqv6K8SQTdV-0yeN|y z(SvCRIIqX2xoUA*LDVVB52}C!QbbO4+;reJ?tVtLo_?Q11rTfMC{PTu2i_OqXW+-^ z)UmJ>&o6yd-}PJ{6uA^RkfV`1Fz`c-*{&&YzCNoiZ(*QP&2>_#)N?x@)IUG&EB3JN za7y(b7QAsQgKaAbxo%M)Zr95%x(<{d5>~c_lf<-#qO5eE8!p^Xh*+)*hJibRw-)RQ z4*TJ8R(4RYVjR2!uY=nS@A)H@SY3-V+>G-JA?m9428bU&dsj^p-A0uJ_&8=Ars`r$ z63tE5DLK-)e6*e)|KQDgoYTIM)56mH{Z4q%=aWI_if^g1`v!V@vKHQ~uu( zlZux*O{=!vRNwX&ApIg2C&h;dM`K1Fn;9tBgdkOZQkS@<#pTYMSvh_8xYgzq1@u-x zrem4JeCF$m+bcj#?Ef<}hGo=ClbFNk!mSAEyO9j{&Dxj-h6{kRtq!B^4^zOvJj4(~bUB)c z%9p4nX)^fDhv|lTCMhjH=X&aTaOn^=94mh_uHG2FKTBKBl9_`oI>6RzJi7lj@E#|3PyOh&jnKUP2hzf1=f^4@8TZm<9e24(EJ z@d)}h&z{0*r&#LEX7%z+SIMr^jCU~RlG?YASq!R-yM4~)?{nWTI^w-H=V@fQR07?A zk0{lszm*5GaJXJY!-bva*;R2J7jwm_2dY)GBFK)eV#}{;+Oz7WFM1w%cumRF-Bpr! zef@BNw_jrnPpxwwR1Xwrb99K1N73todtuHW1^| zuS_CR<(eP`q>wdP>8Q+C&=6$PcE|PMIiD-_&{CFa_A>BpjtiZKnikLiX1aZUSBFPNeehfTZ1``S(mmu}$~VDa3uf*g z7+x9bcM-AOzT9ftc6#h_hVRq*WAI(kcmf&>qq7???c3dG*9`lD$e(^wY0sY+Le&n= zjPex{fymeRBxX8BgAQrM3Og8Liq`FZM0C+*bkQ5GN(cFJW=iBiKG7FK6-?{_yjHkt4Ew`cJ7i+r<=XH9?uu(n2fmo_PWnAS)@6&o zt)e<^6S1cX&~hy&#oHHhst1UPsO)sZDexz={pHPeV#pJNi_HVCi>}m6S2FGiHb!5* zdDncFI9n*{+_NB=^-}zpyFN{cRm;GH#cyn3$E zsC~*}$oy1oINC6U?HYe4JZvVkRHyvD)y@JM6|BmuBr)LGq}Ka^==R#r(GD8*$$+La z`SNE|_m3QM%c)cv42hywt}s2nx=-_B9vc(*do7pvZr4Iaf*eE@l{T$w;xO~YCtcku z%gF@To=bai0@W+fIT5p_rP;%WsC2iZ{f8dvYA0K-UXyZRaC9x4^DV5p&>R+WK0Bw6e+gT~ADfoL?G3ziAZ^d>DLF(^n6S zfdL&mnj_<$1-C_^!kJe-7>2(GkbrhSU_`-;W7=~jn=(dv+%kR}t9hZBe`=zsd*Q5IeU zpM&KIJkifw9Onb{cB5S|gFO~OtXeaSA+Dr&F#LEJS6hPXTX#hIRc|p9scu*67CMAp zFx)CS2Or3_ajtQxk8 z9?L!=O4_jbd^~MYo3|8-F@YAEtz-Hj`Uue9Pi;!sh++K!snFzTGaH7AqbT8r+sN zeRtulJYR-QR1DKq>sEZSX=%#+XaY;oFDNI6s z5H`S!(aU0l$`I($P5Nyt12s-lj|7t$EC)aEk`ExcOnnLA_g+xvbGQDTWs-P)eY%1@ zGy0RGZ6^WzHyG%BCcI1|Nm2Xp#BKHr>3mK+$Wubgb-pR*-7jI45P8 zxXcCqBl{&h&b|ijp95+j^_7+)q{lsOzav)7i($wz|IVPgjE5D#VG~!B^$XWXquObJ^z z0nFRwc>47ch^?x9gq**#p$HBQ?!~}v#;{5wk#lporc>?`hC0Ez3q*HJW#HzC*lpC7 zg0g;ffYrI$`okp4_qQvk{%}xnO}yu z2Tc%SnDC*yixzm9xOtmt&Q zl9zhuOvU4IFiJb+-Rb!8+g~hJSe3Mf+$A z7Z=-`uTJd7FI5gDFsw@B%f4rkNSd>n-h4>{`M(%}7zR9H(9Lk& zQJ=e69uqAk3iMCxpX#$8`|=&|H#$5=4K)e*h%I<8=YWKWv$0RH%&eIc$r7_WkckP-xWFl1_DoZG8-CwtH30E5@zSn{5OwQ$3XZJnG>cQkL>p!={48UfnClCUV z`E}37;EfG#n=^XUcG9JE+#Hs{DDhA59=83cG3#_tk3!9lK&Ow+181BP@2YY6K0;P} zM0utM*CKq4DcFg77*6Cgz!ib7m?$pU^*sqBa$m9CUchq@bQQg)n(X&#S#uz%f)iT> zyn)p#qRuEPHJ{IjOt-oHu65x^`_&CU3A{H95BniWYYZ)cuCwwTCz0<#RS$9LF2wZR(@jCb+DN{g6d?F|-GD3R`cEO4BT(zV&{_w8v40&N?Yor0ZIRn9L z`_aQOBM=19)~`g*8&0`Ke7m` zQ>+)2;K`bcU2@rMN|ZauNdC$iK7I8^m@uh7-S~qo9LOK(v$*6rMnc?r?av))a8Dq( zPfwK3Pz7FS`@g}3JnSA3K;GgUx|-idGc0*bJh<%is)!QM_HDB^WDyz;U1|HkjXs){ z$S*b=As+B(&H>(0rqi=u`00bRuL%`;*0uy(=n4M%5M&@S`PD-C;AiFm`aBkfO~^qG zUD7+;>d$vt1L?ZyWN1AhJ{az^A0Y0GY}Qj=VL4;t1f|8Y%N_>M5n%$(gz5ezqdAdiqHL+@A^fM6g_fp9q zQbHd=SVE@%^pR-!LPc+JLA&KkE+B1nhEdJt}Qq7R&pZ=XFGF=_t%%!>3i9g}!CG{jtZq(1g(C{AJRvymzA5yvM-Y<4a`(8J}TbX6&2f@vHx_ z((#@cLDkH1<+pkC9E5vuWbQrSH_ykuY^Pq40h-kML#(#8 z6bi;nND70|GxbL@U+oX5gj`tiT6bpW#Ed^aCsHQz%)@)F`*u58P>;BGmROmyWg0fK zykabp%y>*qNhwp?#U)PEcg~@%jmGiKBo$0<-~RXkV;R<%X@eMG4Zn?V8)l5djC4V% zl={*^VduT4{sQyG=6EhwhKhKS1mi$(kvx0TqzvK+mv7M_lBJ_VTInnx?mMynXOG#_ z6+#k%u&oY=AvurqGn>VeD7)@3Aph3XL*Jv8ZYC!cu+gwPgWAG}BU;7|8^#kg?SZuF z&Z#Iv18EbES!_c2TD;xpqallLSu6IZrI!ucoMyKWF=kJ(5)#QK*~A|#sUT9Pus-?4 zKn@inDC!|bC8R^q#U1U5J%cE|ozIXI(J49%29K)e$V-yEeJ4Sa0t(xd#H0-rc>nB| z0LiHcgL4b+Js>E%EDC9!3|WY=!){w6BXBc|zvE36 z%5-;$k&PKGF;FL#P!P8BGzHX{1hSh6Ar@=;5clULO>l|=a9Cdfeh4R0{SoNy@5`o^ zYYdI5R!kC}6AJ4!=$+~5?wRNrgbx_(NO$zfmsRn+w}zxkoIg+%9qCohQ2Zj=?Wo87 zP%xnA(}^zR*T|aKY)<69!dc}Xe|-K!4lJ(LvOGl>srY`otx};w%67Qsy_)j)qZ20w zcW#2+_HV`opKZrWOuywYr;R>9ZmQ2K&<%_mWNo*r^<-`PWyqEI9v6vGsM8qg%k$vG zPlQ;e?zoi=J~mftq7=KYhF6mksFe!e4Oofxgn6vqYmpH8V=)A=#>x2UUaXV&oYr!< zQJ>hOOGo?U(D7fG76r!O21fmYv{`)97E22ZFZ>KR;~y`#A`euHD+{15XY9J$@Re&N zPoU_7d6eNZKt{fKEy#`5eKf$gSfTQHqDw&~JtrPiu}Cl*V3jiy zOcu|7JMda&RnZA+EV5C_>rT?3DbD4`q0aL$Cf7-xHbi!j)kutdUlORq9DF{}%kJ%1 zOzI=H3U!t{*Vj_?# zdo~kgWYv)_v*_3BtAIQl1~&;pupfB}C`(+@oI8XAYdulCN-7?kAtDf$W;YLm-v3Th z=DQ>u`~z#H)VsH6iIf8v+8^flB3OKxe5DyHH5tR#_FeGZO>vEaT^U9jFk&1ce7081 z7f5BjoWWFPn8QKhQqR3Ly}&Vp*(pBAd?Z`~WyLq@OcOOb@hjshWts01f4jIKWFAie@3bm#qP%udswmSdm(~=|e*RFwi80e1? zwF7oK(yvAX3)UrMD>PXS3x)G=F5k@S6`=cRkA-6Wd4P7bRY(*O{e>NiMkTQ*l@V_Z zeRZd90j=n92c&>uPE)o`w22GIPArRYNz_=Go{#_TgFFi9pj&~SkV0WPI0F*z1#QnU z>36Aky!4}&q#4@V;!3@(xPq%01zWV`jQIE6r=SxhAn6ghXeIk@`G@5Xqy#wVIF%W_ zGBh;o9C}-CWrRL?p9~8f%x_*wqsbcmp+O8J50AoE$WhJo(kRnEw2BX~dX&xn>n&tc zRZg-NH}A;=Z|6p=mw{_7&TQb0w~@>j)JxftsWRAznDJ+F|FHygH;0cuMHo{-7HCGk z&wqU8$TK&7OXYSBrQI}iLOV9JNyzE;KJi#inhzW-L4G4_Lr@%C{%}vj8r#U=;?yIW zkPY$_Qte@49d&G(r5^hLIigO1*V~Kug#gWS)H=en#XEU!_-cM=*^49neUaI+exdUX zzpKm^HS5AiWQ}|cAlmKO$3AH3cXNA$p3YG$38Wa)h27rvBTt?$CV}t=9Z%UVq4&9L zoK&2G$!|z%`29DCcEOuX36gdM?`Lr@xh(s1monR|SAKNfzE*yx5^FG>HE?&@B>sgM zWv>iBg02o&Vdrt;^*S%M&0Vf%C2E-RNY;e6ZUHMDda(uXVu7KMs#)+uKsM?8YzDPM zPUNaW`9q?#ziljgUN!l#jp<_=|Mj~!<*eVh(_*Y+VD91)!X}*!OFVkA4gwJ$QKZj* zSyCss$L_^`Rs{_Ta;z~A9yyuFPL}KXsD@Z}j4NPL5gjOtA&On5jXA%gPs=2sg|LQx zH*S6#!ePG1sYA#J0i?A;7AFOcFFo11X(rLnF*U(a=8V^yWP}GKp*}V2#>qEAX%o-(QU0Vvqz$G!MPo?QKwS;C z9B&{|VA^!(g3w7P;f3ilNb8!%g!YZnv!ikas^|`Ig$HNPnrTGc(m!*31V$Ny+5=<% zq3`z8sQL%m^@0LJZtAHG{d`oL&)-u}Glzo>p&q?h0hGDxY74IfqcsQhD%$nu&a*;a zzFBKzK3*4XMBo27U4SLZM8Wa;jP7#>nv|Q2Ldy?CTb!}J4JsavqW9?n4B8ubBu(%k ztuik(G-dSwq=`>?TRaG4x9Pti`x#`B#$#uhjDS;dF7cQ>didnQTk5edUIkHt-m8hP zOPXyJyb@WRfA;$J6oh&C9p<1xs8{u#>2_ zvUL_q{W1gRnd0uqWe>7XFeJ5mDD zTSD(unsgA5-V`JhrT5-@?t3nHF=YF&z`+!=9y<^ z7m8!Sr1Z|SeCo8HU{t&n`gqqagbX%FA>Y*FEZ#tLlI_34M6NWSX zu7Pwo-dd)IcIPvmpgym`;cqB;Xv;j~Hj$D);w7Qmk3GcY?87W^zux!+5RepJh$d1V z{`@+A7JEjL?~FG)KqBVj8(&$dJRNoK=lD? z3%g|@UKnGbPd(GGizqLUt|_3gVTs4c3No^eBfRPK`n3Wtrc_FnQ37C|WhMBVm0 zJ-{q`>SS3$vFPf&>+!HB;LW}(495pg-8@S02CVMoK{1{Rw^riZINZJWv*(F%NKoNV zOBgSYlqQbkjnukvv-3eG<85M!m5?ow-LDB~JNiWJM~I1@9d5eD(|A~jfHNZ-@9{bh z{es@CPD{tCu-%hblVYmmjii%C|5O=s0^fqr7a>&6pRyY-x7)2OBDG-n^OckS5kgN+ zo>bf-Z^3S&;Q-^)JZbne_#8X%6)}xb!+gYSVZSZS;{cq4k;iwORa4HyA57ed)bo{n zYwF$Ecp#S?&&r6;1~h-S)on%SU?QlU#SLnXWc6GMcip7b+m4z_wNLqvDw}VLPvbvN zrIh}z-+U1D(KbAUb#ZdxM4I@~i`Vb1h4`8JTVCAD3~Uu6%OFa+?89Jdc5Auya{HP@ z-Ps+lS4A>+B&OJ`@tfjkLmH}Gnse)(8u`x)uyej|r+0e&9rG3ZWBL|}%JJJr zStK%kg??E^jBH&S52+=7WG6H_E%4t8w*qY$rS?(d%az!^YJ3yR{2l9_k3TCi(tjVez6DmThEz6n$hd#xS)4EtM>0v{@C<7p$;={{<*D0*8`s zFA5PXEkdrEy&ElSIkx+*0aw0IHq&w94brDdRz^n(^hU+KN-`Xux1%$)Q>>Gs)9Xc; z{r&ZZyP10#Q;D|h^1;*hWrT_P{6+3gl^A;MH1mcn_oG4kt)cGxBYOxB)(J_QudIX3 z)vmVs@JIgCik|rLks}=O+sUUSM9Y^XvMnF{MzYx#1ld$rKXtUe9LD6KH&+|d?R(FW zW1yJ&P*H}MabVC{E7o)*OA%3QFzmwKsw6m=8Y0sQrrtMcsu?QMH%k4 z4dCgJDd8 z;i{|&xM5m?@n_FtVC{@52X0;!3$Z|t4p=;n7|H_$>S^xW_D~AeNqo{Z68LX8*jVvJqrAWJmhw^)^>x0G;J>G+46@rA8YWy_KCroZ ziE&T-NTPcFl9e`2G9>nX(4DTK*Wrn)8K{LyXRoBF<#+|dqr{S@Z`uHRNd{zTsPDxp z^;0D!e&wB%e(bPB*S9-pXSYjeyBc;^zd?d$(VM4nT6Ey6pe1`!;sLwgRDl8^baCW` zU{LsrsAl_j6Igq#5)~inxMbCrp4?#TLg`LjCot7hxF0|lxl*wdz;YAc`rSE4!=r`b z<*%tb@rq$ZE{nqO#mX1laFf?9AD}Hk2+3*3$6)~hf*2pps$b%l-#r!?Ie*%sgu=%w zEB=T$j1ShTp)D^|Hqog&HGY&plI*9eNJ8F^!hy5sRNtt_nU*8*Z`qL&ss|yAL1yA6 zkM$J&{t3ROr%A`|YK64Wmp?{eQ>)<}u-HKNpFa?nTb`C7ICCq|$yuG(5(Xu>QS866 zsR)cRc15^nTGR^UXG+E+rM>(jL}+;_`lWG?T%9Q>=BEi&Sf1^Ntv9>xIh#@pG}>xO zlcr>VtsUzI z_csdD?H>X}vr4Z950WPy;f~#Odeks#0XnBU{#m> z;C_!<`&wiPUc0+`ZxWj!`lg%dKxV>yEA}V1`6-c7AF7mjC_bI=SEd@ROjoY3a#KS9VCKCY`T<)9X11i&BvJ!`*I%G=$TmlB!sm@(#h?VJd;!I$Dz*Qqv3ekeC2t`Y z=sC>+F&32_IDTl@E>S#kFQOi+el0{&UzwO@XR+mJpYc#Ee827eKzE)DW5LV@!9yB_?KsOhoXAFdOf^VAberzxC{zB<)Fu&+2_?+y zU_CuJ=xV?{O(_e~^n*vUS4sPH=CwZC!S#CMZ1_(~cwY=Q7dT(3tg7)( zKK}f@BEJNqDq$WKMXuG@Nmz4}h*Eq`t)zL%79{FtLSp^npA6=aSHnPaBJ^z$lYg+8 zMxNNXtn{#6|uzuvkC*=ZImjQM zF?;gM7oqbbhQE{C+3))YJHrYLV^7}gxrhf=TnG4`+WDWf;DeGU~fK+k(@eLif{um%*BA`8gO6Sg> zZ~RXW)MvNO{Ko^m`1N;nf8qK+9{xqfz#s+>^?u4;G$7=^64wEY`%cVy zRAJRNLU=DQ6UYI#(%)Q64@)p1wu0tJEfn_=|Md9S|kkr`A@E_w3+>EM!X*?wPQ2_T1OX&5MzKvFqo7_ft&{tBGWMV9YfvTt^29 zuYj4)vFn5ouvA9VKXgm25`gcWmdLxMJ))D*Sf-C0xoKE#%%|&Sb}kgWzqn_befr9a z)nYbB-qZE5yxZaz43WLS52qaFqZ)gGk2Du)hG~mG*A8yq-#i>}9xX8x3B9U)#u6mQ z(c+sV43lyhcx*yYnp^}t*>Hin)HuqF&ybq#kdQ%eaJM@8Jf3INApzm6V=rUD$Db%2 zODela+pQeXv;=%Df!^a`+5}cXcA}xBy@+*!Kxsdx`>9R_RV`7>Fj42un9~ls&Sxf; z?BKMBYn4=iCmAeWpZ>9Ud+nD-jSV z9c$8b+Mz5F-VVI|gq>(n!EF)v8M(4J>=oMI2i9p9C=L_lM~~+f1!AxRxP;PE#)Ag& zfr5d(4!bwu4s@ZCMj83`Yq zI@yWRc7Y)W`o;l&=t9D!{SKYnfDb=bqNZ#`KhvH;aJIa55iRfp>^M_^G))W`ThdQ~ zZyT|u3C#<&W$cyFgTgU8B|sLVr%4NkAZPUpnxsp;NNJ%%q{^ z1UynCr6nIbSWh8Cij#UT(vb1h0(!o0qn}fTgeo3;eGfhES3l611UH5 zLAz`+lX%|B(C_{4b-5fHZ4x=`^GZ8d_WLKo6o!D%0@nBTo$V?M(C*m(6NKr->)lxp zP|8t@!YzDQ%u<$ioIC@`R>}7&wa|nN8-Wh>T~El&UNE$)(oUEEP4STqX8@XX=gI&0 z+>OHV^;XgD$CKq{2p<}UAE$E1htKn|yubU5=D-_*s*j7t9o+!}!;g0;JU3JSyY9xt zMl2x1ni0H9L6BzQSlA7e+to}xyyrzQl7)D*aJv?3Ler9>Ku+l5n|sj?4xig8-&~B@ zlrm8XH=wQmP{0pyK>KP^;1XDkXk|k$FhFnC_sW2)7!GuG40}Y2R7^I$-_JFHUO*hW zZU&5B+kY;|pSlVteIUK2wBLRt^Ij*%u%8omH&Y1))}`6_jFmq^t@a$Zu>Y>Rd2noA zq33BgrbBOO`DC5Djt0WQ-EEF<+off8&bB21IH$$zUC$i~!C9HaBel6L9X3@ov(KR$ zdNRoely%%quF@=y$pz3C!uH3f>bxe^#)n_gspx;&j zK;ttq2hF{^LiQsjImx&t0dA6JI?VYv6US__l#F{4U|}$`f#bx#>&I$yz1w{%L=5Zx z{zA%)B9;rutLdTZs?)#1LOf=zkMNnF26i5-pW6$d?;AYf;($=i_|RhffL_VKuHaIG z1YU^2{#W(ywY#PlCK9PsAK{tN1mL-;Gp`z-txsfRBBx5nwcXai@G_`BQfN0ik@5Qx zK?=r_A18SKIxc`=`wj;%t9w*J=PunACn__-b0wV|{-&A?!K5tXLGhCR%SLUGwoc&v z21s6V@Ok_&u69G(t5#cJd;{lu;Zku93SKUMtvy|=K3>yjIj>?Khg9VDR5v-j;RH|k zhphwJp~vS?^N<{Mk*(jYO?Aqr8E@6QdgkX5o(+HXygxQ!r8FOV`_hymz+lb!vZ#b0RJP=(niB6pkT)GuR{Z`#EXm+cq;bx2(hLcc~7#5@;Hd ze!G?auHF#L-FRnSYKG3cOlhJ|qa2x47Io9+T4}P0MyaVWddy#tC1%6gdx2haxg-6r z(vZukUQtAcuKDu`BBMjw^5<`d`c78k;WX(aY-!N z!>7-8>TJbHuR1zeBo*T0qXSbFKo|b@JCQDVnS-7LR#q7UdHQaO9nvmnNIb1G* z;aFXCdAk~wytukjEDxl+)HcOvVX#jGe2+(TxExLJ zbnj?td<+=r9~K!Nbh4$%M$EOTg}f!nBhzQn3lnD0s@>lL!hw>X;QIvB3t24UAsLC| z6|xpD=Nxo}dAy&^*q@RT*|pq7eWlci{!f+=pw<(Ca$QSGrRKnTn?jmUHIJ&UF{Gwx za=TqAul;j`r#aoDVcHZ38&Q{3SquFz0J~jRp>#baFSM*XQ>)%gK{^1LpQ#vTIOH-d zI?s4atFDnrlWXSk-R^}rPy=$?b#LXQX4tA@*VNU|-KwC-~_OQPV%&F_}&X8~caX zl8d(eg9)I$-#xDqz%8PMb_98Xb#-9p9%sltQ6$=R`V4|V2RKprZ{K)Gyb|9m zYd!8_a^2cp*g;0g$xeC6U2Wi~J(dM}IKBgzBD?)3NK}*5q0U_CI3Tl)Hk|q=@N%IY zTaePW=VD8~13>4%y!w<7%Mm93NZ`S(bREwNA&YIT`?tP~yjR2gPyQ^pD^e-Hu#{q8 zFmIyzvBFA`X9n2bsg&8nBF~;=h@pd!0MPc>(7#%9%z(4A(oL6)E>oMD1i@(!UD>>= zq^z`jP4+mZ@6&iPpLVr6g;E8}bj4NY0ek#+Tw~?zb?+~1y8958+EjjM3g;by0;VxA zqD^X?@cd*a-fA^qh*)ZyGMvA0Y1`BT)gV4X__;q)yQNrajpYO`64aXI_lg{M zZdbc^zdT0Zn8Ckh+alKnsi3D>fn$$Ojb&7NJEQ8=^Xq)1@_bI!t$^lvfSswwiU*Bi z{cCR@+@(mL8F3UhHLO}&*b!4-)Sz1HTo_ts_BFK1#@AD5DTP6GB;SXOMOrMz#f^*l zmo-f&6I;9xUXu!N9V)$DBmbUno2@wPXLF8KCTPl+;yN6P=h4P`wwtbB25FsAq-j0H zLaipEwyHMEC!8CP9`12EO}s@d2G>_w57mwfb6bjsq%6ZpOdTeP$dI)scQu1mq2Hag zzYy84EfTRkGwQT6$^`0OJuZNPs>yaLPmA(tmsE5`XyOawLg`pD?|<#hfiiS^W9~Nd z!n?+a)iVddxz^wK&1WioMzhB!dgE&PppYh9edtjlF??Bu@cU<1O(8mZZoVb@KV^|w zl?dX*{oA1iQ6dNzs5EjsBkxd--(`&>??+BU6S;%{R+qflTwX;Y+H(cUg2Q%v-V6a1 z!aBY-10M&uSl?qu1O9DQVN)RqD8cFyZY)jP>3Kf zR##D7TaO3;M(z7=MxELCwsS|rKuRacQo#6dzZ86$IQ)HOT~?P-lT{ttrKFT0k$z%h zCK&qTNdLQ`^_N6;`%Pmta>M`2xKqWVYRhRGxB4K@U+T_-I>j@aaWGs;uo<6Wd#YRT z;?SIo(;;iA}(SZ-#TY?T0umJ`!T=0klWxEAr2?$e}%r}ze!HKU+&klcMc-qH@ss3T9;x0xBHx^>_mJ>+B# z>+;>%v2W>_83pZa6M3L0V)%C~SmhbJ&5V_~s`9%R_MJ3EG%0q18NuNa!fpu&dSBTM zE`{2~s0680Tyz|Yb}xQLzv2`Kw9os`@VUe8%-x{s>uXr|4q*bwC#TEK`A1+Z1eMb-4Of5^(lQ5{SB@izB}pwL<35d~|HZdHK?aQ-)k z;ns84S$5RvQq&j>jEKD1koF0(IcfY6h_ATIR`+om1rU>(mOJG$mdJ8+;AhB`S8#!< zFM^lzn*afeY_%1w=O_HiANwjdd-pXoJruCe1wdl58^;j;)B88}Ofycuwk{2qrMJ`D zCrSj#=~lDDeHRa0cdxQ#P<@n{Bhbm~$@IBX<)xV6y=9%{At!Hr_X2|Dir>@q;?xkp z;1$|jI}r~BnJ3qn#ML(!?~YZNdQ2^u5iny1Jzk>0&dmU7&=v`=?J<0(+xwEv$(bss zWKd=B0PT$agf>EX`ZlU zXM5nA-I!(4$pYEA(u!x_0va+Fpi5vku&yNDjvFGQxhF0$bToh>gc!AfKEWy1X)bNg z>SUd7d84@3i z2%fK1$f*DxOL?R9cx7W0=^^WGG-ex~=dDW5I=Pt?q?20$lb`}V;Uzu+6>R$r@IStL zA~#?bgqe%OqI-efS6FsVM##C8c}q#42%Zbs_lE0Y`;Ds5X$nJ26VjM8mDiq3-J-y9 zd~XJNxl|093_i%cO1m5@5W`7@eTQ8lMafg&xD^pCzRXBoj7r(KpE|;S4*9qqslF>{ zSoI1(VgQVUR4*;;zyyFy(#r@#<-l6?@tlw(;$Fs%Q$sQS#M| zd*`s_vH2c4Q89VEb2WsfxC^}n>L}bPVu8p6g0A)kcxuM4!!J$3h4zX7uY?-4*j+yv ztwzhJ2kPoP%86#FKdlBjr#2sV93zmYg{jb<({%^!Tyio&vos2!%i!Ac4N6Ta@`DX? zs;Rp9X`oTRtnyHa*^*Ky+eVpr|E0t3Ja?JXq@kfVSOsuS$DRc?Ev0j!Q6|9I0{}9O z;wJ#i1bBD17eBmuMs{^P0{!?l-SBXf2D)9gZt-z>IVP-Gy_KGaYO{94a%kE`dUHi7 zntArcxJ&=GK5hcn38#+P)Ep}>b3c!ux;y)dD4z#BDJ|FkNgCeZe zsS?&e97_a=b<}mrQ6vVWwGCk#lGLJEuVORHJ%L~H%nzVP+Z{OYQOku z@$Emk*IoLIBR5#JTx53)nY1-c{A%H@ev%u4yRR3U+2UA2_cf+I8)87uzTvpf#4bjF zd%i&)=cy-u{5vpc=Ce`^ToF#e58YF8suNZ2*HCI2DC*?L_EKD;Xk_tEh*3Na)QJ~) zuFhdxY-!rJ%^NTB@4oheRcb~W{~Pn|wQsM_62e9Qh5a&{SOk*wMaekWzTJYQR4rwtw0R<#k0Fi5i1=Y=xgG z4N)Nf@rtnRHB8jyXFercTIma;@TWT(Tpi=qrc+g;>MW`{>wBgjD~v;;EeNaO-t@+k zL&ONpxw8wm5_nUl5qvS(Hg~7S;~`s@%-<6G4dn_)in*CpA>Luq@-C*%BQh>?Q(V&0 z60dHo$-`Nn3aqK}=9eKuEpE-Tb5V1liU~6W2V-%6y!_~1 zqg&;!_sua^2de6g-9Mq4fYN4U+B?k+wrC&|;#AbC{FG&q07!eqV&qFUs~odf%>)!SSqt)JO3T3Nwsa{9Hg>Uh$nm%%qrN1C${o#G}yM(EGfP{e=H z4>P3&t@jIx6PdMY6?Vx3GaDlesSDTR@u_^(szq8Y%H&syEUqN0&v(r<4-X;cg#7e0 zOF3o<*Zz|>7ga9(H~}o7Oj&vI)QeE){E!#dQc7oIe{a{J^Si3gC+1iQu>Pbvv}YgF z?P1y%4gH@)9eEojD&~D{umz?~LBp$2F`wE60e?}vm7OIFqQs>s1*Hk3+k`r00&UCha|H$gS1_; zJy#H{FcJnJ_Me=_?1o;Zsf^{Sd2;JU_It%E(wBe1C1Yzu_k3{nqFa1^CpN|X=44Z_ z8vOhG0gz}yo$*`&KnUcrDVodi(#E*2#*hoz=KV-bYPopW81!tCi)F$RXuI}sI9?o} zf&38#MTSx!jDyfiu@w2SJ-p84YiO;5-PXH8MVQpbIrKJ&#{W0Cp(#ffG=I&an@Yz5 zIa~IR|G~kQeooYdaiU%Bp_FJf{5J7oo`d+2J(Edjl-a%1n}PNa8#31B{iS(J~!MBr8*ZTGUPcR z0k1ysrQkm;Mb2Gicv8GDvU$@g6_;*VVNpzlT8Z^67kSw}h3?hm+ga>>b#g*p_R=3X z%Cww*9q6V&gk2&e2Cry^g{5HKQLe0M7AYVuT_5ApL-mXE)?djEW|cj^ORk&hdHGEy ziM>5MnFE?f#tJkqa9mp6cBL#H*G6?{lrxvDI{N`JYVz?997|1~HfH{9X$&sUK}mS| zl~lD+GaaARJQEp*U5KTt?TKJDVzyn2s+4KlY^T<9BCqaPwfZUjIFO4gs~Lml8j5%$ zQsB^jfvXs`o*sbVc-QS*%^Y4BjIdzd5B_f2J!ZIFpEE!Ct2m*6Qu?5kG8g$-MWe(L z8k_>0h=6kZF|%<9P@GdcPujuUZEae7=Z@jI>;Hv+5wBO}tArY{jKtqSq!AuKej#Hh z(<@O^+zJ)6MNJf`J$U;KBt|4S^^Ld9zg{7ra21IS>ETRA+w1EJTwMuRi(&ms;zP;I zzXTY4(d$hdy5OplQlO5Ybb7u5;6UWMTRQ?dch}U*O>{cGdVAIa4fN`}i6E;S#s%QW z%H--?kMvigw-IZjjs^BWWp~vH`8zB;4}BlGco1;n!Xl#GK({`*zuvGM>d{~}ZjKMJ z+*0~jIMyAYKsa;ykWaVjV#%3!DxsU(Nu_sUZ@5;24rmQJW|uAgdv@PIsr_scO`3t2 z@!g5cA#+Q1gFBJc=P)wYd!NYHE6*yeHerAxxL*e0+4MInO$&{Xu-SOTz6 zIJXs>W0-3RZ6~{NAY)^usspu9zpxw@ygz9HksBtAzNblTgjHxDSp$BJBG+N&Fj(&L zUXHC>oL(r&iX&4ILn7S-;Gn_hqkVD}w57Op7#DZ4e_ic{Rqx5DUQ}j|8JZg4LEO7$ zolicO_OR@?ZleyVHi#IN%Ym9q5WIXxiVP?!x5Vjhw2q?(N))8xK+SFEBL-h4o66oO zA*K+(vb82OdP0#HH;cxaDGxX8Q!?cAlhPIlgBam^?Ni8JWQT-D( zQk+6F$7q%QVmsLz?R3Gu_tfr`e+$>rQ=@!qszz1ULR?@z0#rGMfqqp?HKl9TgJqsr znw92XSm@U2L=!G9pN?`0cD}o#6g}=?Vw_kFt?IuvW9yPXKS^LMJ#z&hTz|L$wjdee zM;_9gZ6i9$Nd-nY(X?({uG<8u>|4O89EGo`TnERs)!o-eRp|{I0_0R;^s07W42K_4 z@ppxhpEX)kA_ch}n@;Q%tU}qAV3VCup2ciSnXz#kGp@J=>W%hr`>hB3O2)3|@#6qi zK-W9$7_jRY!^Y!?g)=#Gr`<6Uj5$PeA$JAQnlInQF<|o zs`GQEt#)Ui-hzC%_C%Pk;ED-;xprWo=!^2#Fli6t-kPMaJfDkMJ_)#{#K&`WR%!zQ z+AVnW0fusYGe0V^P}{PWoT0m)$`f!~oQc8> zphOHfV8hin%P2-2hJ-=#!*JQ+c~6^eE-|jF2=y_~+272kIqG`^6##}nAGYF@?C>h4 zDu0FfXa5Ip6Z0r$>?8z0WS`Oefja5mCH`be))pcls9AbWkzV-=rOc2RHbcbNZVxhc*_V={6j++UvbrZ<}G zu&4PhD|>h~9rU@X72#<5G0Vbqpr-Ojhod2eU#K5&@79!P_s=HtPJr$3PlpreL_t%G zU@lF6?OD<)H9m3XbyxW1JEsBXqscFp&=p$XKwaVy>?|aTlJ7Vwf3#_?d`zsB@=AFa zvvv?@wC0LP_CX(XbbcjoCu@@MnX^qcPM5y^v{yVld4sfNhkk$&Ag}ap*SJ=vUj5Lp zM@DjvPZ+1Vn`4Sm3~xNi(}}O3j#iV`yB|0-aI=%CIfqVx6pcy~s9wvkAe5P|5J%G* zXvwBzldD1@Bj@+HF3;KO!lX8idC;ZJ7pB=zf2Uq|r>di*Nw`_Dg?@s*Id6+EIW5@j z)pd_@MIpk>DcRzf+5Wc_Q?h24ZZIoIDHj-@53uy*fT=2sFDAyio|zj?F6Az-{g}me z=+g@{uFT~&(xTY-atq)0#X-zjQ?vl;4u}DHf^2+&b%%02*zj?j71L%CyAi&yjK=+m z6bxRB<3$lR&HI=>uT`fC?4KVH^V!tx>Gmt z6BV;aolkDaZdaxI)rc4{zPrT%nQbx>;_0QEWfx@2oqKVaD4)D&%XKh9!`gL~WL2JJE(&;DFbS|RY&)3HtP%-|HZlIlRiYry|y>z-t zwx&<-UWZ-21M2Sc!PVBq6ugJI0!0_jb8Q^@%AXI{1O~9|j;MH=WRp%?0Z<=-@I<#Z zYp1$wrkpBk=7P?eInLD1O zw1iPzOS^P>AJ&$;ZXy$FV&($>BKO6~fH?7FP1<}a zL9o5p93$Jj%2ysK^sZAl(R%I^<4#l-V?ZFl;qXVZd0Mi|0PJ6nfen09En?lfAnx8R zoEzOKcwsk0SNY&)Kj1(zr|i?hy-RB08`S)>u2&y}6F6hRn>TS`ZxS7nkw;)x_g7Q1 zwe)UZCoA4Ocds*eHM$>dj+`i@LSCHUp^hAOW{r`-)hMn9-ENDWu|EBt1t+sh)Adum z<+Gc5j|oZTesvKPvtixY5%aj3WhGuxSm)R6n?Vm3gXR2(Ng|$cGIf;$M{22uXV*sV|GBZ~Twasc3ox4q zKFyD`ldb0+y^;2S1<|8(lc-B&ho&U|_`#jX>Kt?RT;29eoX=yCcyrA5eX^qhMfJ*< ztNZMV?2L=+V}Di`^buVOk7D+*r_kI*)b;ou#chyDAUg~Pn!TnnzO%8~ZuYM~SpSXZ zBniz0{) zO^k6;k_>a-j%Os9KmMW6bZQWk{n6CPjlIQ=n$2RSI{7GNxSjXTPLhcfqC)11a3qa+Frk^ZsJIQhp$su3|JR)Y`k6M_7$2Ynf{;YJdM%H}N-T z!A%%z=(wrPj{~doXE|@<#z<^^O`A8kB$j!%qT-5p?s=Br4oCT|-couHE9e-u)6dn{ zZLGkHfo%SyuN9@wzD{Aw@(a5sJDUPQ;Q3Cn zo=?nO={s?fwr(h?wG-L+JSG9m1fM2fD78XXUxFwZopf_`G;f z*D(kF6Id|bXVTT?@G#UGi?{Gf*N06mxU<)15q#ax?~)G5c&5wCkprz)x54n{2-9guA@9y6=CQ>RHiJ?TArgmk?{{c@KZN}t%UL7Z#Rf5H82@??m4o7rThj(1F+ zZ^uK5DdRS16yhv7^OR#-U_h#r3z_#O23~RaxF}kTBvqP~i2Yu;oJt}{Sq>ALF`SiG zwpyFvaI_!=nW>BBGCBJk*_W4-#v6zI9X{mfA*6Y{??%-)^oA*m~ zucsZ1j1_s42!&X`e9-kLF&J=;f_Ys3!p+YP1jOw34>9XH_y``HPbT@pQa6~MR`qfX3MzDHo!J69 z-NF;APbTxgRP73)~miU%!>@G=ME zK&S2XA!v-gy+peX8N%xz7Yd5E1LCbBmyUP}rAFVqY(*t%pS{y0sbN1Z=NyUww$Mrf zS*kYUC-nd%RS<*Jgf{`O^|{YCD3YFOs|$na%JX>kK+5c!=E;Rk`-Jh%c;NI41o_&u z3?j)k%fPjt_(v*cZ& zxtdk{j9}Go5%L@L5(}yWI>IyvmuzPpfy_rfzj4*ppWpOVH1Um{G`{N;ob`@nR*#Ch zDh}t6o4P8C<`pK?*L}oGhJVm{`RK?U%r!Qq~Sc+{X(}i z;dd>FNn)|Xenl|l4&?DGptaPR9>q4FsNGOUV7Ow(J~j=4SmseDRLFfpp~Pk4~p}-Lz|^GepmB z-&*y5#QXd*e|j5P;NE12I=zMJ`R%xIS-h;j2kAgffrUXZV#)wkm+aQr%nY(8x__Uj zGqryOI5Yl^RFGjKT{cJb0~RkI!&MI#-9kKVk}l|V=Xpxgck$ftv>-E{BB*3gpRbf*}X5`9-7|;84hG5yk!R;vRu|)|!fTF2;Wp0%h=GX! z0dp6MPgHX6SfQPe0@3#>0JPzxG1qwXZclLshvL(Cq8!eH-owsbOy)t%cVQ!Ry?o!k z?p56j77hhIT1@kbc=Ohvp~D?waE_Vg+VP2JMoEvG?2=znYgY08s%9#o4eiU_-|)p{ zlbwTQlWKY*Kph!dECM9>g+->_7I|=+mEj$eqba1@oq%U&ek*n-+n$Qn>daJR82kZj z5&|FIG%>Za4Djt}t@Nu4M`+9QaFrRaJ}3(C(U;3-*1v{FD{W(aPEUV&4 z%JS24SH1$d5`$C;U8-*d?gWYxf?d#K{3ildO0Pr8b!b|RGZo)i-S_i_ z8()3?__WKkH$7t5{eE=PQ<=6czAyplNqR2pHZDsnHK>pbtR|#e%T}2g3{+8^o2> zdiXV!=gWQyCqMIiU$HiYuXb9u7|h3zpEb)bJLb-MeGakgWoo6`x`g1-q3#pH&o6M>(7a3sP{*?rU^jB%xAapEu`_@znEtbuL~UVt|=^|Tz0 zo#~-L-Y|Fz?5~-P+BL|%9;RV6z0Kn=3d1&=P-zy+j)osK=>Z)nKf!Ar*5yBL%Dx=b z(AYc1A72mDBBih8|6zK0-BT8$?MV5$x3d*#%>9>pVho=qlf?YGak7o1Xfs1M`Nl-j zEf3{_7?YLKu%~$I>)$If4l}Sbhk5I#cL+&4bWzumWRgUAV2E{8O1${!fmiCi3x3t| z&hu+A8ULtEDPIo56Y8LPgoIEk6ORrLfA8+rf;Upp)#^|c@+4FLr9rE=-{tdTdm^Kv z@WI{Z%8$9Y19#|e`D@mLnG1uiQZ^LMj__}9@+8t5qXfvAw?jCFiUUEXxIM0HIx;bFru;a{w*_%Z z?&uTDfBY5@3EGWf;$9!_uS>5*-L=^$h}C(~kHJlz`L!mj?&>n%?s&S|Zg>9MA$;yq zE=<|0gWv&?95D-6*9&2~?ifZc(5t}uZ5*13qSO#xQ3*X<3imYDHA3WNu3ene!_3D$ zU?UloN(_A=uPh7{ieKTDg=ej&VM7)rWQe6^VggEd%%+v(UJm^huvsQO#w@>W$#P1; zGq@1fZ_)n+Gg!-ZM|~?@vzNe8L_rO6Q4)DCX|$O>;hD1< zTq1P-M&9m8rln}#oZ(Bqq?9nsTTF9~TyfLy9vK}LV{vIs#fi}%&P^&u9t2!(x(NDY z5o}zTof6k7XrwpGFVwZ(m5!XtT~T&^hsxy~xs;CLSO}hXe`}SrwbZraN@%s=^0Jq} zb?*V{)?lmF=la{fPBBB6tpG6dyeUJD1s!*7+aii$_kfr zJI_8Rym_mE4zSqb0idykIzTUEz6XFrKty1xeUG8{?yWh@a6rG75v!fKqQ40X@pacD zP&C~yk;qCr|>!ed+HV{3nS6Vdr6B0?Q~@xQiM2b zQd)ivvR{!U5`^3X7ret$=a-k_V8!<6jYl~>E(pX2mjG+hzGCOrpco-jTCh0{_!;s0 zLtT6xc_qWD0k2yz&cSZ5%hiNuQK-k>V(QD3O+EbN`fMA`pO3rq>wVlKRD1UMhQrOz zKw-BFu~6~UO|9P39iqzeK1e_jRNh5Z27ySHPjC3`nnCVYSWdwkFuNBT?8O7}A6TW( z2&lAQ8fiSnB|oxI3U=q0w2OaG>aD^Txp3J+b=Q|s&Bd6|n5X+{iBV=9R&#f(^1XIVQ^_+?5T zYbKvZ45nR&2<=bU0?*}CE>Hgempr3T(<^b84mJokZ1&^W8^h0avGipe zi!C;Jm_X8-eLzZ?o8hHa#pC@Q?DF72=mP~eUF^*Lm&$~}toZkFYv#1P$^)>E7nzR% zNmdGb5(0}xRCz#r;x)xY3n4keg&Mt4e%W~kNgR*rSHJJ(^D=p=uPgvG@nNo2izJ>R ziQzo%p;q}{ra~zIbiA;3GRmfJ96_cYfNfUb4ooRFMDN>LQ^Yl?>kO6rp%gnx_L_7Q zn_U<7*Sx)6!Jqe%>!zPbx(B=WJg$~EqX9$!27y{Y+Jv?Qd>uGT%<`LKx_CZqXw&d z!y?1u5i4@FTZ`iJ%xT`WPQk#y^*{r2XpR1dD~iQs?bu$K0%$CS;|?E5_hbEQZIucS z`}!SpgV(MXye-s&J}ea)-a52pW%@rfocjT|2B$K@FYEJ7`0D^rP6HiauVN0RB?w1r z%2SeE)uAtnY}K${A3+-8$zw>G6}Yz6#`nmd9kcnYUB5<4#~rbch5aB%FiIrfFYeZazNx%8ruq#nc>SmLweZNE2{FZDM(I?d?YWPs#^)x0i%pW&j ze|ZB9M_o6a#it!S5XCa&FjQ(BV7G8|Q!UN*rr6KpVF8YIp>k|#7xEQt%H`{&7F?n% z8F3VOA`P)1APOf^LNq>*ZRW8l3nz6LQMbp~M#I&A|KnsbIh_QX}QnnTj z)W1ey4C+y(ixJ-<%?kIfB7dqK9)S7kyuq3>g`j)zwFEV$F)#K69{EmMV0VDVdSZ}9 z_N>+$=i0MRZ<8dy+#bA-57HqVe;aQ%eVKp_S$**q1GRXzL~QWgeGAjaSnK%>;#(}t z;oOSLLr$45GQiY~Z_Re+1v`*k>6dENCM$f+&px&hJ&~Jf5AbLd6dAndHd9tsm{QRs z`2X?s)=^QnUH3TQNQpEpIst`|Q2X`E1_EO05~kLBHmkG3ma=&wnSFR8^<` zLc8{^mIBv@pt&jC=E5kQ^{`J$J4ZaoSl(81VSj8n&RZRO3J^b#7>MC980YGsf5O$0 zr8Q^tFeCHw$Z7;=xOi>=fn)itmyqJyp?i`Jv|GJL4;5Ccx>wXNq5s^T z5*aNl=U#d9h#r6`3lg}Bd_i>mf2Rh3yD@N9>}Lv;VhJpEBWc_=X8@6y(wXhxaX)`(58<8~p}oU-hcyQ)pS6y@sEDnK z@}mWxL$34O2I#IYZcgO2roHF1)QV!Qr>jVOh78W-`DDIJl1SN;x5=?Cda5XsQpU5e z?-GGAx14Xg+Gl7jjt0BglisYI~ylI+d~*BK%ZA`f5?+t^~swepEGX``tLA}*tdB0pS9{CciW&$ zmj8LDP2|>m?6fmvKKNMt1_fh%7V>fp^5yf1Au;t&Z z(XfeUWjUm9VSj;k&USv>gIsWGhV{~;#T0&KXz%CCSkAl^a0AyHLhEnygY+9kGw=_q zL_9L86d=+{C5|q}@;uCUtiLyQHvPuuyiBm@RqI%G5S@_DdBKY8lRBQ_?qK>#6)Lv> z>W!4gUGJvoZrII-OYM3ZLJ|A!-fw0kb_LlISgk)uxQZcR^1h2RaDh3mEX*AJs^`{5 zo_;r@W0ZFu^4Imd(E2dRxq}lV`3KUAi*wVzs!@}bTW0T|L^}lEV+*2{hEElh`ZhqML%xS)|;qU zsR~Qf^*2RP-CQ1PlGi}IIHfxj@A-Z&SKzUG(9CZrO)rulDlZ@Gqfa|kvh$hg`He3l zV;!LHG$zf!(qLegsdCOy!V~KoU!yZI^3^}rV0D4xXtcu+*|ASXqx+g6b zMm1d|DuWZ)^If!0IwxubL|qa~Okqp3sWOL*%pRKH_#~bp%yJ2*_+W+II&7QsFx&*1H742O1>*J?6F81*vuh%;C?LifB`V_8^G7>BAH z8hNq$w~xi@4kjK|pR7Hid*7L8+iQPd5xNXWEY^R-=oH;j*%fzEXF1*OP2!&wwarWWEi*|L#`%SSA*KT8HrEzt!hp@fkoS*#KD78 zqNB2*RgGpooUP}vI`g82=o3c1dm<& zNf+_fq^Ha|N-4D5qTemQ{@{8$rKj#8am(|^!}GGoc9U==_4w~Pd%v#R1vCXc&ULU5 zC_4XXCGEKsq$jO+G%LI+HV%_o8ZX&0xJ?ipA>4_DQE)~pJ#wGb8f`Og%=kVaFto-W zY+{#YjF1%#hY(2m{g%-Gf1kh6wCrJ2R+;30RZy6$Z|zE!JT&m<(Q_M@V|9P*sm?ct zSeXD;&@wj|grnn#;de-Ot^^M##78Bor0pPZglrXrV3p*C|_Z6=dR# zQu-RqFB)j1YZyn~jaf1}bv!~wlb$hLGnKR_{u*qNm+GsHfz{u(^XzR{9SdeZCT)Oqkzxc#*2&n-Q|oI z=1SuU$X~4g4u0@jt`6Sh7FRz>P5e*!v7B`Tq1DGnQFpge=T6S6Q{?Dj+G^&k~< z!^|1UBh;k|)I(~zH_mg)jzbh6b|cgY5WflZd-0QjR4SuFr|r zgN%!Cf8!5-dGG5Lx>nP+6NAoK`#9#z<7Eyz+))Jg`zFMi+pg|L?#9zZhTnZrBHWD;>8c{((GHRoXA~%jvbx13U}Tf(jJ%+m(;B3a=qmZc@Zj zd_nf=sIU85Xe~JK71_wPXnjL)nCffPOPw?YQ*np!#=!@oWei!bK~fOTInP$>I9 z<9o7YO$0vWlB>5XPfD3Qeq{hnBmcbx`#uhyu5+CTTk~i{6SZIj3}fr<%$PD49WU}F z1sjjE5hAc2U*@C*50{{*spctYI1)Nu5=!4D18MhviNYCC5&SER1w-Hj#7BKx_~!Gv zi*wK0s8ZEEu{)3RyQ=NHF(2AW(k18mKWHjBcNemGMd9#bGsQ7!c4GK(9vS6;&!&Ep zpc;bD%LUWBV0W1E(FZZ9zT{%pcS+s9mWvk34_taV=RbuP2REI5Mr7!w_D&N<#WA%K z`(W@(u~E=HN=0@%KkVXFeUriaJ$d9+PgfKaJs|1jg@w%|+Cl62T}+|)USqwsyZMrz z$_-wbQ6*l@yDI0*$&~fZLt^@4(k8bW_>)L(bKEik&|fER;b3vEIr>5bVJf7B8EB-Z z!0h|R$4LrAXu7?@y`k9*6^55`Y6L=P+$W$V#Ap%}^@W@d-x|Edcu9 zUImdVfd2msx>zVhKmJ6|H6%gS%GH<6dyedF*F;K175bJ>0(v(;=OIzHiClc1&IS0- z+k%yGM4xEgq8b**cji*m2jM^I<&lauhI@<1wH7q@oEOasY6 zaEBkDPHv8R1rfs88eX3xtH|n1@2>Dr^0cQA+HeHt4ZYGw{&J#}s6y~$7Zsm9nNxsNbsP!=En``)wM9OIo!N6L#`tnc zDNU5vYDz(w2i6AiLy@`~Nu=b0oNvuMcCduB*u3?$o(h|hHOZSZD#bsgG%Ffwx@F=+ znz(2XOB0RQr=*x^QbO&6%3S<-mB5C;%bDQ=qDj}ivQ3D+;4t{}=#k3p@6yKiLH}eN ze!G!1Eh=2qyNmMlsg3rHl$f-N;wWEo4oQYMatl>Sk-w(`sJ`)vVq%i}s#w9c+fPJN z-ordj+FEEM$Hi&UCOLr+Ge z(5hq~A+2|uQ2o=w_*uFCSwE~ivuI<%e-QAPZqL%sP!*EW(>#!{zflrzQNA2pZzN}q z9}B0Oxq-WxBk+mlAE=aMeLnM2`;`XwG<6>6SargX zDd<n%F)Wrz$Bn6x zaHmUA!uYa81J%BsA3FElYHhP@MV0FeDS7K&H9j&lA&;JDQ z&rk8y$QK9q=4kko*=!>1CIVD7Zl@b$^`d=r@CA7P(hJqENGS@?1AL;%sja`1`mU!% z+N3=If685Ei6=y^9Dh-D-}}M1(pyvugoVooL|gvB5Hj3btoimU;fxoG`sEiK8+*5b z-`9yls`PmnB^@@J?Tyv&7agZzV9^U4IiPK=hzQw|bErQ*H(h33k(gG!Gy%#)7uy>b zc~x0+$obhw>vE~R%!qaF^qcaQ)5shd*QoNZHymT#uBh_=Vg264xd{SP z$k5|<>s#GjE`_vaBtctxmHQ)34+a|-j>Z>;|7b}KNZ_Ear%t5(>8;iNt^(1|)S%2~ z9%tokA7SB2TEARFZPH&6sT2jfAa1}CdqjH;_N}j?=NBb1`Wa&=ZWI$sHwqWBPWDL_|4qpn#=xy#hGoY)5FCr+lfxJcWa>`#C>m( z(bmBN`y72vw6hhk2V@^%fs}F_wB?5)Nu~V1y^|Fwg=m<-s$7N*bHppaikp!MiVu7Z z1{tAUih+u}%eE^ut}Eh}R0&<(a3j@2Dyxlm^HJyjtT-(iPm>S)2xPEJ<@okLX(;mN7bdPoqc< zMF>z`RsVGjM6&Om+l;|`gdWF8rmf6KN<>e?pYIZI7Z*7tUj~?6SiE&wEdV1JYCttP(~u|cJ)}o_ z)3O+}9RK!v+`2c#|tH*EjZhR>MZG6dGJ>`ic z;r@M5mWG*0S)hdI@*B)(p?)lL&z4l`it5VO)1T-ZyhuUUV^tGU@)nI+t9Y6y3b1GB zXUe(<@v@A}aRk18j!^xKVWw+@nMH&g!w-+2{ZB43aF@le zTz+>=gSR_`UV;U-k0reG1O>VGw0z1$ov_hv$-xg?5j&SzSU2)s0JEI)gYrISqjty@ zw|)ov-6C#GQ~}W0bnDArcIZetEhGv4>vj}5xy+D8ceFa{KmO!g$lJPWq&42710-xr zVs+x34?`l)dmchz{%YmThtikbXI(AdzDx9$^l0SzWJ({;*q?6a(hJ4=3#911N%hnuXyx5-JKR9S~U;B}} z+#NgpVJ2h`+|R$*8=xdn<%hB>x;>czL_qza@gT|*AD2jEzv(#;k@T*>Eb!q#A;j1P ze$5teF~@VO6!-G-yo200EtN;&U;RGELxUPfmYp=*#{{egS)jS z^;ItN?wAJiucl4!A^1F0gmdes=!5u)F26_BzNpWxh^SbIR1`}&^uMIuD80Qbv7M&t z^!_C}ZvWR~pc$~GyrIc?%SgrB@cO-lTT-3p`pqxGHl=T*4%Uw>CJcc@Idc-+FMS2K z25=D8LO?~F^Ga(zN`KWZB&Ov41xP4{;PDw~wf9#bZcxIBuNxjY6ME1;n}epjvhxI` z1=g^Qzx>&m#`|M*s<749lF!*)(i$WbXHPK8fO`f~Hy`EBQ1EqZFxKo!+vHmI`m9bz zPA6ntIcW6nJDtfPh>J#2I1y<*c6>9c)X1?hS3*#cfv0IYjIboa&!m=Fb``ZI#2qmjX zQ(V5&$6ilkYW%CBwhkOc^hd@{0=Qmrya9Lx28p)d644tv9qhL4i!~j@2`-Ost#rI@ zO2VSOu_58GHg7=FDtfO{>;X>`$B~Q3Hq6GmIu3HP`g`h{Yv%W@cX<}jBgXyg)vqbl zJFA~`r-N)pJ&IFO)7yS{!?w;TCLq1%{0p5WK)-INY(o4v?c zl^E62KKnpE@{2YCsU4T=*hP(0jYK8F_6on??J}_9++i2#jm`?FG;n!=SmND%C9pG- zVmT~OG4+^AS})W5H#Uy~EKo|gWsQ~5C}W=^-tm?zD=jxyI(1Ea5RyQz)>4Q%**bK1 zplfyup9dLyI~s=J8PBbZ1fAFp#Sz=7r zWO5vfm$}G(JdR#z@geXDa9b}NT@E2C-}5GLBkjxT&R7l>Q~K!MB;j=-8^0mJ5iOCl zu}l`BOe2$1%H&~a=_WGOat;Mjbv4aKu)vKR)-tTP(y@ZOo^l+ucov*2>_HZ!IzKgf z9m}vj(%T8^@2%=k!e`nVKE6mjpwKd_ut!Ie3Xb)>YhzcIprWJ@vCO=;Dp46po|M3H zI&iDqT!1_`TV+iq>@)N9QiQkR!6xn%dRF&?$Lwc14&Fs|^XaRJZOLHN8PHDj9|GKM4UKw6TxFfrwm_BhkDq^fj_O#W=ZDH7V%+@ zgX}}=OxqFtqr>c9Q1QY52C2D8IAKm-@1QZ>_}jUaR9i>3iQ+x8(!L3NrngVme`6@I z8B%&WG}OEt=B_%H*2-z6u81h>6{zO@eBs?A54}GUZO9Ir;}+4noWZwcN)^3NTxra~ zt{uxeRqc8}Y4>c+^7iYeav@uxN?kfzZP(r#e0-(CO?y1ndJ)IC08Jeki;BH_rzS4d ziZ63K4(|rLEL^J3Hq3oJG?f>;?oSZN_} zqbL*go62iSdxV10jl)o7>H0#MqB>UFy&daLKZ00nZ{3Y=XY1aR*}%e+UeQR9xgNiohdVP!K}!lMU`MbS;kLw!z=~V?Hz2Ge>P^gofRe9=HuyVHockmLsSL3=ugvB<{>`-r~p33cSOqo?`#ch*=nl(Z5w*^PcJ^N^4WCD$9dBhi z7#vFqEh^)ikys`?&M|i%goG}XAL%?%Y+6BP(*^x8M1&TRL%Ugk$fh$)AwJNkJGGs9Kb*GYsmiYrA^LH*69ZK%! zr%o%g*_y+N`9(gPL1Cs58a@uw%%_W))0GchFPp+Xc<;?k*QF|WyJNx8r`D%)jF*{l zE#9d)65IInQob*`Zwqg4)p*<-o@%^gqrbG&uzxXUaq~&4XSYv825dDw&kE0u07)!B zXA{O6mPLvaIB^H|gZ6LQp&}b=q!}F{5rS_*q90=Avlu*l|4aM9Bid4qJmM8ZOcjwo zt3hbS&zI^HYkjvz2&uiKDe2y?U^&yvq7v@{nb5lWx$^7Dp6oXCYX^1CgQg*kr=@*1 zKg~$cA7kwJG*l_*RDcw(+weBI=a_iAe@4IE&G6(EcThag_QeiV9K-fa>Zg2O)KIjY zadUA8RW<1X?j1Ok$m{R^V$~^|tDmh{*0ux6n=JP4-lg0?O-)G;d78R#m$5UE>5Ci) z5qWC0@h$nJm%HMp3u%|g*#&!>L{lb@1K1KCp4?t#hr>n#{r;3!;GzxX4P8%}n%tK9 zoyL+!R%x)n-Xxl)cg#agdjpR&u%8E~@NOWoH0vu0q9|2cMZ{Zjo&{b)Rr)?R7E!&? z0whFC@yyLXQNv%-9&P@6ml&eJoE>T(I8A9rR!#yx+rh#usF zA+fGEUgVk68HP*~sK&fHIGX(HWcrV^0|>uCpLf4a&M$DGuKS-bTX>6q_Kfh$xwlVZ zqYmO_e9}h(&C7z7Q;TvDiTSz-3^(@Dc_$8sqI*p8!GdBP>mywAxR7PvZ(xTt+7SOG zyRE36I)Zj%?Bv>sDjA>uR^rs)P)_sU&d$OdLw+LaU;VMy4(^@xoua)sx+wf18nAHN&fsXW;+n~HivxV zmR$c1A#S0_^jXYJMyao*DA!%p;!+&4LNg7bhmR)YytXcGFHk1Crne3+mjRZ~QXb;< ze8VzyIXHz5v;FS23iMG~IsemS0x%i*z#Y?uAi^A&1iv=mw^`)YN8vv_!tUtwp)dzb z6FD8dZAMU`pgF*`3KV}0nSdv&247g&ZgNDtjifpH)_Cw~l%#rn?g0=wFe2E}p=M>< z+^$gq4&iQCC7t|yLAW6&J^Nd~1yv{RMYPjFE_aR_27R` z&#C~4@j922^D)L(qL8pE7?dMJ+Wy{UQh54_mVw@;0MUQ*O1AL`UEbxct9281vt8D$ zy*sMVi$Hl*J7+Aj#7ZS`57)imK~qf*)v_q9%Mb0kA?OmX$zlhvSdaC*GIPj%m@H=r zz0M&iq(^f^>-9`A5@u7{_8FH(Y=pZu<;yB0zx7(EqVhRT2|BnK^pkHVAI=Kc9`eqJ zPwBWPvQuVHb?z^ud2zr5-~)*~>O90xT^D-QqarlYwI2)2iXQH+WntYQBl3T-l`HD; zDlWI~^EBe9x!k7jN6)dFPaA1ZNe8>$v&BJv=BOBTWX~akS>R_l?+mN3*(dO>WwmkvWmUF~*BL zl?cCYyfa_Q8h`y6++PH~C__iRU%bad(4#T9`ygkq{&->({y6r4pBm~|Ef0~LndhRQw>6u^*c!|r zKd)Tckx6MM3&9-e2egc@fFCo*{}@I1xYm3r`86q7u8Q9w!|T!M@c|>I5lEBG_SVubXL(4i<3CL&{JgYD= zUmru)Lpb;rJT<#|Ojn^OExOcL59zLtOJ}xDyC%(D!Y*qT(01D>K z$#t|xJ|>-5Gto2ge2+;|{8!;1gO}Wx60ImP7-E@Cu0Xl`*dW6Qcf$2xd{#8>_^1hM zAoG{Yy%A~lP7)>J7Cw3C9w7T1V@N4xW|ZF>s@xy0gA#dj{!aOcfpoZ$VWZF7UD zoXkh=?I%oCYDasH#(kfPaq&U8sNS8}USWvyMDzX)iXA+^8s36+yl5TeuZL?5Tj*&2bqul}n4SB$_{rhY+g(OKljSt;t#9c7@QMM^JgxrF8n zF#M2b`#7c|zLAM;=?dY)0^(Tarj*9LQH*nZrvnb6I_~XJ7j?l;KATZTc856-l zF3#9`SGh7GA;VuY-OwG=xLicw#Txind_li=YdzMaQSadXH3W~OnBuBs$gspzx_u3# za5;F8iIVv@<-r?CHnNzBC>XK0cG}|bd!s;Piha%S`5yE^Qn$hBe3Fyxr5hjWK0(^{ zyqK^+EF|N{VF1k2)MQG&)yG*H3cfKuFkMm`n6>F=!z^_%zqfw^oz^LEXE^$5q}1CM3oX*K{X6>dqW^u{K@Y?&KDBoqX z%+lxEp*YOfX^vZ68*4ic@jTumDpX!u*;AC?seDHajFor{p%w8z2OkktVBvBHjpsaj zDWfw67=Bf8S)pyLNag4Sy0WkFBHJpmH?We?J~R%w6+buT-p?Q~!i!3Cq&8mw6r8Fo zVa~YLCLQtQ`v;;^gUdd+F0fexfh4zbDyb51jk02KIRsZ0XB*8c71s_7dMnXPSU5ax zd877SEMzeANciEwqRMxKaZu^ToJJdahf7&pZ8Xey4k;RzJ=ptg5xHeEr-p*`JkKT( zi%`DCnp;%Uu|!8Mhv*S6_P$}uzO^A?MG0g%sF3;#jiZP6Lf1#MH}h|ok;93u)j64p>siAxH)4oiQcCmUEsLBVpOgx94ids7i+ubL~h4kvP+#t9FQ!=D%a zP#(?Gt1afly;SNtbzMy!@bV2Mf3DsSYrrUQz3-URJ>>yAY6VJpJDn*99tOs9j0f_v zy3KHwgZIBIvztDT_B(bUfa--IU-xP8)`xpB%UoXZ-tAX6Mn$?}A?9Pc(!~$JdR&Wh zMvm_yq?hk)mderhA7b~2WOm8J6uR69)6zqA$1k?$+yT2|113g_v_obMl$leLOgrSh z67&q9q8=H|r&#gb$Qx8Q%oT=)N=$Cz=#^~_fihWl z3mCTn=UVJ)37Hr&x(59(@(OrCEu)Ju61zk=lJ{_+apW|qTR(CCaT%OkZHXk=*==!S zp9@_w;s~>d^m5~})(XF^7Q(z98#)&vCJD#nMF+m`SfL1K0S{VLsAR&-%dvc8{UHL!T>Ls+?@mx!mE$V@(E4Vl~*X!9y6jkC&Rkd+e3C)ht3I-6O!$lkuzKSjMi~jF!dn4I?yHia(G02JWiy>7=H}mf8DuY2o)Lo9*MCvh(e4nEOFd*DJk=>%c;{wCY{D zjJ3qZ;;S@pp!M*^nWJF2={~(9(AUh`iN^EL$`ZV^CX3pbX0RDL?0lBGEhee7k4M)W ze&0kA%#iyjByk>UCt3^&Ev@0B!bV)f!ca3`7<$+GL>zN7fE}0u6}CmPQWbapmF{U_ z%z)c%XL=2CxH0VtO9de~oLgLV%nppuNSODK^<8V!r_B50JcPcKhSdA%;L;;tchb#R zqzpFVR!b-XDA61W5DmkTbOn29s>0wFtedt6is(w9jv%A{SnsbUe8$JPxD51KW8qY0 zE>raETmx--=sGa3`>!#So<~Tm?&{*f;fm59izm2xqCE?@cdG7i(Ci()LNCXbuFu8& z&C()KusZ9+AO7odGHfY@C2>yj)2QyBpanFVKb%Lkal^0SRJk_J9)H=wBfnejSEne{ zKqBk{MM0eHYAY>(k)u1X@pl%Sxq%5nH;(5K0cT?u!@oKX$c%V!;nFDJ_kfAb8q=^o zUgEhuP!z&hMLyVrzAJQNjs=V_DNwVEZ6bekfWf82M14Q`;h7znN@?Frdw7@hhytXE z_(~Z68s5iQin!zdUDdJYKS^Wo?gXJ)K{|mdNfp3eFiX(@quPuI_lFz(@ zT5nV4QqA70x*1pZ;ZfnY3x{ZC58VBlcnR!G+@5;zU+fKSB#jO>)J5HjT(a-iC=p7u zQu0s?89B6|>*+0h(Mw0jQkb|Y$TFP5e71V+1KC%9rCS~034EV>F8+9Q>Bc@ee49b> zF0S^Q!cpsnM|W|LvJC;yw(me5fKk;Kg~A@!j5FQj(zu%yLeN*E zX8$H=GC|w!ENvmg!J?fqA(>b|-UI)4Z1_gui?Ux^Tx=$+D)ZO3e2cMP$c(QV=W_B^ zM`~kk^Is1n5>{7BB86Gxqic2cE=~os)F@SVSqWx2Ocv2lfdx<2Hlfq&VGp;W>9bzg z*Bo6t^b!HV-|(==YFl2Ui0a24{7u?&Fr-y<3sChXR%_)eTT_rt31I+a%GvL#mC26A z(ztIfSa$xwF%im8H}FC(kim@|hbTk$AT0Vs2Bp$z+Ps37x|C+gSLzeU7e`o`OwCyM zevceWkjPmU6KeP!Ew$1DVGWq3S7Ta`iSy-uXe(FGL|+SYYqjE&&EtczGdiy#ua8pw z-=#OBh~fQ^KaNpgv6)HP!tN_xvS~cGC7+#7ToxNv?0Z1~ZxJ`r1gWJW(mAViju%N# zMx?QKktSjLGr{qw7<9JFYz+qZ-}%6t4J*;t3Vvv`B8kt4N3nW zJJ}sQ<+v*_{w8?6BC-+m6P&~g!Q}qMI#$v5Y>9j$0JTya{Jqe!;K{h8bV%|K;a1J$(X3ZsH!xGuSK9C0X?_pzB!w zr)DS-X<_#465 zur(RlG}9ljEdLDpf2PTG3JRv`yE4CKY9O?ZT_)%{+W$Ihwezm z{XA471NOS_jvf_4`xWs;7=IEZP{7Slj74vK-)ovIi-n`LugV5Fd`A8djNCWCinlBx zu%|Rl+(S=0JtP(>U9dCjs=y#hIe9#V7J$|*I9UjK0PaK~lMSw`(!&FR5@>%oN%x`vVw_(5b|K8VG$J`wj~Imgxxgj*#vslE}m<4{*RxBJGcJE1mHko9zD*r zc|8uc`4sa4-0BM~_1uTtqxuimbYfrd90tX6H43I(*BodrmUQ^Ni6j`9xXpBB8_Itf zNm_(n&*E&D$-Y@>zJAvB_s>XZ1YW+D)ic>hx-=hUTEQ)unEpEWT^vJE~JuyZ!y z;`#0RtXEJb5Xq5jt7A;sJe8tP2Rw(s6b-@X>qy+RUn|YPl)UEd^7*^-bme`(L+1kF z|69}OnIWFDqn!JWGk66`y4%;lD~JSR(nl>r8bT9qC)t-?|FN8}@)(oe3%Hn_pM!F( zTSERB_jDu_b?G)fG^3WCxB64a%=Bmf`#1AwVNUnBmD)NI6MWYBwJta2Gsj{PgImHX6AH#go-+`PC=6HtxX>CA$vH zP&R{!o?HxkHEUijH#CXi7-ceG$BoWs^aQiX+gWl{muA^zh?nn(<$>qNl%F##mPO0K zH9G`^a}B~j=@%(q|MN#jz?5aWRvt(LwupM?S!T{(D-;-?*J>Ac0Q+x>S^jLkxf-b! z1|u|QElKQcfM>9~A6A01clbL2F^T){D+WJz@Lg~amZ$PEMI@m$AO1boaY(5XTtL$~ z6(LFTwNa#-R4ZTQ{l?|rd@@zhsZdv=hF+)8ai)c}-nvx0oaw`+ z+h$~%BycnArqNZe@4LZR)_Al7#PA`Rgz2bn@429)G&2?;L1AB2{#Ao@u=-=|&mi)Dv;(4C zTxK6_d=c#GO=8Nj0yzThuj29#U7-J-+7*=Xf zBOO91_C>ZZOmKa!R8$WAAv*yRHEb} zm4*%lPt*Y04>))@yElDJD%+RrknG{P`|ML6;ZAR=T*Uy*zUrXAR5O3 z(vT}ij18I$^gI!Hh4V6NEcb>~T+U1u9R*wk{EMH>D|)sR&u~^CvUnP1*r=JC@9H;; z$URutJU0uynjVY;6Zg6mH~-lyb|QoK2kr;ikiUk}a4+hL3m&>y($&OgzOG%yLK_0s z`Dvr8jD0SrOm3g>M??(P(v@lgoGIH<({03_9g9cM<(yws+=ZpW`e6G@87Ia@F+!61 zZwTB)K`z2Vk>oUuLxTgpet$azW9A0uf=DM~s2d**?+0%6toIca#MJG_X`#BYt zKM6CvUxIw^lvmcbymn5WO2^9gy5>Xfdtk+@P{$4~*)t<}^p4xjz7lB0 zabLF$|83TQq(<%>BjuvjmMqYF=!bON4R3ulGHV5%PzC~?eZ0D!9D>wi)vZ=s{KyD% zYk`Xz&l|+#lwZ*^f^^tD9mi7g;=$U%FN76H3(4fS{iS}cY%gt9M2E4h5=BH}<7?Ht>w;s3`U-nQ~fF}h^V zjJs&tZGM~%Y*eVobFNFcZnAP~I^kh_RKrrs+-m^KL~(i)9t?IesBmctPO*p?XObzT zF@dK~T%VZD&E-w%z<4_fmhNy(QDF#l za#jg_1u9sb3J8)|v7Pm9xm;i?4QJ(f7v$og=XaAZXZiPI<2dmhb23eGxHE4ZerdSx*+V2t5&<}C!!&pNkyveyZNQ)e0OiUHlP zq*nWg8Cq1!z5NMmdF^w3e@JNftJN2^IRCGhR@H6uYj!S(pnnA{@(g=0_f$lGTn@5t zPe}vyz0|8X&<5f-)snV(wy#M;XQFVVx|dyOc=E@UHgDT3u0@JE1(YFQaiO!2PsALPp%D4t}wua}jtdKqeULzsuvYGl>Ar6*ucc*u$c zt*75?{#xKkmgEl<$d=v{mO@YVxdT(oHxYhWp#R>+e|dhvH>0ouuBZLPG1tgH)+fUAJ7*1wKToymk^e=)q)lbQ~b0m6}YoY z@%Ei<-ley*^kB9$`b^kyY#;Z0!im8wapCm9^v!VG`C>bSPAb3Ydj9i`tMUOyc0A#I z{-$U3r`D34H8%9QMr2A(;(hW&%2Inwo5uAywtrQ@2w0ZlZQ&@W*Bd=vkBo9z0Pl@C z8`@m5^wPc{Gn|}@N6z>}y>i=#%c!vvCrvPA@#5A>z!hcRG5Sz)nWC{vRLghgGXn}@ z+x50qSF6X3H|s%<;dz^y^Rx?c+2V`FkXtq4&Rl|9PJEojD?wnA4FUE40Wuh;p85IH9f94eakK!k&0J$-#^}6zML8HWUU~)4~bVS-pkqC?yvuhpS zlurgcI;F&qvcPnP^wx;qg>&$F*Cd_rl;rQjc}to@Xh!3rgb2|uiyx}62SvxG^-qtQ zkIE{ScZWLI$cgFNx6~anTgTFF()p)=Z-X1z@=$)6?ySR}&c5x~H^9B9oc1dS@09UK zN_oOdx?Sr|8VsHnFMW~fsT>%t*j@QNUlNoEp{vbCen4Z--9_(S%@a$kEbhXK_&HDA zjqvZSEiYH}NdE#8sDP}kYhGr_7Fl(vmKsq5f^2rfF8uG&J13$eoa4%r!Llm_4N|3< zPkg32D)8{}W2b$#1?fYKOz#iN!ABDvY(cuMrKw>Od<3d^_@e zJEA8NOn{t|W@gG?(RA{@S$)tv1MJa7IuzTWHwu!3r>4tbTw##$`uOXZH2u3XU+18v zEv(#E3cLc5KKI7immam$=JB+KZ0u2<3@kGp-{=t@nFp-wDnKl^8n#2{oN0meATNyI zAk(?-FDU;%V4IF^8=5|vv!a-#5WmE)U!dFVn)P^3|1ts*Ddztij0_V-5 z$k>Y$Rc~EMrO#P1mjdwzJ5xbdL zQR8~1?=^xA5K^hrrfmmFV8>3DFZ~sAgwl0y8CZ`S6I6R_9CCq8ZG9`WDHBWhaf!OA zik;TJfG)|PM}eJI5aD*o>6AkkA!WfuMg~PdZg5ly`3*P*`>CG1qB(xwwyX>WWO`5s z#C>270zNQCbo?A#?r*`)oHhLE@;WTPkw08#(~_unr|SJoV_X!B%__pZ9lt6f{n|i& zlhu?b$P_I_4+~wV_xt@bfO%E#-7mRU^$Nq*Nl8>IW6@_=C1sT%Kgyqf zMOrv#iFe7}^4#;YjjQ=y0iXF0FN$h*et zdg_M-Mu!db$9oo(4}+)b)CY(jswuuai>pG#)zv2FmSE<3Pw0I8R^2GuGWdRdj?;NM zQvLPkG5MlHdOE1%2%;jJ>xZ7QBUHdLw?5c%oEC5S2C8d&-y;29sM77B69PuJ_Fj;% zUTjXD+y-c@&RQFrpZSZao`z|_LDDDs2Hyjv)6=y^U2UzNjz0fL!c0%p;PADjP9mr6 zTMP3gBAf=kL*}GO2`@Pj0ol*|_asi2^PRj|LOWiKQL+eix7^Mtn!Y#;+_9@c4zC)r zJ~IX*zV7~N*+)W8Bc~a)*3@i@;IRT6*f4oTlcBle528&XMNf!ZjlaROa{3K^y zi<@x(L~ye_bd2zKcTP`EK4ZW!_oMGMLkJ??nL++b@69%D_my3WIK9~nu3@UxtKF*I zyp1atC(&;{k=P>%4$F&eQx};7^MKAhk4Ut-fwuS+0x?xGmF(-e?^}s3JV|L%;ZJzG zHeQTuh-7#?-}&)rh7o%Ca6@Kf_#IX$!LajJ1I$gXZ9j9=j%1ncGqTU6CHM1xTREk* z@jw1eEB8IKw0L3X`tA-<_PK<3unk|9CRn7Z`!P>DgWyX^TUPAX!yP#@vSWGvMZITa zOucMTJ!+djy!;E7r1V0LTE8hM^hMk3kSg29BGCUZ`;waq$Chj^q9))JP>eWw?@UzRk2nIq@^5gn%n|_}=j2CfKB!QRn z({(LQ5vTJ+U?(hiphO)0EpYGa7KgOhTn^s#i@@LWPkqr-rQ{^ppZR|6fi1Xh<*kFk zH`s3(gw+JhvC>_bKGxzZ*QPb>BI6{lX>whlEa&K)xAI?LhrOiEOyE*?eY{dy9)P4L zl9BZaIK`pBM|JT!1I6h7BkU`~qTsf+0SPHl5Ger_2@z?AE~UFfL>lSt?vO^5?hfga z25IRSI;9!9JH9|FVP)(Iz+^}z$SdX@Y-2hQ7min`?N;cn;Tmjbp|mttoa-UWjmKOWb2mf5FZ}$o>zov)2=~*vTj`e%N>dcuE}!SywDRvq3V*u53afnSl8}V|1tohky%?LwlL)MiP z`V_}+#Oj{hYxyA2bG*@CjZ4e%^iVHsFg2|En*VjmjRj^k|{c^)+TJ_rKx4qSECGoG_^$XE-!*noB7Nc>6(R%Sbu6;q*U_5>w_K-fcZ#+5{Rbump{Fx zqKFT5t&uoF7^e-n0}4R+gSq}QuH_6G10S}pQV)GQ6``>vl2e016&*STKWoYH`uE)VRe}bbqo-%)C>;7kWAi**Vc zmzoUkE>6H_Kl%Z8`y5`U*1BCXTutA%Il$;C6sh@pB1x&R(hr?tFhZh#V1O;#)b?r} zyML13wG6@5DV|=2;=|Tb;a@2?HyBiC^E@hRc%9_>p}*>5uE2cY0pJ?leLX3Aq)<&l zEp4}=d%BZ>T{Ni5;B{vx4lvLmF&tDS&3GwjBC-NN0m?9}QuGi-&K)K?)boGg@<(G}CfvpZj z689;Yo5STmg>|2@#*lwtzYpdkMRBJjFaZRN{s6$2h9}}MvM%Um0k}t;{^Ande<`vu zf^&aSH=RzOvMaVyRGRZL4Y%`A!O!{E`NOAun$IZHb-sRF{K6TE{QZ59w~qFsXZuh6 zRTMlIY=pXGQw;ru+hsXldAI61dORV2cQY;<@3`=|@S6US{{qMTcdl9UvJ!pi6(O$4 znrUPq2Unp;Fx&Rs;Zq$npwSx0yS7TEc{1me8Wq$TWJj96SOwQs zmr(C?fmy6(L8)L+u^{UQ05onyO-(7Bsn>*41I>^D7+-LNgPD<>u?=t;mYmCn;E76~n#R`N~ToE{}zbyiu+D z>SP{^dm{#bz4#1T^0P7+60_i734DevJ1&*Nga??T%D^JUs)pho9(?QD)$S2F-9Y*s zCn#jnh3oJRh768P{${!ag$ zhbI+llS|llFAc$5Z@6!kRk2NkZu=xoyjX!%00kI^Y~a_9?0(+H{J|bR=U)L7cjw0e z#7}tn(koYIGR)}?U+ZI}TiPaWw=aR~L_PD-yDx@rW?j}q#lD)x)zJ)0i&?@4`58du z5%2lb0|_ap>#tt7NUbIQ)(-?L{)XB_bOfh+-5zU0E*)9^B5sw_gUO%ad*@oo^0I9W zdw6{U2oK@2+OwX4OVDe(#h5G^iI;em^-?l#Q|{$$;hNLZ1R(!^o*F!vPrwfEclR&` z6NSwIyeh;>G8{~VAF7Q!;{Z6>AJ4ivN}a@6t&76VLac(U$$6VAYbpWuW-9Q|>7*ux zL!D44BC~?-;$-@_m^GL_*7n!oQ7=-h-KOkSO;(AX^c#}dnnMQ}aR^75f04#@#JkT6 z+!Ht5`D?JF$*Qa+I&EF;l23_Upi@h#UC$0oA8*RdaEbNkd^CiDLpkFZYgvtS7enPK z0a3!wzh_48m$|)OHBJd2v{wm9+*wMG@M@+h)7Rr?M?DD)lTEGwgX8Bw75$(To#i_^ z)%MpXlhh&;oy2MKr9je3mE1=Q&JXVqKJ``x;@m)~cwT?iz;a#5Zo7=}ggis2=Ir5Y zae|vIg097i`;cvbBx%Yp5F@}iusbAY0m=);&T2`lVF^8&|0P$%R$lSKMA=|O zZKy1~iUP=4foR0rnT%-on#DDHJUi+r>=_7XKxTelRgRtTnf=3@ilpn){a)dz`~_%3 zB5F_cE{FHKAbQ+rQ}zJ+F5@@P<1gxDz??y*tJkf$0Atmp}*E*4(W!>nu=oW!0qtS@XeY=Ht2)eFNI?X zieDBSxN1!aeh=@icIUavG+0vLi+6=FO+CVQ`D;}=Poq0u{SD{9#!Tbxy&qJL_gnZJ zncySfA6@>6OP!&Lt&oi^2`6?W|KY_wZV+DeWGf?-a#&`fldA*=8HIPt=hQ>h3>6R? zLV`>YJb7<-0KilcUM?S&u?BLYUX&>?rgbAX5aW!*NVGP*0$R54T9MlTKH#He9_U3l z)d>~K!v#3{4O-QO2xeFoUNEt`TLG<&Jj?4_7dWl}rbcn3zb4kSCry;XgY?+UKSRb! zUQa84WGYzi9FBvzCO{_TiBh!K6))MfffNHeOLxb23$j+JiC1>HIGIPb0Gt!CuEXhm zS>6Y4m+~`P;dAzyw|5yraSHkhEr)HyIIM$O!E&fO#r=YsAm5i!K}}h*>Gt|o{0)T( zyro6X#DyQ+0~{{9)mL0j2h6h#mnkDq{tmnMTKDh11PCL$Gf3)>+6m^N)bGr#=k$cC zY)?Y=O6T2nC7rLjT8~EG@k6bQ4dR zd~dq5Kc4<)B}QdrD~9@>0t?cF(8zDRTfV#v&}Z^cVSFibA?bT|zHSv>PzCt>T(o|5 z^(~49oCO5Xt13;2cd^>xjK*VR)V@LmNy zt~!G6s}ONZ1|Vju>GcRUu~7m;?p19OX{=M?6vbRlNi*MohVu0@)Yqk5D10}OpX(z} zws-|IB{w)c`Q_cPsZCLe10 zdlo2ij6kLRFd>%fZ7S|`#FQz}roLc0zBSlwXv(iHbDL+Ht6oZSk#*okKzbf&6ZXU# z0sU#Z?`MRkk@5o1y_mFqbaWu1C2rgQ=zu^V6TguAAAD_okXU-$xCoR zV;!K)q`b|n>Iga+C&|XD^==VMSdVPPYq5&L_4~SPcseu%zFI=m5so?!bO!ljVvrx_ zHs!!th=~enn>xe>{4_fK@g8#{g8yLEEVE%6ovEaIa!!}r_bFS_R8E?pnTRFglLf6W zX2AfXGy$t;$)}zU2d-Q#SOrtk``3S3E8bm2#Fg8bFon4-2s7kqW_Ops`VwlJtrbB4 zJJ=q_c#L{AsgHCF1U~&lqt5&0ZBq!&*ZUBO7v0O0om-tD$l#xT8^)486y+4aVgiK& z#c7kTY#cj+uL(V#m1a<{XZK_h&WTnpMNtI>k*ka^y99-0)P7Qyo2)JKJe6FySTL_e zSl6hx_UH6zfk1ykZJU1tzQ-=^F%>3!60}%!bVzqCN<#I*M+u@3LW3mW%*5!W4=! z-p>4;w3t=#CjD8^-uiQY!FO}K;W1TZVU@RYDSHipFr8?puR6IGC$w|BlQyV+dbPiJ zJKYV`J5^ukD))QZVu^Ctdu!Dq7^ZM?$8d!@(B?hy36;KNYD5~K%I+cT1a=*Vv4FhE zf(1qC)Z%;~b=@xZP?PQ13`X)hDFU`=J{?+))(fywAt@Bz^l&mJ^&{kNB>~=49jGf= z8W$%Q{m1TZ0T`a?zYkj_8;J`+A-LS}ed$r$r2F>$J%XPk1_jB&g`bFB$KqZB7h(8} zbj^6ixHk0=hZLq>%%g zSx)~A+ybQ-8Idl1S;-4>VEOtd+oL*_ql^@d3xRKyrsRngd@8O`I%Pgf5qNTS&s1h9 z-TsuIFuP*^Otyv()eF(3IvAApv-CM8@`ri%+K9~3ZSc*Bl;(A%v2BQAM5R-Ea}O-M0*XaJuQ%Mh3tZm3xKAYV+=!m+-w|Q{O`(4ow|f zBQacvMiI;>M8P$D27aX^ITL=Owi~e9XW8ClL!#la^v*~|w0qeh+>hj!6zvdek{Rx1 z@-+)d5jc1`;z8RHGr~e2?3BKE5&1b<2J^e}V312pf=}J+>YGsctIErKqy4_dNk$d0 zLxVzG{SAU>#B&+dBT5otvZsOP?8KjCR+Ete>rJ0P#ENU}@?_`2BfxEkZ$r0N7t?SnfYOhM_h)}IC(50WWHYY?ZivUwM1kEMF zukDE|8wP)}+$*h8&h%n{-W`CbLbKHLYpY3scLT2*KP?!zFC7yQIFzVu?p)>b<)DS4 z)fepUf4<|Nvyo2d#pr)#hVFh>JgL7zG~uj^64=0#`j^d{9*q_E)b$uGac`rNj=0rF zaT|D-j(lbiG|*}u&g1#QPs?XDVut>=lRHIqrkhX@OpsY5RAvcWTjdxTwSWr53WtfpH_2rFORe`|XfL*i)9#K*NHMiu(hM+Hgc?~DC0@}pPed2+QJaOWGY++pL(&%OLAu!%?a zuRTEZhO}3#Kw;5`35Ge%uh!V1-v?@6n8;{|EmLwa5|dC|aEG%hHZP{jSET^!x+9&r z*;xKXFoalD$sEezcwlCv_xOjcVyC+`z9CdbKPW+DJ45rzy$Zh2} zDhg8Xtwv4^EulhDN_iZ%)hP5stQWUL!qWP3xE%5Jv^v9L>F+FLu|XAcH&yZ@WOx%# zw`xaPU#|E%$L&aR*w1Ly67DLWUpTp~w3$;BLKAdY5MWh&`dHh| zU#m8ivDH7CW=2jP^JHXM08twJXk8dZwl_-wxDyg>N`n*^>%e)-rb?r*W-?1yJ1!d= zTR{fgOUak0kP2l>{uKa-exB_QRV+sfS6o`-Y;%-HOu9K?_L;}ECQwg@DZdbLHpZA$nVa*GUeGhH&0WZA>H96O=qLBXW@jdKo?jDIJ}}$|tuE_N zLko^te#Ok3qhyfLAOWL*sILaY7L9>>m{Gd>P9^1OEWP7GiN1a-dEfCAlXZ`KuL|-2 z?H}BOw>GVo&_n$;<#~D7*PvQ{bbr2Pfxq2@-V+{ZmOj@uN3ee*C$R^3&McC*SZjpJ zk3@hP54E7sB_m4oGCUib@^1%uXCaOro6@cP=U^Tx^bU!-ZZS%T2+s4^W45Y>$k0bK zcspxUOYXx4tH_ls$~vOqBvJ%Gaz(0FaT!F!UB_UyN|_JxI@+J56ySn5;^c)WK~%NR zT0YJm_C1=rWQsLff&66yD?-AuDBI`Gm@#-_EOeM_`PUA=_u*r9a2*q#lcnNQHElOk zCiTx&9R^tkTs=JTT3D=}{ur#_$V0QjV=?+m_;hM%9w-WiuV;XifVY_upAqvBMMjCc zjRIe^tEGk8Q^E`6JAcQA7_U@3Dl_Lt+RJXIfshTZDGAEZa<{Sqzn6?tI1#1jBPXC3 z7d;AwrK84+b)E|h>JKC)h$hT$F5esMA6GTJ_!hUCOdoO2rRa=g3x=z^!{l?>JcsDY zFX%r0+fx9${xub`k6y99>bL4n&%nN47TH{6u-MpUyN~K5WiI@*lCQ}h(l{+R>hkgh z6{XWwM%071Iy*C2o_v?RHdJ73NWDf6O1}?Z59s0Xu#R(U`$~s9yaL zN9;InIG2V|`p;_Q6)W8bJo$g95zvG320zF^_-cZpx73z51c?QQL=lP8RJiV()d~VL zsuQRWgbm{y3A}%IJ0U^4O_R0m`&>e7fbk$Kf&|YTdDp&u72}&rNinp6L&v4y zd)%ncKa-MGzA;cS#VtaIeQwOPvk5|!dz}TFyh%hFCAZdrg(Gp;hg?C;fCU7zh>^z@ zyx^Qv+c~dwC)u#vs{R2gLB6IB;vIGPB)d=t%(X|qlY+82rMm_y(kc zSpJaSzivYo`7!~fGp*r@8{5oNJfb84t3cfWrC$w9IePM^``_iwWd4;MUH_VGSwFx( zRX(IsY70x?^0y+-cyBxq1M(xQV3^Lr_X%K!J`pneT|{#!nEG4QZMLPqF8B89{A_|? z%pFX4@6Bj^xeLNOYz6(W6&J=+R3xonFN(H#_6Y_-=>E@nU7r$Tw?v3(>-tns-=UL_ zNU^S(z~IsT57krWZOPDzH3=M__w9_@B2zC+fW`k6{*g(WC-u%pPIM?JSL5}UVj=h; z^`$V_D93Vbvx`%(5v$C=Q-x+4n+oAUs)&hw5pap8r3l#J z{&mR5q30&{MX;^xB!CRLOLd@Tk9F^b6jCK-BOaUg7oJIm7V9gB3UBi!VU?u zhY|NTk8~oD7E|_>J3+fi4Is>C9`6k%1Gx3JK|brh#QejK8(xWJpZp{lYn%`h?zuP( ze%zCdGzY6Ov&q}f8*MOi-ql6brQ2jtg_mNw%5SkOJnMIkmAhXsY?(BH+Y0~r;1a-p z5vcjB{DJEwcohv(p#SYg-h)C4go_qi!8>G67a*PPU7v&F}uadeYYG1TRA;(ui^5v{tuwKzgfb zVKB|W!?*Egu~-~+75fWsb~&odVF*T^gpNzy23 z@4#g#JGdeK*5FVh%dUt)`R7CV4$_9UnG=N#p5Zo<^s}q{UEH8+*TPMCu01W7-2#(PTjy({SV9=KUitgj0f@Y)pOEhsCkYlJ|9> z$^$Coo8WXWM{$Z%&q;bjSrg0(Hd%;M#j70>DKrzBjQH4iE933S++)W?*n`g#x%&(8X=}s2a}>!v z%?^&Y3{Psl53ghY*Jee;(VMqPeY8*%WYl(hEJdI+6FpZJ8=qXn!q;gv*MyDsXQRo^ z)AG945hQ(>x9x|TJi3rSNz(X4zWikpI-_4JU_nSh zE#vyyqT!me@S6qvslmaj`J1Tdame7V1)*$@@y7YCn*;A_(9>;u)z*1*gSJ*Z#+Fuh+z`QZl=fi^)VQSOMkJjF>lnGO*%4E=?8|qeQ;in@Ok)Ph1c7X3l5&pp zzczL8QB1^Zk~wpnvocKcJMe4SF)S*D0-_d^dSpOes_4-WhfsV;25ZZ$tYjCAyjn;DXlLy zwLaJ~6&XxP@=;zDlWe~?eE08VBTcPNb*$-9o9Uf(aq z6YCXv1QkZv)MZg;z=_47msP8cc!wg>4-$4lTtPkCM+i`1=NGM(o#_LzjUvsmuIt-DUC;srz z_3z&KaH?q6N9?@4?sBxaz+TfjIgGVN$nO*+k0zdBv*w-|n&2>eyaY%AGiH9YsplN1 zO}s}fR`CzF-N+GOUmz3-T%lYRwXN3g8c|Ql!?H1L`;ud10j5I3M6GTF@%PC?cy;jT z(E+oZJo+^g@52_sln5m0meQri`ugIBrpDYA{^@vMBg-khAFf3tXAf1UM+jQ{y`V`)Kn>caVoRu1q6EGHu&mKlL8 zwB5M%*Nn?9hrqn+AxYnh4{aXPyI#Akh76%3{C=wyeQD7?Ce`;IGfh4gTE(%nu<9~O zY?^K&$rl({Uw*EkUt&4QI=40L9)hBC|5(E0$-K(*mY+C`x!9NJ_{uU%GB%e?w0~{< z;#cV9L5wAW+3O=wBT4?1rNDFdS`|bVGAIQbT#`s^xwCF_1DFGCuL0 zA7Q9h@3A!8Han>*mv?M(0eJf(le$s~A6Ygg7g9;{(gpi?d_oPaNRKb(pCekg(x6xW zTdZFbHd&txYCY9sTyoQMygNcMfz06kMQXW|gw8VnFNcU$=133RJJWgu;<}-xR&;l% zF8N?5xn_DneO`xwqTx)N*p$t*n@GYUfvQ4f5OW!((e5_d`au-GqS5&x;x;BV6eVSz z@PT+j=y!}0=RAw`R1#{LliEg<4aR*DK5s$2W1PawZ|agxO@wX!{GJP6B0dNN8pUbM zhl+oJz8{F^&NrIep$(EAGzms@hfSUre7&wK6_Qe;ebr<}R9Ii7FDK0-E`fvNCW6G; z)%z;bcLb|=x8u1;a#U=DpxGy4!{!T=ha}9&2W)jOT#!7Z?vRW{z7ka|ad8@DrY`ec@|*);Qq z<@woH$%^zMKk(+`7D4N)Ya}O)^X}{5B2Y$V1I;8wLu3ep~p>=CchpWBLp*u=9 z$4*Vf8J9Be9zvaK@uBBxqKQQE$y}NK+P?d{{Wzogbl+=2>K5UY2n1=ZDQ%HY;N0(? zSiS$A^W{`5{vn$qPbxpMUUFL)`@$pcRung3#z9K^dM_#-$2UjvI)$~*v3zx@Uqn%J z`@Y_q*c^DHhm}AdanxI0JY34ox14uoSV5E@0!5?LQo5(_v)P_O6g2C%aw{Va<%=2PojF&1vbYlmITfj zZnsZa>ueFPuMRyFCa<#G#?b*NxxUY;&Pd5=puq9dCxbEZVRsbf#^K&@z@D~}8 zibQjf>&17&itisHZ{M%N8_X5PYW2zf{W#CJT08Vga`Q~@EXB!K^o{~mj}4ZmEi=>~ zXEzHgTs859JE-;w2rjL>5C&jWP@qQQ0%BGq2J-KD1_XUcRk4rgi_+2;Ic?tn^E})U z1;GBwOX3&Y7R1{PONc<5(L`((OBty*v-SpuoOOyw(vhDqmY$&h)--YLk$FzRH;W}f z1GE%=kwaQqphafuOVEU|t@omj0AZ6X+hTyQRRVp(ZRNFfZ3{xtc#c|JA>iy|vek5sm< z(~9t%&BnfF=<|8#D#JJZa@6@g&5DqmWr6{^tgIg8{ezD+eyhFsu1&gYe1KkOHDF{^ z)@szcxE`F)5uthslPs;?D_je)t8`k6n>ys@ukiuH5AHjBEGv_I=A3g(?(}+F4v{rfaOMcZs zHLnFo)jw2YdlQ|+whzyIpELB3$~;@fx>#Qd9w{|{>EW?glTrV0a!u2up+1}lJD|#vQN7?4qkKw&yT_j=|5c(C3gN4j%{BS!jFLw8PFOyE>J~K1~UE(M_nt8pA>pqR5v8mR=nrqiKN@cY8ktX6u?# z@A>aVvtpWrp>aoBNaj2X?A#9X(wZLE7DqY!*A`QiYsqtLF-g1lr+74$13&Nf^>NNN z3CgExyM@H?GIK>3RGD&33IZTwD)g)ceci_aZNHV2`d+6mOEjM$@>CO^^Rf6tH>Fgb zAGW4l2}ywaOSPhdUpEz>akh)2c$~5Mb;}TO@pR$oM+%oOrCHrZrEZc98Yet9(Oipg zcHFG7q6%MTU<26)QOBXBdN-OKnhUQpM|Gbq;@1rASp5Uj%ghnFuN9^{QXQvw&-!HA zDhga_6Cnwq_!aDQuLh}_d+`s|vN`*FbVHh!N(KaJ`)6G=?p*PX^s}l_Fw=XnyQM%O z4nOsq<(ki1n{5R}_pU+@CvCOgXa}sm<%q*r=aHrFU8d~7Jske@vHq_!3ONq5c{#gD zt}c#QMNe3^fCN%BynCfA#Q#VQtx91!kU^RH^B3UCBgjHUeQ+u2`EIXBAX1E^a zacouIIa@Idxf=;Eg~pN~gIcMSw<2$Q-a$cHB5&`0D;1MqKG%bKkC|W#{Z0uHRW!mx z?Kb3M=khkS_5#wm(q2$$xSeo#hRK>+%J~3NZGDL<-0XSW)t%M0O!sZ7AyO3wV!8B) z^!@M4M&tbLCQUcn^ipgNU-GzLzjMuyqdoL{|i3 z2gQh3tnd!DzKdSeYPpr4Td04znpJfihGTyR|Cqdy3+!_=Att+<{~cDMP13a`Y9Yn< z?}*s=>*D(s(g^Jy0R{^F(*zMV`0EAt{12e*L}8>%>J`;vwup=(_Zl1%!7J&{6X+-Q z+~=;c>N}Wv@##4A`;40nnPQo^?Pt3Z6c<$rid|1;!Xx7&IydQ-955ffJ9GfT2O$Ph z_pkSwt{@iX@9V&f*?6w=TLcz0#-7!k>+s{g+xOKtxZRQx&^Ddk*Du(bZxC*Rf~Tc!#yQsuunxYshDQ?hBB$6 zc8&sIsGuO%B+Z2X=Bq(+FTJ{0r>v{gZv7#O0Y>b()%>+b=2Vn)J$aA%DUU=9!)=^# zNK?${*dZA`8kw_yU)+n1H;Yxf$vo0FJu!p+(%kDCl^$|$YzVGz{P(X;D@|@QlMCyN zHjX%L55wGSk2mI>{6nBpF#@_&Wm_OX#54v38*EgF*4#^$7ZMCNJjEw-nOXcEc zG9};^%5s8nSHbD8R1g-bCP!?6uK$|5%-VFvmjb z>cwy*NE*bB;ZYN~ac@LHN}_D@qzwLAmyoYZBUn^pK$Y_`igar@JXge62H9zg+ zggzC>ANy3oNHw4QCT+5VfhZtx0S-t9=Svg39I~IWrdF@t!%S-0V@cvU#*gYP{546_ z<OfGU>MIenYZJ%75Ma;0BB5wuQvarCMuMV!wbHVoFcAR-=_@F<^FTmG*IG?X!f# zu16%j`b89bep`0z2lHCHA8yEa@Cwrk2|FY407=#LP-n`u?CYwI@uly=yTuOX0C2ef zS&W0J_>@p9<<1p%ofD+8X+#XON_mf&*2hldUd#~4hSEJo_J`Fa&sa9+W_@GnDbQeX zpTEk$S$GTW2MMMpy8f7pW(D!|>vxlDUAZcG-pLMit!f0E=%P|QmPT1N%fB}==$#xS z7(<9D>IPUMqOYWDj4ae0a((JOxvM*R6!e&IOVrox6a|*}|D&opar7(Ztm*}y%1^Aj zQnqpf67Hf zOlXm#Xv*&pTneUs&0DEq;{nq)*F=XSHroP@1<_*VKgGY-d`&WMlr!AVypwo3>UK`d zq%MNiTbb7rRd#B=w1h^`N4~j`40+?CaMf7{>t|F)yK>hTXKnw}!f@ zt-Z&>&J=SU@h~0iu@=+NGCMsA$nUi}ju}#DwuXG0!GNlLC)a%tc7cU&$UMf?KSU)< z|1)(5&n%Lc!9%Dg=sj4aK3zu`13V>Kn+GV&zg<2@EPZJat9lD5+~P|Gz^ilLt3#AF z+%rP$)n|IMd-`n>9LFTPN9f23^>5Xyy<@>w{6}Vzf(c&@eZV;|t@`MpYMIT0@RL{H z==I;|vzFiU&&SyiQCTf;*2FL59+QBvl!=ZaKB9pyGnZSmom%d4L1ga8l@%7|NMe$W zc|`MpJ$#I(cKFKl;qi(k8@^VB(!9KIRNb-5f^R6Dv%}X8VZPO~u>U-u|oTVo*@s6AV zy~yR{ERS18l{~|0@P#V?`!mO=etfm?Z9vYGN;hxG_3G?J{eW!iSG%iI!|62;x`|9m zm*8f62hM5z8(w<=r4(YsqpnUwQq5{L-oQy{rH1 zQx42q-ieS%RfRK@o2ul-5u`Z-_RR$Z>s}FWBNTC8bZcdj9ER$pM*QhSzvbNz+Ed8G z)1`6KzgOsFN(o@%9+%;?4C~wsFSOB526sE*Mt%zwfFN0+=W!UM=U zK~f-zGj;ARiT?hk-mezsz1Hq7kyQL$?xY?m#UATZWuB+Ki0$J_zIU>K7wx_NX4>gn z$>)P0{kmc4v603wN5a%Swpf=-j*3~)AaHOUueZ+Nj2>Lw_doo~N2k*J%~H#yX0#=f zI;;pwy=A3g_c<*ft<^FJH2|-bj43D)tec2;(e_-c&HTf-I`Xlvb#f;y< z(@?Il$fB5MeTr-B*64LWfPcJgwdPcyPM-Wq@iXAxNSwlBCn}boj%LgNvg!tLpOx}y z1dxVg#~o94hYoB%nR`8H8^`!GUSe9(>3p$^DH~CjU!T>tF7Li}+godGszUy`j`Gv3 zbLGF*Xxzy;%n8ca_>{u673NRzDOhkNl&|#`GSWXz?m{Z&9H^*?St2@P^9HB$?&GKY zz%M_h9#iOypwietFo`?NEq@B}{>_a2QPQakge~Z{TH33aSTU;x8%C3JSr20(bnHJK zN3S?GEq;MI8(@kmj?M67@QsQ9W9{w4GU%dV2k!j#$IqiJr*pXTmWYez%#3ZmB>a|* zDfKhtiyEkLq`FXuRux?Db%vrc;rCwU?{{(0ZqpsA>PQ`di14~{>9O7*NHqlc1Um9RioTyxF~3bzmsZ7J!fM;r{3z8&%l`0jki-vM;vv?V2L}KBB+(6^ z?MQan>zU70OR~JDGz?Q0qgZO*;3ajI=DmpglF%nv(x00k4$tSxikNpes>11U^nhN7 zVf|t81WyWg7qw_7w(7gKOTwUR%pDSn{SWtw(&CQGub6Sk1M;*wMhg5VM-Hxt=a$+J zxraotS<&0WY$6r?-a>N^NA-hx#EygG(BY=~trQRB6#qFM9s$V%aci#c$}5`XyiOGf zQ^5h(AaO}D+j14s|GqqsDft?d5!&VUPCAY336Z{nK`r7uSW@+A(icSbw*MypI%3CfZNdT^wg^OnYr~Zkhf9f6gGtGoH zmDyj$Gi0)+;sTUuhZ@vxXKN$B91PbWvQ7=f{$I<|d!pQ_H)=3()C2nskL{(!>e7A%koMp>_}n1mA4ho=FCn4;RUOG))ohho=mxj5oXKnew~ zZ6xS28rqWZ2x~onz->g}tK0@h)$o1EpjUH^KP}2TfqGLO&$txN@Qg<=!BPbkqX zo3r9Tp8+(OLqRV2FDf~LUoO6R&+bg~TLlNlD*mH%7QoXM9ib_&ZB*9=cY1g$mU>0> z$FsN2`IZEOzAMN-7Ol&Bxl*2Ebu=OOgY#7$%lzgfE8h zm2gi`-jW!La7V{!r63$)cpk~sFTWaTA1MzA4jJ9I$qO(4uNB!!p_sfGho{0IK``T+ z6vUPtled4*SIoV$`QrNjI+}Kpku>XWOhX`Bfy-8Hd;EZOb&E=4tbP3WWz~~x$zK70 zs;)d$QamP&QZsP)#=hu=g%3r!6k2qDH>ddqvr4DK?{@wH6TZ~m)lirC$hj)!it@t~ zBL;vbQl$-Bg&pyT``c5D(sSLrTwx#kfw@ZM-;<_%>7S`%J4=D3FgURh-5)ks>8P zLM-z;0ddGdj_*9nCDRQ&r%c1;ZhrlbIw1~*`wVDqo+%<`bcA$AuQjA9@N!Zm>yO3k zi8v>rvTPX0e^m44Eb9tS`ae%l7#p=L74In%MQbq$L7kUC9DOt_!nDE&VX+^2mx$j8 zqn$cS(O86joMf-+lr#O@q04Ae&7rEFTicoyaNjf(t%VjB4t zEKo)DRD=Yl+lLCUzlMt!VKy1B(C-vpgwcMR)g&!B#Q?P-)pH%{fPYrPTD5IKu*KKl z=s)HCFsT&j+^33E#Km<&e$4k$g(pfiMYFa1LpN8Cp@hPhb9IEBnE#&10HiT6MCMoj zS!t2%6QHQ22+WH+I>Tcat^u5KyJXYBEg;k4rP98K^}%}xhqEL-(!=>0g+?o}O|vHk z_6J+hV4KCap%Bw4@$v8!!;vP1_3zWMY0+d-Eem&1 zF&2q#Sa(#@Anzr4#=lYhR)teMSdG3 z+_^gPmE*jZcwu?#92(L2u5Zo}ph)vIv$ldCK6drDR~$61=qEh)Jl15wNA;d~Utxw= z$T*Bt9uD`hZGEI`VojM>Sji|cEKN_IxIA})?_*V`ObHxuc*K`!^b=)1ceXT1y(mWT`8lt5hwGnEz>f$1H~*Tn={P+TdrOJS|j} zINwH6T?C4SE$r;9+B#FX`H)9tK&s+ zQ&aLz?d1dK!@>bah@@^0SFP*0Ea=avz3K6|CY4Tk+d(B|bdIZ`kF_Qy(UT2yVN!lj ziS_L&{v6V}oyEi5y2GdOkc>!!{>QF6gdV6YtNsC9wzJiDB?joVd<=TNe}HKg*F_lo z)bf{fwMLyE-?c~-03iF8-TVzIX~7GiT=ZeHks)Ki_Z!$RfVXy$b!jRIfle-YH4$3r z#K#k-Z>0PHKOdG>0h@$3k1aAQa+(W~8>9w);O^&aNMADS!+Qx@dxQlH!r>@UM_(zt zqwaqtQC5C1Z`eg{qK@0g0?iQrP}=4WlFnx=Vtu_!Y!STU3LO@f^irFhr}7;>PVV7{ z>0e@W)X+ko$=bAEnc2=;$ft&sW_aH`p6%P(a?brg3DSU=j2`E-Be0| znT?e*(3@>k$zpiZab9n}DV<_lzEB1t`IMDC#cgm9tNWVW4MH=#M-2IovNTI3d=ZK2 z^(70Ck#0X&es5@@H4qif-zWw!-VkhD)`~foTRPsey>hsyA({UOwc&VdZHBD$sMDgR z;Xx93h0I-;+hJB3-oR;gFL5a^C;TXme$2u=B-T`IN&aG^7iZ>?_d>bBuG`U)>}}Wh zCD2O3ywFk>PP*h!bIyrJb`97f2d7#?>upOab22_i6A;n?MfAwIF$+FjQa6jnR_7UL z<}y9{&I{N)Xh8=qMd+dsIm%#LA?eyr%vB4z9Khi0&(DxAS|;+t4L?D*6Z;>YnR(Fm z1$sRU>Fe_#>gUK#F(W=S@k6lK+C52!R-KujC` z2D~bx!y9g%;y)T#-7U@21Dvk&LOWi)`+*-OKUCd;-}vE7m)kfXGRZU)&w(`Na2TO{ zRhfgBVSUKyiM5#}^4hA|0X^X!KiY?N6@JuTadk`S6UHD#GB~ z)fLoZjaSqC`q<;3Ao_06;pt}httiPhrYw;7G>4QX&>Nzy5cDk*vG9I4-7|d@%W{lO zdUS=c_@U$yLOF~A!Bw;$; z-oHHcLSPDT&SLu^8?f@-z)18(p_416bTXi-JgFvfUf$n5w)|1&yV5q+FeDLp4&&ME zbC-JJnHC6X0(ubhP|~XV_%T1Pjc~c=1n?>jg~{2*ZUS2bIP4d{R;MsM)alglAfr9o zHk6Jz(CW1}E{h%lG4cB>r;3v-k-5&y)gnEm z8W$#u$C?!dvBL%{@VD98GoK$yJ>a3BeUaU@VAhLOdB?S-%`hlLD)VQqFXAZ+LSF&C zsa8PfF4SOEj!D1JG> zPAx{^cIx591Om{;05Nvd!DnFiG&rbDL17(t}dh`Fs#$OY-nfYR4iqOT`wN7sziX2mop zOnQNGsN}Wh^+f7t)0vDR{op|9tg`RS8;*H_UznNCZHjg82q>sN+C9Fb+kFEN_PAG* zhutA z?f5J;@4Bx_ldhulrl^P@y;l(h=^)aZlu!)4LkJO2P-)V8mnKbmO#qc%LhleX zKjTe^=+cNys6S-o=+aU6eWJ0RP0qXwwQb0;6-VZvh{JZ zH08NFQ>G>RFGVgp?i=jiKsI(&?gJT3Xhl*JxXw;qE9@{8XSTd?@HmnD8$EtZ{UL7EsF{bHH7dCqKCUMp|E$Y56> zDqokD#DaTW9-xbGzFhR=S)_+&&?%8c!aYI6skk$QMDg$jfq5wgIEPR?G^ZSIDG)LH`2$SIVL1~ z3z&oWH8L-dT}swb?e7j*mCQ_IkXgXv&ceds&xC%U&!x7*@|}a_rQj_NP*$rcu(~lK zQmAb&v|cVD?lFG)XT3Z4@bP~FQ~&@+M)fZM?!#%i(g@&YDQvc*c4bX970hJ0$*5WH zd}@)O>yd{15Jtv+tE<~ayS07wBdBW$l^oNmj${vvs^CzFT@2jRTP(g6MIU)h9r-v% zpzSO=&7?VMUwM~ZV6`dFPgR7E+AZGwiD>bg_Vz6Rq?>N{IR61U7z4I*J-pl?$HP8R zX4&NRkek7r5kKS~@$@E`WOR0QA2g@Fp49J3N7~f*4^VaRG4ahzoO^JwaC*~Dj$XP| zM*v5N&%V>8vF#WtUa3OGMJ(b_53SGgv0VQRf*Z`6YF2gmrql^TC#Zl%S)S`8g?Oad zg&j*BeEO+3+1rz5pnb6?1yE`JJVJaiq+`E4kEjKx#RrD`T9__Pz?T}&r7<3O8vuy> zOPBoXE5Lp+%+0FtA@&wOeo2bp+^q32vhXn7XBeq);Ty2jJ|+rNyz`{OOcj9Qnzq0H z<23*#a+-GA(}}j3_$<#ZY3HN`9F?GXR?w<`z|P6k ziEGCGIg$3r`WR~esW6z7GP=8r0OUE!ly7h4zEB7IAIaPO&KG%-;uuv=MC#>yTAN-U zSQ`zrS&NoKA`$h^a}-VaYz%1Xn4C+WaQHB77q~n|=?sbNZ{yfs+G;bn@;xz-(@~N^ zy5wnc%Q)=JHW7HA5;OkNn#_>Rms@;ps<6K5<7KTCpkpHiI16T@>d!R0PA86S*&Zx^ zC0Pj)Y@{QQtf{P>5C?YI z8KyB+MgLZ=HY{KfyG+pYL*o|*Hziu@lB6xcc69bLY>CQ5HtcsQQ?!_BJpGu2ENG~Z zG99h*J0y1=ArwyKVy@nh@te`TN%^6Un21_bQTaNw>n&9zsX9lgh!W%V&3J(1eCq!$ zRxu7I37Cu((MCW`3l(HDJ8*xPa8NasA07(SJoA=iBQAdEV->HR`Iv)sd&9;j*OZpG zeGxf;qoqP=!5hgX+d`5J^Tw0S))Rcrp+KE-VRr%5K1kzNy#&Yu<{qq;F$M-L<=U^u zcGK)$)I>rE%VYiHaQD-9^`xW1cOS&FL(guQw;o5hD=6%G^y(D>RrkuRM}Mhel~1oJ z_gARP&ATHoee_xDF-&K{5Ba%1$P`Q=rrU|efQs%H?t`LzAX1Ul7KlHPU|f@^38F8e zo2_mzo7R|Xn{Ou~5>oHYLYxy<#U~^Wh29a$iM2V|e4@NH{brGzqbzWi;l{9>cwq?tQ?{963|tuf(8QX&zfl%715Ud(lDz(^uu zDu&mA^{}ScLc`?0CY?q#BI0+WU@`kk8Imd}@2x40{G6Y6#dwpa@_wZy9ASU5t%Zt7 zYeHMwXnpyh-zQ56GP#D?Te21^Gv}FKhE4=0{k>163p<`2jD*VF1nPGHginYy<-|X_ z{s20B)45Wc_6{tapT0_2uGtkvR*J~7rPmr47ouZ78V{h$BK7S2*o|($0iYGEqBmUqZK%Q`xhT|3g{J3BM~HxkWe z>HPIj@iVCCbSCvg4LMQ)%J${DcsjkB>DM!L zr|gU2D<2qQ#!4$w`(vzJllDI7du?~abWTFl#Pz?wW=f)z1nhQcdi5VGC9L*tyZjOb zj67+DTJ&^Nyy@JWo0q!st?Ab4 z^06Z&diKSEzlCR~Jt@uMcwdvTX<(XN`b*971uraA+auh;;x|>Z->zZ6Udhkb!VMwp z{#*X|)pV3!7e#cr@x(9n_f}FuDGx6VfAEt@`!LDgR;~R-DA^id{9(96(;r*vyihMS z&{bW(w)pkdrmzidGLTC;oL9i>S4cg;QRBY+B!E*9Fux0AuGS~M`WVu40BWGmzrFbZP2g7S^qEJ9w4<&JQ z>hV#vX)W~$W8V&Si0RfwE{VETiiH_}L-67va-Z0M0#Wy4u#GD;V!M1~!^cJs=lLDy_IX6WKq zVSi+t$GN$k*3C@WlRKGGkVJXHxLMK3zzm0fiBP0L=)38GTVU>NpbB9FXjPyRp~k-s zqI~`9Uj8kf$I?P;)Z`NX#ljHKdC9!4*&J!UvU`YZruAw0wUrRPGIMKkG406a8n6&h z9GZ+zsFLyng>8*`^|Hv4+%Hox>@1MxinUVH@fAhI!_6Y)b>ek*&9 zjus|UUf^Vv)qM)tH1lbNZD!TQKLLD@dW!$WzUn`TZefjd<(a^~3Zz_ICQ~$BKGXl9 zHTsT-`TGtacR+*W&VMeV{a-adaB2T72<@LJ6+rzOb+5QY#BRv|&ZK|e<`guy2C&b& z0O?nf6Y`&n8NaO`M<%$90Mg?>2?fAzaT#6rPx{aV@P_^$I`(M_BL=+Ox&FHj|8Gpx zf6q_zSZf}E@$7ys9qe}E|eZ?R`UQVtLw^>5K;z)A$fnE`kBzmv;>JGLLVOaK#| zLh<-N$2I8q3xB*MX8;i5XkmY#khxh@ zYR`Yqc^~-4TmJuYCAiFX{3qNHDBXZq!pj!;uV|p`{dZgZzxmex|6~8(Su^0A+|_xf zcK6#uVQIyK5l$dD3V7MGd2yi2#io6ks|mCWMpU#VFHK^nH!>%35cl!LmjQ#~|J>K& zz>1&ug5wVGSN;v-@0Aql1OP%$$edX5#y=l77VGBgA)NnwZa4&5HTP&R{OhY5X5lIP z|N4rPn3*G;zv_Se?GnW=ubclm$lc-NX)hqJfeUo@^-;6S*>h6B@j~;->(Y|yl94;{ zUiJ3XDAu8?4>h92V^~~6E+AR7C`*N)Td-))4RO97-YzLQhzfuHeP;ASnpS&D5f^J) zl&5BjEU4Yrk91Rhw{j=7OFK5(&Y4-7Nqm=7|v7)D-7P< zfo2qB>M>6T(-W{(Uk$gY8VyZ@mIUw>Qj4c)tAsRnmy3mVo3OtcWijWeTkduW4BZgd z44T5aO2K_AUXC=;CHNJCMeCR+l{UATjqtzCs}w3RLX$Ep2{1BYZ6E8W&C`31rA{6J zzW5kj47pv9%qOvGg$5GvbTY8%9=9BHf2##UMTd9*XecaaT1b(q+XQrrWI%36KC2maV!c%Jdc5-D3&N%)CPK+H3j zQRnxQ1%9eH-zdnuXGu2Pp~rA~^4D}>f1?5{z&+sW>wrCR*mb#xVU)Jp2q?+l+Oq59a#LzYyegi*R?r4UXgilfXz=%xASsPmWB{wX&+c&EHg zt0a0Q*gv6MZY+L87x$ePd{_!Vhn5UXs(^`Q7II4D1%zb10(obm<)V^4>Dv7^XsJ%!S-fCeXvWE> zSqN8RO^*5Hc@`RhF=g2cEcJR$)@g>Tzq*;TpP0N-@|{0j$g|IGb?9oS&sNJ z|JwBIb#fDZRM&^f$3byXz5)A5=79@Y<&b07&1MI_fiw|RW7ky#78uNNET;Fo6~Ga! zW#^Q-4%rJk6A4pF1to80`J&6iR{2$&|84<#x&z?lVXoShoG5c*zJWcy2RXhCA7}Bu~cGCvhqngl2QOnw| z_1N2DQ3AD$ukvSCJR5_1idk5AQJui3NTEh8HWkDqE${3%5++9wr zgAg0DV$rxz%!~67S$6E5J%4qg3~nK@_N}vkt1lKdIU+BZW1{$_nl%+z! zo|@^#ZWektRlUPA1!J)rfOiTiEn8B`_IsYu^lBr6?1xn{NpZ%&DtQ){le>+hp6}_Y04b$&Muki!Ghzf@EN*`TAg)oNYFG1Tl^&{>wA- z&4$Ay-G-yYJAOW2=c?1h%=mv3+&{CinmV(FdO$8sT5>%B4P`h|(2WDjhDE;8%!7`q zM;yB}X)BX1YDbR*ZX!wa?A@6@wwCzl*BAzzFq_GcVBxuxJ8h*9mQ7G6jp$nki*78` zVuY#^^KAC+#dE39R;Cr2MY>YDs{yOa47fxU^96~+OjMA-v6GtFEe@OYP~Q>h$pEr) zPcdfPN~$jzKy-&iPwYIE8uE|UJ<8vH1Ba~2Bo6v-2XQ@|o0V`!{JAZ1Jm`^c_OvOc z`R1a{S%h!GW@49&ewBmNf;%ufAz2q?-0YONm-*u~JRoHvtixNb|EXE&r~}rG$+ngD zk@oAu$dPxRLp-L>8(3CSC(b{mI4u%Q-Ju#d!;=qH)kx^C?Z*6u0g3FXb*M*cg^JWwO?2F7BURn3+Q{w@unH?mYl@8e;>wkwGcPFPNQZD}Fz!;HNrOlCGn9-0wATb;Kt(7fXv86D{ zyNq_vZ9ZRL<#)9JZ+1O)x*H|Y;Iog>0=CBtUl5cxnQNFF8)^?FKMWezA9ew*-r5J7 zVdbY=Ic#7@2YB0&ENWeLvdJ!)e``~uSa1WA)ty)NG`+iZiojZcGPEMX6XJ_3D3NSnsT*W&ZLaIFm(*0d08toq7_Vfgu2EW z5x})~L{M(dbGC7OGr>EIeUIsT^6029epjxqmp(t*4@+1}Z(i8q+9)@4W3n30)aicj z0+tB~*o<3bK4qR)*qGnWl!JQj04DGtJ2MV^4m4PpoNz-6OLEXD^hkXesV`g8#@B(Z z*)Q5{86K(Y+)Z^GGWO(cWNo&;KwlzSF^nQ&Ks*(?b@pW_tCEd=cCbJP6tsGKWKd=> zkj9bqj!hEJG0h40B17%fTYAFqEb8?>sAU(#&C(UcART+!|E9)iz6JZ#)!(8hki$*x zz&>qdj*eA5zR<7!Fu(DF+?i|nbS>LMjn^^+V+4qV{%o_i#_aA{?zm!*K$Cmr{BgcU z4Mye)E;8krDT6mrjcflIeCAwDurKMbOm4RNBUh1qb+q%_^_?e`I$A^PGW2Qppk;fl zV}8h+`&zhVUN`4jwCe)+NT{q>WE1$USmlt|?!1_r&Ub3znbu_fMH2$kR<)msejTDj zhAd4G?@vkG&rR|Nbmd}F!*MvN+{nKJKX>KuzsrQFp;}tfWX$u~Dg27WsatAjk~RRN z`d50E{rYHuWm8BfC%Y>#&$ZQXj&tQoHfZ)x_C@NVRBg@MIjFMtRH zDZQV*BfF{qQKzy7kcrPK5k7s#IMiCN0{g03DrVmZ6L7GrbT;1|Wgjem>H3vOSqjUG zzqS5%M1|pG|58B%+ehmNir8#s^vq`k9qBiF6kIgND4g8#_4(bY{9!jwrkn*F>H?B2@7lI6#lY+qKl0oA{7n1c1cUnP>-X^W>M1XtfKdwD>)MT3+iC){ zDkM{x=h8h!3}0dUdztUX5#$wIr;N|5S_4C`P;_*vgQIX8+6R+!*(1GrmP}KpQ7rfU z1il%HGGc$}ZK&kW|jm?2zm+(#-3}G?-_(p|VS%w8;Gv#L23JOD$fB z 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 E0C4tfy8r+H diff --git a/site/static/img/envoy-gateway-resources-overview.png b/site/static/img/envoy-gateway-resources-overview.png new file mode 100644 index 0000000000000000000000000000000000000000..d42ac01e27f616a7e8407d7ce829e3fcef19da80 GIT binary patch literal 563199 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¥vVt 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 literal 0 HcmV?d00001 diff --git a/site/static/img/envoy-pod-resources-dashboard.png b/site/static/img/envoy-pod-resources-dashboard.png deleted file mode 100644 index 1236ceb0f37f9845921aa0651f4303a2092e0c66..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 74690 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�kH#6gh7k^t4#rLw+Dv*S_Me%ITedb*&y; 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 diff --git a/site/static/img/envoy-global-dashboard.png b/site/static/img/envoy-proxy-global-dashboard.png similarity index 100% rename from site/static/img/envoy-global-dashboard.png rename to site/static/img/envoy-proxy-global-dashboard.png diff --git a/site/static/img/resources-monitor-dashboard.png b/site/static/img/resources-monitor-dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..74c3b1759bf78730f71a5dfdda7a369786f83bcf GIT binary patch literal 261700 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 z zUK^!6_N8}@D+!I(;Nj62+Za|e@z@@E5`3`BR-A2*d|5b2oSkTUFC3b+CIh84Ueol2 z9+tw*MS%yvLZeqdg{E^R#G=jO=FQF@X$0IAf^Hm>@g`9JwHh7N;Nwdnr(j2D8_KAVQH`ipZ0=`p1`5970B_*Fzvshj{vRbfA^%xnh_ozI= zX)Qdo50`r?+gJ}Fb~;EEF7=h}RrS-FQV;cNS108bu4+c1{kSM}YF%Yb1%1`g;p{Z9 z5Mc}4cWYhqsLtt%UHzj6ZQ6k^V@ywAew1I12pR9Y3l&9) z4SnyfI4_0cyO;OMx@sxDxXm|RrIJQ`gry|h+q=+9TaWDbEegkvM~ZqYFQ`p0S$4H= zD`3|vT^j*iB3JJb7K|n$NJ-^03mMou8jJJ7K7r9;!cX$17Q7DVPSR8mQ4-RNnSzBN zpHsqp<|odG_#BJUQFst>1D)|5=#cw-jOOL4zJ8tIRcLM4)zvC=tB@5+bhIeD5cik8NHcv)&3kyR&4xlXh~vCF z?+;dmngWPOTp(WPvF$Xbkpf4^s!&rleH&4kaFH0GWNuH?V;r3ItGEPUA_1TBSa)K# zb60#~O3Cp61E^!-b8q+ESz$7(l#mJ!g}YZ{xOQRJcl2KvTXxX*EI@|z2Nue9Xp%6E z+W1|2{K7f-r~A40_h%Yk>_!V3dGvU@Z1P~QFx zRB-&oO7)tS()iD8E|y?)1JU`~tN7B!T_rS#)6@P)`8`|u6LOGt4o#-AVx!0~N9suj zn+`R(+e(56(|q#`{S-W~H?8Kn+|cJFasuvVWIj(jHDnX2aaBsZVzkmBQ%EQ{2QPS@ zn>}Vy%sRscCQ}U+A2@#_^+Nyi&Ie8v4_F1EHOp%YByad^6FjCVrD`DOh~43$(KA9| z8Fb>VbPIc60bczSBeF>XaA)(5EKl%%SzELhfIIX1RR7^|E%9}+-D^c*kAg1q5zS)h zk~FW5@(aiPD%0h49sHE{duNZyYME>=LkRZgPv#`B3y?X~0!%~ASxIsBkCdRC{ zO~gXH!|i24M>p*ZFSwwuKCve?UU_K~#9OEu#KA9Zvfg4z!*eO^j$8#67=NsziymVq-*h+WU@r>nSNIvqMiuB20D8q2^ijmH({JN{VGi21kT zmqNZ$!cK?VyuG0HS6MX!2>Tr9^2aFo%ri7Qx?#@}RMYF~v}+8vd+)nTR+_?`X4p*l z-sqafGoVjmAQ}*XOSwS$033wKh9aVGc8U_h-VV6bH6cAdeq;*U`7rxUj%)pe7NzMH zT{{R$svp-ylj0*ZFHRH1E{QTWeeeqZ=jC59QQ+IBK~g5HPthv<7UY>Zo}c)VgXuWy zx+x|L%4Bll1+CsP58H1w(G3(8GP0}VnQ1XbO})A~$E4EJBaG^<9UolGWj7gH*>ZXQ zB|Gdxbi8yWG-X2D(m4Yx)Pq^`K(%YMMXbU1v6>`A>Dtz8zFDmLrF@@T08a?l^>~^v z6Q16TOwuxezVVFPz(UtuH{`5j{&Y?|fG(Bwc9Q)X3^*L=oW#1b)my-he6^xbPTv0 z-B=@&YTZI1#(r%&u%3_0vHI4{w{>&F`dfXs*IM%+#9VtuNej~L)oPbg{X35Lz0U89 zd`hmq%gv&DQ&oY*6hg8*vew)esvGX?Q+W6MI1eaeWlJ_K9%+C13UuaKF}2&lAtm*ea_1I07$$Tks`V$0J03dAhvCB%N%m+vVn zjdzA?F4yw=o8qKd!V8U}eXF_aos=tOh9uC- z*9%8RE*dWpHLF{-%Iv{OB6Q%Kttjn9c9|kzul&qk3V|UP6D!;QitDB z2YSv6aAo_21=Rih9IkUORKDA4j8%O(!=A27_%(}b6baEI1v2?hkbTR9Mun>5ncgqT z5@hcFjCHOf28Ia*6-^uz*+pc0`|08OO-jm9ExVVs;lYWeL@naexqctr%lW@TfUnPl+aZ=yGPON9 z7qhm|(}k>DOaJobZzkQDQXT>}kuy1K(wwKjf?7MWd8&d=J0k;Wd+WM869Q!%VXzKt zB0jv-o`+lj>FxC8+ERnX(F%y3AkhG^Sm;?vlt#j|<=m3#%w!V`EVO!*?@zw^mBoAc zte}5Dzix?c0tgLjc0Nc*y;RLbv02uz+QPan&3{|?neR96QA_;glH^(xrKG0wo5(y& zbE8kORhw0m;@p|%1MBwcw}#{P=L}aq&9TPC`$o@uYy@sqc=x5~lmOV4tE2YrgY*&k zzWbrIj9GHFsvB}eC5{28-VORTsuxE{g_X$z~5b3t@1qA#v74eDScMTt4{xgZ!-#U;azzhDMI-t z8WD2{GT`VJ36HPUBuq2(GGB2N)Sn?Ii$_q8v-vzLmNKnK?tX3O|h^D4UY1lbf~Wp6_j<-A9LSk?aBfX%;5J%k^vxA=CYUgdj}=2tM>a$>wTrybW+b z`;EMi{8z$Wq37Skbn&4|`#xh6(K{Cx%`QsTgIQYzn@Ky5g)5db5VY|VZ!=Dp#1yB| z5B7nD_`TMEoI=I^UK&4g!K=UO;7bpfXZr?F_C&uPn~N9ztfe-~1I*xV;1;Z{9x7s@ z?gtRH*-ZU`Pdy|3oTnjK*_98URnI$0{sjepY?{ZIKX~5csrUcZSAfEJ%@hkqK8JqC z11ihE*XyOS{IWy8mF(Yt6vhS!Oop+$iD4gZ_rDhTGxbL?7>a6j;dBJB$0tk5tE0c% z@;{KBKgPI0gp}f=0dzWtfh4ZGwEz3t(J+2;qY66AazHERe}AKJhAB#-a)-%{;1s7SA>5T-i2Tk zBI5^OR-n=Iuq|)%Z=e3xb^iKp7k?v1a10}Sp=6k<*qbm~>F;m1+d#f@Il1m1`TIR^ z&{UB;3f^qR<>>zX&3}#YuWP5#zN~!Aq?z|h3HQ%RVTLiuOeSwu6Q=g)Kd^>x8Gd@(dN%p<`NNP-WnUVoFBD1 z#n20S+PbtzIL@~i_W1yq4r+?3~atb&j zpVvyK_Mkin=2H=u9LaC9H;XUVPn1YZK%lCrS#EtfPN?tYGO`wKP0mG0xdh~+$pqX! z-s|ZZxSRtz6czO_gb+JIRy@0fJF7hjp!x$g>w7*I`7Lc7ZPNZgk;iqFU%|?lx~|ar zTkDsi#Tn!Xf`cNnti%gs8)=| zi!@Iz_>=WiB zg+rUOO_u9hV++3U)jX)XyEq6PrC$H@GtuJ9AwGi{WQo2+@7YxRt*x!SCPaUYbVcFLXyZ1E)5T%pE-bPDt^~HV+MzZ=<)(^D=7!^EdQh=%?cU* zrB0yQKubr*Xhk}68zUw1IY93Mw4WR`MM(s~nq0P_&Cd0fajNta)g<%T2>udqi?~Bp zT3U$F`ZPNjsLODH13fA$0!T8_aew+Ic2CnQ~6U5v+*hVmLfdlArbmlvj$CV08gJOxwnc-c>p6*iQ) zNF||}{MzE?1$J0?ICuBQKoYabvZ%Z?m-ZCfCisq{Jk)A;YOLI_gVZzZ{=--yC{d2o zxj+pfq%*whs{}ZH+j4Y}E@xnrVT4vL@hGUl2ENx2C*Hgus;+a-F>Ng%_f(Qp$Y-f; zUC(V|uGXjNi0PPi7|kiq?@apro&a^n^9*Z$s)$68`S2)JWa^X)dktC0XM4vrjH6UO zIua?G9pJD>pi_n))rVuBnLzQ_O<=+MtPgmMW}^>GD?u7FI~?d5T1(kG;Inqt*V5NN z>*HneiU%sp&hM!e-fe(kyUu#95==b6-nwHkqX%w9k zDDncQsgdFn1gCsBg}~i>fGB?2c5E?vHrOdK;#xEO_wKn_ec@z;Q+xLV)nm|#)zH=T zeFtQAc6Lm%!|=yMhbh3*ChL%(8(C*P-BLMa5cm4M`UUKV0aXYm$Uz{!TihDAeWt4R@i#FFWelAESI-Ev#(ZQJhrueeB9*yVJ!_l zay-z~nP-Px5@ONg`4oJNz7ZmDG>FfbIX*c_aTY-CbzV~MbvFps&%YPvFYm=@Ygj$P z79s!(3#HV(1$3FMY*51)Ma1zRtH)qpx1R`6lU;*(zz!c7aZaiPh)|w%IE1-JNQA z`Q@v|1qS&cldpcf*Cm14F2^%uYTMbyjo(2BCriv2AE&t7fUz-vTMpsgITfkOWA>djVds+RGV<%#k$r@14=At!S?3(XI!lmRbR_|20x6fYz@W}@`Uelz?cq2Lvy&X8z_UL zK*=}RzS$^4K>NedMX$Zb+y`_kjt9>jUt5*2sKIZ?QWOUx3FPZhnm(2(W%3?oBYlgNgk*AtLd`XZ{T_kUj=M{A0zD-k+Zp}_OCjF}6+Gp=qd#F+dC3Nakp*P2!k@v9#uTOSI zw-GnEDxDs8GGN3GP5@7M=LDBT6wcRk|IROoPFs#v>4abtvt2PkBOzJY=iWb1^ly+I; zkQdz?sfHo74Z@;d^OMF6$>*kXKg^%!2Npv+Y~-fKb4*~t&<_pE}^5Ddwq=8%8W zC1FAKc#F&PL?Hx0feHglWw8#h@qWB6R!m~Gn1JW#6-7wqF=TA`M6H|~o{D;PUG@=1 zB&%bkuGTaRV_c*2ys5LQY1MN0`ix*@bri<4kpO3tC6Hqe~h!WP5`I9 z$8P*k#-&hZCLUAeu!B-g(Z^*jSIJm6`uqT^Zl>foCaqp&=h7cKyN;5?XqeqeeG{2!6M(H)8%^_4XcA-sTXdqUrrf~;Ve|>e(yT^s;8D)Fv?jX6B zXSD*!n3T$T5o^jus@G5?a{{rz?KqR1lRSx%0<;AWSov@Oh>#GlKT*k~H^oMYtYv08 zn@*+xnqKm4&*yXbP2|0?%vR$xas|;B>-maWVMv|&Mep`^iJ@cZ9q$U*l_6R|KiSL} z^cDH;`xp_I5trGEWwAT=bY(_4Q=rfuj-|vUPR7<FJUGU;tzRuM)@6QH7PhV5B@T2 zxEo_!BMP5Q5cI$Rw8-G0#I0&~QdP`SsdPsjG0g0TO+mOir;|lF=R1#twnfzn4XTu? zTX-A>t=3F%HgLA1M!%;Yq45GA8PS8;H9YQmGVi31P1VX~>*h5Z^h=a$)LZ=u6>&7Bm!}S|Vj#%8BoRg6^xQ_Ar6w2i0Rvvg!tW|qN@k@k z^Z1_EsUj@K^~B$^3buN@W$tMAyo0b5+#5^NcSn!!YawtyOXqes;D5_uQtPqIHMv<_ zX=(I+c-$#FfUPDJH!>N1h82}tJxSy9dRD7P*|wBPaPr)IZRdhYygYq*QmyP@1S|1H zGO19mJXTd>$&pqn)_TvKvm+Xbhuw-jOc{>Bpsmp8#R7-f>hjP+4)}7=+0wa7o3niU zZM|~_mocN0RSz|!xjOODT^)ES9Nzl!prB8mVz`x=Ak2tWbQoEXEr@4?i$c^M?)6xH zPq->dkTJRf$+Qo|QTSYCfi+V-Tc4v!l&;oBBs{CiGSP35V4F=1V z@py)aGgR@fLJF`{YRr2MEC#*87;)mVz%E=79ljxgPj*fA?V|8$-MNAJ1m_n$x@cV} zK8ZT31|~wz$??%4#!!puaNB+1*osml469<J(2r7cgMzvwVI-8G_?%LoK(yPexqNPu_ zEjt7G*hi3mm=Gigw?8b#j|a((jc1@fH7gt%UrYup{!1TL9`!C@I&^fb6$|cLJ~mU8 zp_~^XRES8=isag63AnMW^U(PbV=<8sz=E9b&=aW$}m0fXYn$7I7lGEhw{*XB``F|BE;X5U_I;I@A)jMK!o@kA$ypOy^{>}L_61(ug?K$d?(4M-yKjSF%uKUtTbN6!H`HRsZ4IlA>;EoeHe5;*ez&;MBB!8HZ7=)EQ9cu3bMO?`O5S zMT9?AuZ?UlI9Ys!sEPoo_Pdxu>7A;#^k%csD3ocpP%cf1+YduxH<=M;R~6{#58JyS z=NEjQjhBia&b`y?^@L&dQhG2!f$9LGXGHVa$XoN#nYD;7ozE7HGjZ0}VD>V1K3vci z&XEM(2^?FCp3_f?f9O^cft1yD#V7MU)Kv130M6P^!yxf~-ge0Drr_&~&PbJM$@4O8 zmF5mW8#Wpp{;ciK7jz;|k8FzdA7CAUu?+kW2GUOu;owj>1zORp9 z((@IXv$Zby6WVRYo5gPS-oEF3v6tx7Dlz`g;7IP)Sv;;1;o^(6s zfPx6}xIOtq55#i$+HGN3eBN^n+E*<%rG{96{BuKtnz(1D2h8O9_>7m;QQs(Llf&iO zX%(89+>kzSJboPTfXed04SG40f0t%Kc4Pe-(~@L z15@H?;m$Oi3W_gIjUuorBxg9 zT_8b}pa{e@L4)T3XWRv}vU4eF46#V8o7fNgs-Gb$GB5A8ew56P_Hh>RSdzo1oGVAm zjwlsd&K1?_t-_>mXf>H-ri4avfIAnPZzY<+URBhZ%EFe(Q=bk^WM~q>bCuF+w-v~L z->tWtA9Qg4g@{PaRL?a7HdnO0v{k?l3D@-Tbir((Rl1PK?{f}Ek!tN81WdOlpL-(E zE1y5yqGc9R|Its4y5XeL8FVzUoNypd_sL0_Zn8>8Duq+01(G>LfG@|POI#g}LG3%^ z*8w!jXc|HcRJ_ddWc~sedunhPOsYUI6Q0}S6{6qMUljDzp2z!ym>d;qxz3np6N8xG z%%{!W+2-dx9=FpwZ$=Jnw5gJ^qkcFQklK~N{*5Z^y`t3RFWJeAnh7|FTR#t3l>dWnE z_`7cD)VUb&twgpY4n>t_5)H%`KS)G9Z+WZw(06&ePlo-7So&1RM(RC6`qp9WD!D)- z>bcN3JeddrK7~`UiS(8%3cf#iIs%Cg#no32#M8y*kIN$$tAkbgmoxT&%74yKI~s`R?Wxs=p~8H`w=4$e6`lj-ZXPawLS$F1ZCZOvDb#Qv2W z$eZh(sDrYYJf06D{V-N~U#oQWXE!Uqwhq%3Vg-(;@9-V>?aTMSbhw_;(U?+19Uva# z5}_Nz1ztbGTgDevZv`4Q-rnMCggQ&_1oiqmhx%d5%|X{05Pp^xJ_Y@~sF=L+T%!HX z6{bBNo6g=KHxFZ!-9JVzJg)o>lQ`!WeEvWf>go?o^Fw^y~LTJKT;1 zEEY3U;$3zE(UMA;7O8`U(%WIsJ6zm4h;2jqPb{^j`T!qkJgru$Ly>&KVya1`&P<-s zYT)uZqt(d-=m2yuZyG(ejDXGBB_&(dM8a$;+s`6sQl`c?TcVB`)8ZwQ1ro$isNdX{ z(f}v(Ypb9OUT6gk%V{*B6snX+>T5=8J$m~IQ5 zotdP8BYhuJj~}~$;2T>6r^o9vM}jEmEp9Tgo_`efDBmlolz{1EX5WC7TSYfAJTpE; zpvcbV{+G)cU4!gzJmQI2y>EA&Z?Cf>czpV3bC|tzpm7i`d4t6})EN5C&?w5Kb7=I$ zd8JAfk)3Xe6;KP>R;TG?Oz{4mHy2Nr;2mZT6m+^~GA=%f#^)6PXkl_`M5&B`l8mo; zaT1wy3XS3hMq@1#&k)m4Wv|glQsfl}nASXQy2z2+BoWYmq%3(3gkM)zx6D8+>DH7o zWGF%mKsLfy&XNhnUL|FQCliZsMXb`yH~q}+v^!AhKk62^B&tRXqe6w zO5U1b%Us+MDR{lV$!}kI7-a|8zq8h0N8y1{?f$fJ6Y}|Pt%X`S3jnH6Yz24pLlo~r z3~(?zWuF6;#sA7g@c7q98wK7j$ZsrGZ-4d%EwZg#CP-H3w5Bo$UyJldWclpr*TkP=fDn`)E#+5-$v;>nKsvi{b|n z>9a(T+G>9V0kkQ#o%ae*ZdL31p4dr<3i94)!S1z3T^S-I2=R27Xp@I{k#%a`Rx1FL z1m)!kL8rTVyH}OU#dR+2pT4nnCFMLx`}WwbcpY z@Lj>ZMx!-x3?kzOv-KsO)EU13KTXB=#{{*I_lf)ZOfdb6u2z(aNy+?aERKvOdklG2 z^9{8yLdLzL!fOmoB9#o=19drYTGdGmEfFfLHd>Z(4GfG6)k17ml;z7Gt%zBh1hbR6 z(?c|Xj9En^O`J$&85v7B?b9t&Vxap3a_`{FVr%I|ahC&-TV!+J9`Q73Lsk+d(L&70 z?#A&$K{s&@W?Hz?-5cQgA=Ug)3F1G=2{2D#>uw2@#&y9}LPlf_F)!u0xh~chN|yg7 zKi*H4OVwcXOAu?;POZ?zuX;tPQnUL}0a%bQp|%~G{>TI6DFNfe*y5UFK;)Q6Y%@`2U%P|xGl zOl0j|?)?YMA8nSKA~E?uxm%j{RdlwH!#e>$YDPi`$6C2U34Y=m-z%1)O}YRG-sxg} zdxEM*84`eGhGWfEnWrzX+(Cf33XTo4S8rj>zX;gk$3J5;7fNM*f~8Q%mxutzY{G0} zf%IB}lq8xesUREAVF;vGyU+UQ%IgHDbI4LM@AYsPFxd}#-X$a_M@C!akIuKwcD9-v zZ-QpNIBCRa5({8m?|^{zUAR6FC%QU1O2%<&&z7)MjUd{3($L&k{1OXT?{wed?0{JD zX*S;8Cu;XNNaX+GhF0Aye=?pn4?(+m{@u&*SLMS(>+_N9kr!MOd@EmWdbo)OgiJIqy^^hz{>Zc8(Y?iXy0OG}Vq+%gom*n2lJA*3cL^qKnjW zLMv`$vydODo2>Ok>kN}v6{r&YT|S|f2+G6Acd0Fp=eTdmP6xUcMh^bZlOx^u&& zG*D%wQyLA|un#1%RE2T1|Hx8>X+bFmtj50+4&=wL}ZkFW2dZ! zfTs59f^C@`be~6DA#bB$I*L zEPF#<#6}W8$>9g_^{NZLMo9`(-0!d?Zd^SkgaaAO%a@M_g0v_SfKh3cE7Yui_9*L& zzwF_cAsu~E&SnQf@m6tZuzv*Q_NhU*^!{LCgv%)Rds(UuC%|$%MRzYsUO=I5E+qJC zSf}j^QGj!8{3ET$4w5$`N+S9yP_K)bJvodoM`?Ajjf-jU?-7)uk(w5`AC4O|TE{61 zJckk5hI{Dw*5rI7iiO;0y#_}V{NQ_Y*Bxup+mpe0C;ZF|gZDAol6riK3_@O`SA!%=_B2Bf3vgbzNHV2v#MwLT3?fB~GZiqq2#AH9CB+wJo& z|BwRsdjek1IzgI}t_SGI&u%^j5AQO!7$k@s8wYTlHhO+t?obgie{F#~uVXO-r!yFG zKYn7#n_6veU2HgolFye^<p)^m&gZgo*c@3oB_x+PhI;Ypp z2${d$?WKigek>h#!ELS6FX9WrvJ-X0c8N66My^G`)?mfax0}SfGO5jZ z$8=_6+euJ$h>Q9s(q4;shnI9TYZ;#8P_s=*b1?99XM4f)lI4Y_FYZ1VU;W6kmwcLo zu9jBS%!Y#HrFQ_gi#hhmy0gawr+yPnRE6h@^FS1&=4=lwbj6!moqrC#!F#Qi;{3NtWLC`yGt_cW)iN&qmk# zuU_DYr%y@l4-$Kbdgt!1T9}e}egjyH21U?xeoRE%++U8XKIafa4f%#tP8pAc)%8na z_{)`*@LJx^I2b!WxxK`)1dTY^fg(6g8MHQNpDUMdmBh#-Q(UAfYwDqwDppeMa`<_C zHX=rg8^EN`RE>@4;^p;zwnw4{w|}ZJ9reWzt}`7kJo%!Crta5C9O6=Vn0e(JeHv> zu*1IxUGJZnTAQy3?p;1)(baxY5|>0tw%6>`XX!|;z+tvfNd0tB1&8;Pq^BLOglOZ~ z)VZk=JS#*J6b0!mpkKgz`vj6^9$)weHn)<@Rw}N!xz^A99EER#XRS-{?i}(sx&#OR zhqqejHs#scpkQ<+6)JRF)Oo6?V7pMgX{X0x=zO~2BT!~A8%Cx~4 zvvgokjpVlWx^geb>r&SrOHT?EACV>^EPUQc8!hGWo%pW^R^Q_Oqo^b(-PhwCNF(~& z1y2a955@3tHJk-nc5_nsQTK$e$yR#_?w)WpZlf*#T&=SPAMW$Lv=OhhQE&It3m zLNvs*L1dvxJs24p!@WTf3B6l`Q5IK8beh3J-0n}Wocx!V^xA~bux}$bx##P?-$~K( zl%DHVy~p~@1BS}*96Y~vhVW{2>X2A7zSVhABRav|GCN~{^>r^#*Jk?KNokj>*P|yD zx7%Gzh>A0w;d9pM3ZE?J!`=ojv@$`lyV+G*fQAe`+8FkTS;`CTC8)MGFid&ep7!Xn zrzn?YUx5sIP^-mBoD-&Vo8JaHbG10XD2t0O0VqODuUL$eM`y8Y^!Ns3gVRMS{859Y zE|r@3x7i(}T<5t!c?^z)Uoz02d%XDctf{5q{t67c>W@@6zK7d}?Uuvi^e_n>L;6TH z8I&rzk!0He-i+C3sEjFQ|7Uv?-UqHn=bs9ocmBayWwg6Pbvu1;8g33lU+zbp5V8C<6GVI zDXGCEZydF>zme|y4lsrgr=x}miUcNO3mVXA7Zae=W3c{k)HlyD07QY=a0TmqXoVvr zyK!KKN*L_vHO+#)?YYLS``qrA`k)xnR`ZZnN-I$Mxn>)iC|`c=O$8u@*0enOzV|@D z=hS=dpR(l0ajN(H6)MKB|9rysX0AMpcv;zQ zE3w3ipNF_mOE@%91q9K4KBc-~ox#XXE>>Q^K97SF<(Vu;JrIh8F7L-=$8{Nwvbk=SMA8PEC8Y|j{_SCf9!0nQnwung z-RJuQ)LcTr0s&|4UrP-K!DC;XaaWohkFHbV7%Yh_4Aftye`TMq_}I8Lo_z8+b!v*| z@puF?2KwKxR_QY@&|uPQvA8TlL3P`hppU2uER&{>r&AtjwjYS)WpH^zAechsQeZUk zc(_XN&K*m3;GHfvOQT$35)nJ@4n^GHpDekmH#lvKs#ZGe4$am}(w7g*X?oop8qb!x z8*s-^<=e=%k_>jpa9M-OFc2m>q{hw9dC5z>bR|W7Th-s|letVn< z8*||~J&ClqRS>9^s_Jsz@;^M?aK+}n`$pAU4iDW%*Xm+fNXky5|6n#n zUUY<}w_E6m(eK@!n>9$=U3GKD_CjSa%udN_xu{^3i}-=}$K!|O;}H9T zavO;r|BxB!5F8GR9^sJ7T}mHH>AK(_gj5b`_K?X!pe|(V5@D(E4+ikxU4s%GO=)`^ z@un#tm?_T#Gr67HX2-k14?EvlMnN6R1lYe^%f6Qf2oHU6`BKG~=goo6@Vd(H;w(?N z8fFGK;uQf*{c>jTqkMqY6s;O$ZI)WnEu^j`ld4QOR z8aAV9zCWQoV7BT`i0@H2vt@|F(RZ=!p}2@wQK<+*0A7b7HPbaD)`fE6d9o;jsB;lY zk=NBOyjIdS4ZPRTE{mmV*N@=CD$LDNQW9TtIqSw4RKSwU66sa39d%Amp+zH-vl9Z5 zC#Q^Il@VOv6Z8du)S0C2=*#u~Mo#b3e;Dy97zE-8pV1GfaO`X=y-{jUj0c^HvBp*Pi6f!E}$MCA@9E(PqAT3ERjJ~PPH}U|HmiS@2nK~;n1Q{ zYoO>lOQ2wN&X(nC9*0D|z=U}&z4otG10hB40*!75=)FaNA^nCQq+g?*vSx-kE=tuA z|8Hmsz*t$I=OyHSN3KYZ(1d{;b4T&?s?dr?`4WlUsyEniUfLZitXNkOsN@l+v)>5< z!x#pzz#`IF4L3JPyv3ZkDA53NlKIazc#OI439y)x0%7v)@G77m%v;o%6bWePIKDpm z7c=KDPF`|-TQ!W1BR011|IwHG7I>4kS=ye5NuTxhXlZI48DOrtf_5L8+(v1avj3|z z?n0|EV8xIT1=utf)#V$K1RT)bPjNQilB+4eoI?-vs3*X2yd9^+oRS?%0;F@2^RxEJ zXPJWt=|wAT4Guwk4ui#J=aKk}R@XBha4}8P*1WD6Q@zX>P|G?9q{pGfO-{*k<0**)1t3;srT2tZ4`>BkUIu6I(JXX|Eyn%51+k?1MpckjUxh}N`+Dx+4We%>dA0qzF%kRc9bc^ws^J~suv1v zU1#fNIK4NH10zc7QR)uhL5_4izY>IXHs4xB9{`{&y&5}it<_>IVXfWIT!>u*VxM8yk-Bsy53zWfFv|OZ_!< z8pc&^k)FMp;8=BG`v&6CVlHX!v^pq!QWwqr4a`ga1@gA%6is6T%dY2FyLoL3)kOF$ z&GcFTKNO_HKJyl!-_0{-+pBFHS;J`_;fI$8bh=i((kK@5|n zdL(*Bu&f%8!@h0@)GfGNJZE{oDN&3?X%OMXB<4Z07|ojY7!LE{?ZoE8=KDOO`>w&c zz9%(PM&VNog$ry|<9FFTrnG}PjpSJ5UEnIuP*k>7cl(_8T=Y|DcPs;;5`xy7LN}S+ zdO45I6Fpun4%%ZGjchtwn|G`T*Y<2!sx1PQbQ9w`DNoi&#^Z>|<@U^2l#(OMYa*jEwV1Bvk3`>;2hsGqZ_CT@}Z&dein` z2rj1PT-w?XGWD8m8ymjW7Uz}qkB|7?T^clh#gqQg?^dA}bgsKeis}N7w8P1^STZOT zRmwzX7N_d^83yi=K}5ePl-4O9$22^%m`2U@)=I%eBkglZ%$V^eO&A6riJ_rv?VO%y zc3XThzeJ;mN|5a?*#Qga+yeZP*pN5hjBv*mBKvk@6v{t}yAM!=A5hRj3nw$d3P{Z8 zu@zl_4Wb^O52o`+lPy>;pvhnYLNYnbRia({)?20v#LqB#Pd58u7r|2@jC+=HoKwZz zzN4*HB#G9ay9{X6Zu55!)^p7pU$gi zfS!D>v;G@%tHuo8ays)0r4H=BuNck0PsA@xx_#HvUE=GP2FpPp@w=4p?^i2|-7NUm z>nKsn`9KP8G~}+#IB-~uLR?lh7u9W5Lo)j8mOn$o$>2Rz- zjY}`>$*-;{-2jMHJ3Z%=H*F~4FbrDpO{1(%&kpCK-6Z;~PDz3VioadcM-ipU=HvU* z;@x;Yh^??(a)U8{j=w)UE0JCXxj|e3CFu-`Pa)3ZzYN)ob&*CBe9}o8TEB^f$fplF^mIZRkMDg!u z|F>iPSf8+t`D-QEIJT$^@_!r9-;L=%j_u$;+T|lySq8gIsQpMjQHR9|{62Akm;+)n%-|7A+r zRbM*crs^*JUsoj6!v&>-dNacnkN;0Q|L?#1Cy`%90@zp}<9gt-=6@Wy|2k>)xr^)@ zT?cW-tkviE*#G+F|M+8iBw(W1yPfta)7tp-!T+a^%Zk5CgdS+j!OAa={J-7z-_K7- z0A!PECC9m^(WCzyr~fd`fBC;E5WpNv-Up-o=WP1>TmJiaeHpB&Fx7EQR|o(9V4Xhj zP-6F=aiMad{^v{o>mL3b*r|YiD&aWF*m3?7yZy_q{>Oub@OKdwtU}@f|Gv9_evJSB znF$FWQ5MtoT7;Gg&j%QzvE+V%h-&QrzN;Uo+2lP0|5~hk(81XKt3XrROV0}y(inD@ z692+Qh!<5T^1teWT5GtTQz&f%bgtEtw&TS(8GMg;8hIS)u}mK8t2uZV>#!c6&iBU> z#*l(B>Ak2cU5XV8L-4uiLV0e#?r%BTpSEG!u09(UV8{UUqJjSYWvbHepAls0b38f+r z`BjV2Zx`l!4J7oBw`Y_oXZ*QQk#G1j*hBrDla2 zB}&EC=6<)g*Ozk5`9|Bd%cY!`0el~-jRSQyfF4PynCImD0uo@m)(k*BRfACkxR?ku zgSDa=f&rm`D&mvFHU-em$Um{#pg*>B{@WbOvAtp)lX4KB*Q3+j#L3m(tNJ} zVo!3ht_v_Nb9qN@Cy&NpwzaocOlFO`{RRsPUymNv@+G0gvgT=m<<7#s0*MrfsROSG zBwtaM{s8E|tjUhXSBvB7_Jnz8c2=1sjY8kR6DTFFwOH2zSXPnemM}n}!8WyUj4c!^ zMgWM+(s+D4$|W;YAH6(XJ!_3ee->C@bx8@7RK=0im<$IZC(UB!`*G+8;z$d~EH_=i z^wY*Nf6fG$TdS?E^ZX8_iK_`zDvBv)oJ;f^HIc)tUe982CG}LpV9@Nb05SFRl!@kF z^Fa?06x(Zp-QXTfbzY1ZZ#N>IQeh*Fpu-e{%As6sPb5*BW_AlbX0W?U?W9R9Y!#RpTi`~Zav?ZP zKHj9kVK1Ilq6#=kX~viXRTSUKIQoVJN%IL|Lh~sjAb0IX&tGWQwp2=EauytnT;cV+ zuhQ$bGHucMSA6JC1giGLWY@)Ta1PkxWI7LY<3}@L`CQ=$M#5Y?_!Zw0+Q-L17fz&xW-vvx2SPTKw$QcQqD zg#Nkr_H?!ei#dUK_4*QII1fdUrZ4O}Bl=haehm#(9q&SZ@&bbkzZVS4e?2fq}gOL6E!7^?+Nz=4hD-rG;~{`SYfs+T$IIfhNa>g9&=m@-~xDE zU+ZSR$wR`#w0eNz^>o~9bRt4TejyK7qCy3s>dS>d-6inG^Ss^{czynGPW1jfmF2MQ z%MFYR@9f+HqoP9ol={rN;=bcSPsUPXjqzwmPg4kOYJK32I9F1sg*ociFNi2%i%)lN zr%R2@xnYv8&jAK+Xt*cti%L~j*sg^))N25| z;ZVM0(o(xeP_T0ZTCvuz5iyczNy{@}^m${R2O=~H#bBqTaabF35sCf1#R^In^N$v!d5yWOTl7Hc|(?jeylq1Xb+nz|k z401hNb$@wG7)k2;7Emz1(;G-CTHpTUimk-xsFttb94A^&;Qg!%6XBYw@-2m^FO9A8 z32{H47H+cYC&@>`LZjlE$WJB8G`ipsj}H&^N9$w|<^7VF{N-spj?iT)00Tr-lqGrk zHyJRXdInKIqUu)WQMcKkK9ROv-~VX#yLV6|Eg5rFCy=TQP@1{JhUk4c2BM%a=< z>A2P2@4<9^c74H6nr&XaI4mB-D+z%e_YdP~Y*&Q~_EhoZ$v}Z_k3Ms-1IMHSQZ^mz z5gmie#b%a{v8%L#-z)0=^t7X+14!J@mKrsi9ePPxZ89K|DN(C;MwJDH-kz@zb&7`M z=K*cG=h&qgusc}>GQeW__P_&!0c;i4f3}$qyVPJ4zgJ_A2oNNbStMP?9`!Aw@wpn7 zTU{|n={Ea9906uCp>}vgKI0!cH&Dw)_oYek_H7A>37pG?0F_Ll#bYufMFGloF_?`z zYDl!gb#ic6blNS1=$dtAB+yo3gcvM%24nBiejFyG<;;Gv_%YTdFCBp=$g41tKrI^x zmr`YLJ$Hf&$lq07s-JO$)KCKH)V-eWt66?Nr?JfTJ9HaN$!ISY*<(5cM&L1cKormt zCsN6aV$X8(9EodweRP?QgvO*z4KrXg92kivs(l#k_JUJ0cs~ABTYscNE}K}RkWV!n zSJ*>31PC{b-F7in;Qw3B=fX>dy|r-s3+d(~+dxq;<>8j2{!=9FAr0zUh7bJP*WYr! z%N6g*<3CAH*Bx;*mn-k52zB1-B5^hb9qkTJ`zMzSMhX!qCn@LD1DzlQ?VaOoO2cuq z>CB!V8k^~cZrt9hQYXb-Q~N}q+=NIxDz7}h1$=6NfG;PL@M_KbO@spKM4iP*AueSR z*cN8NiV#n_%NjNV&dOjqTV)6lSp!v&VRdd|%^-&~H+Fk0hD)CKttyd1vCItG+-Bk2 zcO*A39Y7+thtmi)Hjz(JbS#rC6AT8tLBJ#*qdNgP#0mI^duvybc zskVUL|EVe-D70rR!nZB4&C`8|LSSdR#mc6#E0X&V;z`cq&RBvX+WWoGZYc47J$yF%`K)WJ_bmajyi&0)Qs2aKoV2m1=U&9vx9#FL2a&jbDU$|7ENy_}E`xqgX33gY?Q@~Q z{|wIoz5@NEXLt>2Esf5aBDV!r`dMP(r~ZAFebocy0|VylW!c_+^rLtjc@ppPtQA}7 z7h_(Kau)5yYYM-GG>tGEhpG*i1dDw55+JUE=D_OFZ?yAM#0Xq2Q)q?|Rc}&^A^BXP z+86@`BD!#UI9I7sq=#H4s2qE>ODorIL3?(qqtS{}fHDx7QnvE$yIBm1bXuL&>1@|F z3L{wrbPzJsLnr-jl^fozqp{GR2r4JB3n;_rw7I$LoQ*>8Ja68qJJh`w^kZW0GoYv0 z5Zt`YNK2x7!op-onXXf(PN83Eu#TInFs!LE$^?*R?~ZR`Y!)+C+eJhWWMP=JA$V|# zot_uW3?;=xaj9dUiY)Ei?k23m{TTp+T!{f`sfou5Uo^wpa5A}t)Pm2+yy^X^nSo!wp@GwR0Pu)y4v6PwEd zk5bA;NQ}+HDU!zE)BF|t1zzLTpj;XJVp+cR+j_U?m>wH z(g2HqG%3SHa155QmA1FLuVu+CYK=`uSUB~Hd<<#Qne2+{Gddlf2fG;f(^7zZY^T^- zfrC|BM9z}rhoBaOB}N`ye!XPheKXMa@zoavir)+aT}{2&L{InyVksK_fP}l@ zb0aV1O;F)P`a-Me9^fv<8evti@N&m0R+bD0!qsbpQ@ES2glM@PZ$CKzX@t(@kyx{C0xlzt(s>v!ZHO1CMmb*azJsLs#CRd+-jG91@QH^HpGMR7BTi*Eiyq-Q~U;+eHe2}TQ+;*f5lD*Wd62@4vB?v(qH?)A?0tNXOv&oGpytkL_ys{`#Ep-rQ~#v^&>AJB zkOThcUr>c4l+kG7miOb!dx@k9Lc^%hBzlde1;yccvmQfba+SQ1rC)a!h6%+ehy=XR zPL}}xjk(BBoeqC|?JMe@f`@!YZzaL8 zSgFadKxm)6o5irxxk)Se99;mo2O+d7rZpVl+W{;I=(WTkeTPpfr|7nq#qCE z`Qy##!&Tx78g;|AVK>b#6oaS5&zcyxTu=+JNAG0;IQl2}{Sc53iH!RGDNG_%7b$7H zhOvDP{;eA`!;X3ky+-~hN_lqn)kXLmkA;g<*`Mz^0(e3TfJ(PF;>&9q6&?cW58=c} z0KR^lP()=M4`}`?sYNOP`WxV*!8>7BBaMf1Gy23^`&F|~F3i(bQQNG}tVvs6CEg@j z(y74*Qc5%-b&&@F6Z1sn@@U?Y<^a$R>gSbQ0@6Y9TiQ5-pDPC2W0|ckn8%C8jyaK-Pz3eSnlnm`zr~ibIp`tI zM=BvA@XpmI*smF3(UjB9X_}i?fCl!MocJGNBZKjl1gPj>u_%BsfD(ptiA;}7ln@<^ zza*R_x6me1F7H>cAOSynF=6?u1$t3LgSW<#!bBMY@t{;kWB7Yd)EK88T>c=+45 zY6EQ=i=q%S5rA}nh71*0Y?Lpafnc8e_;`JiqcdEEoQgPJR>J%*Fi?;Gouz@^VUcoa zhuTbo_1bc&E1MtFE+Vt(1eHAAhD|;^Ymt@MIU;l7Ifb4sV{$$(o4qV?ml0QM*sS_uO4a4XJq1enzLV05qbaF z3&3AC>h!A~gP$MoH{V`)DJDi!Myq`+@h$o}J@FO|W!dVY?q&kq<`7N3Pustkoh6f$ z+^<4(pIqW1uz9_n864-(D8t#oEL`h={Jh5@&{Irw44abBPc*OkXXW<#*@As9qm|`a zjxl><5ePUMqy_O|P>!@5>dz^?)V~nB9tsee7y>3Ue=#+4qHJ%A;?PX1e81-`t`X!U`| zWPd`Nv(fkLemH^Ic9x3eCkWh1;p92Nz}9)!Yx@+nFn(`_0*{qkXGmxY0mmgG)mjgj zmM|&G@D6p73@&&goK!GiXT)kHX-q)lm-~pWZ~I42O^%HXx$|Ii*t>B)+R#n=hI~Eh zv{v3QMvYEX-91PCwW$GQ(imLD?#o>|H_y!zdvG~u$UX82gWF+wD=Vl)QVVfaS=a?8 zq>z;u+@Fwnk%LNrH-BmUTvU=emTX#$Nr z0u^1`v@nyBmK9O9U^W(;X;eBeGBi3ZdXuZ#0C%oLv-1nkBd7jQ>GZ+U^Z>%;i=R1d zmCF_+jZc6el%obf&`P~uMCTFYr_=;B}4@T6p&D9Dd|Q^iJ?Qf zlujvW5D*ZMuAyt_?rxCoZjtV8IBU1=@7sL){5r=UUNg))YuGT`gv&6+XPvF^j!v z-cEnR9$DMh?*$%9jvNVp5GY6YxI)+x5PFe?Ro%mvM{JM=;e4k(?{4MByz*f9)JgX= z=d9s`>OR(;D3V4LIe^<6`UU`Ovg1Jl&7b9{l!Cfze4j z-w+pwe##@fm0;2}lP|h8F!y;m8 z(hoM^8VDVub`Fq48SaZneR<{o=x}x7b{aH&=Qmc7dM5Ehy}F*cr8|*XYIa!-yxwoK z(CRx;rwt75wK>2M`=dV*o?!29QTBfLV3zogyn>4`JQF7sk);!m0l1HyB%8h~DLF^+ z5mH-XGNo5g^^(+x4FjYLyYM7?KqcGhT58XoZzrz=`eGpgbu&?`_59BEzK2=n;}wAz zVj^A$0f0)RqR@qFWgwe-u{(qs=Yun=>%1{O(~>>Pw6y5Ac8tpi$1wD@kKgVH*Tn&- z`n0&=Q{0;mdzN?f-1n{4YyP{Wh!scctmfyI$*4G#cCuUahl+)d?ZR`Wx1R-9L|E!g zs4}ITkv4{Jyl}j}tXVw+gc!Q?wwizZkrJu1fW@)uPZ2%<)<^CYX20g3wejyPM z+=D)~&v`=7{Nn9bgs5-JQo&V9Om@3XBN%KY)NZq$NE0+NO#`~(>=uh5uIG0F8ZCku z2OCl*QCF1St#zTkqm?2_|7MCtHt#hRttpagcYjJ0i;6%9FDhn9HHBpQ_wGci(q;PH z1!}OyPgu>rHRLcGw@(5A0mV@&?5A)O7-kp}#f_;mV__X}CD?kxN82=!zY%P<5!_pI zIB7I}8lvigR(0C<*ZXjY^@o7NP>-_#wVyk{(kIuKWj(6{tq=bJY< z7M%#yBZX|Pw}0&4uj?@2C10VhK7z#nh?4cjwD@@dE`kx|{bU5wEpcg`-3R=TH?|Y4 zK5JLEMnq(YQMM+lle$W6aj8fEE60_qllYM+6Z+kJApMoc=ZD8JG@3B8n63QeK6+lW zP8idkg2!RrUC zT1)gFZs!frFxv;C<9_-w+L!&l*}w-or{RlYi80l4k;3wC8Wyf$k}0H#%?YeA_t>X5604_AoVJ z3b-d}+15oD0c^>1ICJLF>j>+8%7v2ZsN~8l3fkG~J=H7UK z-eR?bWo$2V-w`tPKxK9p%O{FC*I6Ogs>KI~=Uv7iDXn|tGYwM!Zny=n61+YIo%-IR zviCq!+Ne0P-kGqb^G!DD=<=)tAg?T-xwp>zMO``mU~6043_FsyToTT`Lach1Dk56mQkwtR?StAu9ZB-uKSCxMMK2c@?KQtP+9%h6&rJzpnOly z7whR_e}pGo5Izdt7+(ibKfpPprX40KZeJg>sF(go8lM?4=$_wxTg0^GPNZLDAcL++ zTa~VE60TCc8tSpuRNAYA#NO!y5n{}!66=*o!~Ids7Do$*Zoq7*KX<0rzBLSJ)0ZaB z_Xww0X!b5=Y(}ey?JQZ?4rXhdBg1pmaB)Coh9&Y_)ht5359OafWx{^==I-#u;quTU z*ZU~;58N0qqjBfTK3V76(B6u6KDVXI`#eWX`1vLi`aB*4(iDMg@q0p<=nncrMl*r& zBA%p1kYrmKEiXQa%_Mq*N#;C-H|l^@<{U~%VPZfS_FPe;CNC0r^^Xt9Wf2(SdXYrJ z`;eP-&5;m3BOKTauct4xuO9B4k{I3%U5P-r-xa|u@eyCEip&16sBXL&w+xFad^VH% zMjQiM)#JRIfnYDiy_Es94LU;BL-gk*qB(U`S{m>xryo2-*X@dAx7^mVyj9_CetaL5 zqYV_nz<{?)Qi>=|7u+sCG;N%=QL=AAOW;Gbz?J{RNB^lVK%+Q^<}l@zU=~H1Txy2( z*!$h&w(R>zY=EgzH`y8_m`Pvgwm~vsYXfCBPV_njq!h9wWlI+AR!u4>stMM*;}bbL z91Gw!86QBI3=Hr6Wmq8j-VhT>dbO&y$zg4*Tx}QmGT%Rr`^l@VTgFRnCsN_FvG;o8`@0IA6^j! z?=AL>gA7L7;`2^GNpa;*-hcK?YWR5-HrU#O5j14?d}eX>IepsN6DTpqjXcq@jpxxb zDzZ1kz7smt5v$39=-uKw*>ghUIidlK9pr)vOH&^0Rfnpy_OSF1o0E^mNYT0rk!ai% zKBnqc1GIO11jcAVicLDk0wAgTewEr~1!bm*$^s~{8xWM52Jv!eL>JXRKcKMQo`pmA zdWSBTId~_*O#XKSma7@xjh!9mQwrw9zz;y~<6Xz;(bh~RYU`#Rh@3ReHNLtR0{u)9 zjks%%5A+QWhx}jkMv}PC15EVI0r&9ds=cL08V_1%##AedWzekjW*`kEMiVepKdoAc zDRwmA3$HIO4p#vM$50$CRV@M6GIDD_XFUh0hkEsi-libzPODlGk3yvOIoba)SJsPq zY#KLD|8rutWx5hfPYBJs_Cet58kFV%MicoRck;s#@1%@VpsJT4ci>)r7gi<={Tl7{ zmT1Q~1W{Zr-#Ae{u3oX=?HMR04z(Xr2twBs@-+ZbC(@B~%y5w83p0Z!>RUWafw|8k zeg}ZK<_X=fh6a&{B#lIH8cmf%$~V*5QcN3j2wx&RWnp?=+bG_+)Dt6@WJ7_RoeUDa zueDJuS9Y68o0TF?mUs9a=%>4b$>Q9FCv)zN>S-Pj`Cw72;18sdFD50I11<++n`5a0qhmHEgyv+GZE!5lHRqIKrR*Ns29L`&5 z7FxehC?0c8pM3rkf%;mp;nxSj$p)_%u4@g5VK%{8zIylgtw+-VpBa*-{G4Pd8#K&@0@CnuqJ zc5;?5<9_R#XV#k%^jmb~#IeOxhBx_2jOTNaC88hb)rA3=N5{v90`iYIblTfPMQ5s= z6|xQd&QDNYax-Hnm)oVQ=D+gB5DYVTl6C-u6g9-%!kzsLV)N1xrT}YnqS)%@93Xmh z1alC+m?N`U-B(vGF{&Gv^G$k7{lF;f%y~~i9Y8@)OPNEOUQ0ZB3e57)e#M$RYtz{h zhwJ4>>r*QPGu6^51`m-Eo@H!}7poSRY0Tk2_iLd3WP%TrInUUuKnT;a?Y##oPVkGh z;cQutjX*CP9=L|?*_)ZG=YoZfgj~-h>ylnpllBjT>AKNJ{*FE3S23)+mu>lMzwTMC zy}~!(Qkq`7k-PxN^H#~>__#OSDSlXSRJv;J1-y?`w@+?d5zD<>^hNyEG+ZM?wJ|17 zJ>ip4U~*wld?_)6Vxl$QQk<<^9KIs6%xXq}l!HJ)NFq|ypVA?WyVf!}PxdwXBs}gQ zkT{^+^on4uNl6MdA5A7j#O?DoL*s0%fO(9c%vt^ZbNtTk`W!dSg;ir&U%=tL`dyAR4ICfsL&R z2LQ7*n~8O~e!{RkI*t!(BnMcf_wxWz+eb+EdtzLJ3Lb*Xgd_D3#%wa#@MawZ1uCpe zb9X~~dY>gd+n{Ksl_tgW5*#sktFaFlitba`{n80bs(-*q&I!5kHhT4 z+&4BrQ23B+r8gq?7WdraYIw!P&j?7|03nu#^gdKt3AV%!Qq3^|GFh_z%JVKEYy1oy zxm|>!T+~DYie-`0g*|yT0vjn7rh8LeNG@X_qkchmj2NTmfsa-r!=|tXTS)~)8F?@Z zVttGK;VOb4Mc!SyID~bPI>dUyjd9*%jLT+1(nHTdwKC^p0W3i&=gr?J2UnN$90Pzy zQ6*Jm1rigLn)3$6UuYh`=9d<0$bKjrNoQUJ0;beIO6X!sWMJZS z5s|A?rJ08-E49Yn<}AJzMTu6AuNE~+b$hx?HlU5!4@HSDPnMYEzjjA5IBo~j%*cO3 z|HBdIjYr5sf|V=WG_puR>-<<>=M&xoq$=sH*UjWGVL0pE9{g1Up@L|hYF!hrBfW!L z(i=!I=@5&JrVyT@q*TWfN61xJo7w>39@0#Q;74Cs>l*?wX!72zwcP5X=z}vd8ZNTs z+FPtMtD3JNnOYo*dER!Ax< ze5xPkj*6B!0PeW-GmSD-$Zc-TheeHEDOiRx`=yv)Nhfv(IyoE1M{@Xm0NN+h-ZQg> z87`)>gv;jAx5Q54vv-Ma+rBQuq6HCv)k4JF7USuOZ%hOxIhPnrAh@x(6#{7`Pjs&c zp4!{Ed*$D?r`4$w_rG2_k8ehP?T-}?dj9Cyym|pM!`FR<1XmXPiw(vNkKFDK$y(Tpi>8|aSng&|App9tklc5fr9uk+g8qItWTeuYc zEohZQ-+L2oCbc@LA`PTqOv2rUQT;q9*BS5d6CU3aR2M; z93GV#2Pq0ud8GUtl*6}z5lyV-^V5Ld4#3^R$oZPocT_>79EA!t90m*ZKik#zB}B6h zl0?jtKU&&<6vu6k@y1FH1NqLx3!TpWlYvyxaC$qN{oC4uMf&}M-nd;${kBo(pP$#> zh|DdwKMPxslY9Z_6vu08XRL3E8*DXyNp3Zq@W82Y|2m(6KuMD~-d6M8lFFxd${DW0 z9orTyNa(6LDm%zxuNZP85J;J$my}bz-7WOQolgKB^@93tHI7#`{q1}Ba~5N_m233v z6|0r9fnv&&+f*+f;wgxJ(*NB$2mU%z972}NJAGNyxLRz{94OR_B;?Jf-?!vnJh(b` zi2G!8lnYe@kI?s6gKo!Q{H_u5#!%h}(RA)7w zr{|Fe$m&(IANu^z)GxR%%U{qz>|3bzfu?@I5AQ0 zjcg_!N$;WF3vV~PV~h~X<=CHLH6KjoZ+PU_$V@kSl&E}kSd`{ba@KtL6KL1P;up+5 z{!t((cl3MW=2x)p7qVI0m%5TZ%g3L_o*=u_?cwy?zO(8l;QNpV_BwF4{=)t%@qfT{ z^Qge~Z`~|9_hU=XX0s4f4se7(=(4(2bFpGJ%X+S%oNhqXI^^d*tD``Y#5Dgr@zv+I z@X8t}!>`SPU8`lnX@YtsN$^c)U% zs9`-@yYITzmj?oNmI_I{N2c|A%|Qe_6+*ase+Z*(_MngBV_fA*2`v_T69ph7pe-yANeT-{V>u>*WU^ ze*T_n4T)(7t)DBwnV$8yOGKxw*uzNN=nxqp82%Zs{4$YvWE4AXLIn13IM(NAEJ z88rq<|MZ1_c+ltwFhb%E7Pk8TrXNd)XEEw|Y7&i`q)`6;vp=n?KV}6hAzU~Jl6Bk2 zRXzDHv+vJo_~(l}Vz>_(eR5r&I4M5=i;=2BMy``VQxMc;BC9H{diHOALK!`PYiK!3 zhblIN|ED$o-#cK|9bq|r(VZnUT;N|^t)UE0V`siETSzS@^7PM*>YrbjQG#d`uCal* zm4q$*f4_mBmpL=(jU*`H<@_I5y#F!AM0D_U8TAr=^2K$>z?l0VSN}Rko|s86U4mM* z|KegIBzT&`yO+zUM;|Q!XjI_Uhd*Q7e|+5+sjQ;nNsq)nN>ZWDi{#I;EHb{o3#k0{ z6Lk?TLr|}L@pz*hFI!gew$ju2 ze0#1jxYd8U)?QbzF3ik|>_h(FhVvFeyZ~?Lw&VVN1CRQ2qI*(^Sn1vf$e$7a>W5!D z?GBQ=04DwYW`44#vj$t+{enZkh@_SuM}ktcs^doJ<@V_>R;Ln8y6 z8pnSuOcykedc8?^Iwtu<@Nc`*pO5p?3zuMYGw*nYv_7`ZHWMn^e>2zW!VpyU8@X0 z1g6IEs|kh_fqyeV{~vWrI1;)sZ3xCsTgD$7(%)9wWzV#ui-J^~ooB@!X^S6Z%AkK9+{o92k5drpS+nuzRV zi9=yp7yPqv7Oh$H?diS)Z!TQd@sy5^X6oM2ALU&Cs4sNmhk9h-Z@fjQddax_Ly4-P zw|*-p$@>qL7QcnK2xY7|JZvOdVl;{yM1hrbn@^Lh$jFU<7b!2G-Zd%S;m>fi5{{z-hY zQv9Vq;kDKDKutj;30`3)KbfbQ*s=E(u5T0RLmmwG3hA1{cfZ`dmq49Q8{t!QEta2$ zEX~`uc7JpAPWsnqJ$1puW~?gosOe3?l2_D_Q^T zuKs*G`?x4sNkhZIWw|#&29!rg`j86b!LJs=ZKbmCJu_2;gRPIZX*szX-+b6w?5YE` zrDEBw-fxVD)?OSMYRq=AuGd{RJ$N~Bwhxg+(mVoRc-zyEh=jC5nd~2Itp&dN93&eo zmm!-DvKHuY(0EFPobi0^k*%62HmCD&^uf0{*Y)ab4ZK=?I03CtM)?;1=L?0+jg6@y zeV@Rmt<9IB3nGpe2?Y2Vl%ASm+ru(R*DM3B>Pfo^4MA@Q?n74TXXs; z-FkJTqsAZpz}-7kmSW4Eza;9ww(b4;@gGWax=8`0Xm&h4xS&o9d5u)a-$}WDG$(Smhe2 zx!{03BdWV99#MMCW!LH(14JSTm<${~A`o9S@LV3dfl#&3PJ+XJU+TLKc{;x+vQ17V z`fKK96TfSK;@P*km{%8JfOiP9y&F2*FEL)UQenB$S8Pb@SNtN9xOjekJ_*0Gbmmez zQ9yrU{mbKtPU6#x@4<4$Bw`AAE<(o{w$9qx$VM7c#2mtckEBco_y{#*9!-hTLO z+|HLDhr8ru0@y6Vwrb8&9M4MSy50`TrZGmfWDaG>*>p$8#oEGR3olJ;FC6AutOh;= zJrh1VBK$U)2~u1Mlq^_)l9T2OMM>I~tg5Oaa4ZU{UI*mD@}f@CYdZuk$iU{PcWVJc-v;cLI%Oroj<#iqh712{5pWh)i1_X zC_6<2>+nt(E-HZNbUvVvW?$pbcD39*zx1K4F+#n#xZpX@OP!J>$alI}+JLoeylW2` zGg_tCU*5^_*6obIUExnxK2kiBJijpQjJql{4!dr<{g!=0xVX}OUr7LRo1DH-f8bM` z^QGog#Sk>HmpP%Rmnyf2DJq=yj9pjHXtFFaTgR=!3UBno!Ri9@um)=vwLXiZqoecj z+yuS0(DOIjneRy?>vWLJ>!CgK$f6^tC_8az}htw+1g?(8mh=_<2Y zEEa*Mec?0yU@GvbC*!^3TYko%0H^Hn{y@*hGGnOTi?)>kGJnFW1t=4h@m;Y64}<2DmLfJcRhu)0qmntP0!h zAru|%$O91eSDvzIlKSl-xn23-$l2;k%Jl}S8#y}?-FKo%nL7iYhL2^=1e5x)O^rLzh5q!D)7eH3bhI~wJUDqW)?2p=3M-0?RFPtkHi(bHP&nj89LnG_ zW^ul{z`>i*jZY^<#eQytH&$&Q?x*?@aC~a^y76dy;`HA}JeGr-t|@T!Omv#6|QY;bUayL1~)jgwN2@+ znByMS!s&5D0ZN@lyozkNyf|YW;1jW9lc5E=I0dbj2P1^m8j(U;a=B^)o6@~$QZkhl z5jrN5wa&R?6UoA;$`Ii)8rA87C=_t~tVSP7wpG0%bnKMvrF*W+x0VvV838Aa`3xo; zJUlqKO^b$T!EE1?j~zT9qJJnf*bQnsH5fsT`V;t-$zswM@gKqPM+1mUQ+s7SKrvdq zv`Aw*=Sz5z*fGeC)4mjK<;kGTQ1Prv`FnpT#uT7>s;;YHK3A&(u=zF8FUJYwh|BoH zi~V<_t}(do=OR>TolKSJO^Ya`B`ZbPGA;Crl?xSeMd0lm)#ig8j%Mj}uV=Qrqfl3Znw}_ZZ?adLhtQ0ci$7l6VvTk^pawp z%Lu~72jNr-Lfeuiv=K`VIc(H zv-OU17lysNIte!VuY8v>I$M;= zyIQ(6mby2r2iTZ5w`y_5&Vz5|htjI|$=k2zw=-MEc4G_SlE0Iy`XGGPHyMh%y~HUp z-)VQSy87I_1>o7uOrWel8LB;aY!|?7#z!ATvs*Qo7?cR(Ma?$en;D6YrttvM`1mj0 z#)~G56)_ka44g<7eTj+jlD_wDu`^whK%DsclVIzOQ%`$X_0jZ1oD(kN`Di;Aivi&CXl_Rl3Pxjg!q4)_g~kS6QF{ra*YA^Y@TIqro5#=Y zb!S>JhL2R)8m0_HKPon@DPQv)zgZ)*6r^Fb_UUayhJ3arZR0!i7qrREXsTx_^qTScWVP73I#BFl2bYWVmIwg zA%uHJAQUKiuv&-*lTLM4j0q)T=si=cwksVFmtuR?&kiiXBC8-$v`3=4qDHg$z1Vh| zB~`Y2EPB1Y^2{%{rU_Y$j>SIM*05RVRPBo3G3+H10Le>arXB4JSz=66gBNXQ!dw+9 z$k%R3w-Ks1;k6w_s3XSM)Vj(H^#-dUbH^@(Q99^FqVf8Q1>?nQJZDBv?a`pweJ!MB zU)m|3KfCYy|UBhBSJEEQYlC}GL zB((yt#lD_ed(Bs32kyq9O2>hRX2dlo2%EPaC64qFy$0I)jBuX8W;-WVvyI7`j zHEcGU&%@jAIa;&@c^u+0Lf##$7O%rTzHJLSTHZ;Z4vercQ{J4kjcL3Dr6e=jh@HUvnU-2`R8SF-)rfY_(|!bENoSY zOoJiZv4VI0@l22#%LRjq2%$*@qG&ZNyZv)MSg*zaW2~Q3w)cZ70^ubG{vu&3oN^LwX2jdW4&ft?Ezx>Wj*$ zK(7+0g_^{Y4jT*<&KL_e_|(E?0h@s6_GV8^$fP<&5xs<$IX<2bJusK2%g@c-a)EQ? z3(tIeu!^PB(e*6M$OD`P`R&c%izi z5v(nt46fr==lx~PF0p0KE#Q$g;=hHlXPk6%yjptHx>J| zy56^(j3>*w6*26;3Ho6Bf+#_b2lk=|MG{OUX)48=FwTDSP|1A8iDQ6GH>m~CZ!5;+ zH>a!pMVn3@bhkl&Q4c(-|bp7J5F`-c5noX!>kh z-Vtw-vh1Li=VJdNn{L@T6%v&^Gk38*nxAdE_eta~TW*uAd;w%n$RKyuVD53wE~r+) zaYUoE?`gsCIluM_O_||aa(MRQ9l+6z+4EslWHOz$0oo0~eY9ULTxPC4{#KZyI`jUU z^LG6!>u;~*9&%V0?NxmS)H9Qww6VuCj-{TWh=nJ6J$um$GO$V4#Yw^I!X!J2b%oL? zWSpFwpg;y}2A$V^9W_bhp&)t@rkz6Xj!ltytI+c4PIlA8#2JVXEp|oCw33aT?CBn@ zk7WyvBz_E_2W0Gl#c?PEI>=+QnKk8G{1Q+>J1-1`T7$_X$h)l9NBbfw$91~Yz-3GW z;DAY4io2(z<#%oLtxm9xgEa|jbCxoE+=%-F99n6AwEOAgM!Ir|!w|vyZ2CT<~05(N2BkQKQueYK{kfx6p7^%fE$_LX;}M zN3^6-gJoeq_1af$<7?nN(4Wb9uGAiqzU~bajsRW&u~PVd{Xkn98`~t8(`q`du$%L` zAGy_j8yVpk1U_3zAx%ig|9O*GeW(P@fnt&IG8MWXu>)Y`6<52OPf7 zE5ZHT*O;bdl`gs|A`+_vfC;*YKG$v#yC}qe*=jqY`GC#l74GfF&ad!nj0EghvHsC8 z`t1WReYo6NcX_bX!BO4^DFufymK4BK`xtdMF}6|m zksNqazcK#TcK-U6vQ(GwRL@j!Q9yE}&5`GjAhw>0duDCKX(M84-^EF(R{Y z$N{qUyuH!4uI=eMN5t14@?ALJe*F|92yhAaT)N4Qnfmg&_|<*hItU^#cyCVa_m>;} z^D6xH5ea-j4vAiNqh8Vi`}#|5uzFuJsj|BbO&*+>PhH>T_jv2F(pxl&Z}WCh{EwcL za|txqJpEo~a_z6X-X;YLh7=s?bY|l2*AC*W({I_P&7;|}>keX&RJl#||1jI69L!^P z3r*gqK287x*pe}L{j!^;(w-f*d0LmoDE|1zuJJ;HcXf4D$WHY-L z88`$jzPcG6y$T=uh~_?gt-Jo-s7XIL173~rN0XS|jii%M;7Wd6RPY_EH{ku{u7T<{ z{2y8XKX(1!_RQH~kl{iuo)C>FKj<>sx#r4WzW9HYS|v?yd0ou9%UX{1<3s<)q2d~G zUjtGcA(`^igZ<}atR!_%1M^k$_YBwHBt5YP%-PpF?8ou=u}l9PBi1Lp($rr#Uz^v2zqytX%`Tune2BB@X8#|<|JP-|ydr8r zsN&+};+iDR4tpZlE>Epa4NPcIffj>Qj6A?_BnJq}m|dFkT2(;>*?m9$-WF_V(Kh2lDu!>^q56?H?#tHz;v^JM zZk^}6FdkFz9FxCG$U*w>$q$`h$AvU%y0tiZ&(4P9!!j?zM%6&RD5xE1O~XUR zO5yvlE_TKjJh(=OUvRk`y2j-+hs9z!o72#>x!fwP2Xw_w9P4v6<&%C6g#qhsYt~oE zs3*h9q0pk(fC)m@PTB_2WI;U6^`+D6Dz(!Vs+HB*_)(Zt#!$;&q6H+2yp=54En+9T zi@9%hc0i)DwlARz?86&0h#*Y%;sEtK?f2(4-h1EM!dcC=u&^nQy*hH}ShUl|=z0Wy z3a3u`yIdTa;Ob9T4b{CQ{4^481HZL8WXQ(x7;zWpPVS2ZaX8&v(BbQIBtt=@5GY_| zT4=;_+Qv}d|9OqvlLn`)wLQ^@8>fwI_rk^km&0;6jiYS%&7o}Q3w`Tu{Bo7{r&FCF z6nmre!I+?$sy>ABhlvP44OnTCHChNq)A29)b%m%uLf83zo;zlSuA|)2*`nbDwtLe~ zSbZZ${RQtt84pB&S0eI&Rx$at`GDiW&d$mC9zAVszW^2=k3Wbxn~avJE#NMMvB`u@ z+U{c0$Ts1p;Slf;atqB5@ev9l|FkrT;JSF!57Kd+sV^DC{3@()-4*{Hf9A!<+`d~A z_l8|MTP*T@C?&T^vu;-`w`0rON;d69S;#i)?jF683B&_xnbJ|Yb##&EnXTbwwKj52 zx5ZJJPGqxL6^C}k=Se=yd5Lm<^E?LYZ~1zv-Zaf%x?lq(Jw;57QG>sSxy;`Xv8H2- zS{M)*PgH5HsJ<|Oop(kKm|E&_=r`Ok-Eya-kSp-bCN$4;zM3|zjXBX8z|MdX8!oi- zRDjF}Iw1z$p?sR;O1_AM9!dF~6^6+&^ZCj0!c6(xUXdwKMD&iMtx{K|Rpyoc0jpoj zKxQ$Jb38lTig~fHh7|T@gxvyy%Q679P@)k>?ba7|qmUs3M85Nbk!k5O;FFow?%6G4 zVo^wEorqEwzU|InSoJm2g;p0O6lw$ZHbsRswl&073d*Q#*A{^txYCF`HSlPT%bW*9#M3r?g>y3>1GS< z3o~L-Z<6e&9S2c+M=n*bHXd>&UjZs!kpkvy?eJz72<93VuN3O`7)>2+73l{9LiT1q zbWGMiUO#y8v>}e&3hF*R4bl+oW@ayC#+$X%VxcQIcg%+tq#3!4$1N+`d~j$@CW|hk^n)8J#c642 zQ)Dyc9oZqPMF!h|NK#9CY&DmNNXzJyFA};+IaX`B6`*;OKQ41|pl8q)<}_}(f=MP~ zdHgj2O!%vdqeP?%?F8j`gdew3ttgiu1CUkCr_WrR4}NYqY}6upA-438i7fFnm`a|; zdDokgQY?VzWgA9HQbaevJWokz*HB>!xs>hmOjdk!ip`FqZzy?x~t3;7GUov9L`?L$sy z7dX1**%}}Hc}@o-JN9?lB7v^5Zftt4`^*{F441=kfT$z$59g8DQ&&Ls2`48Andw(p zgF)0z?HUwlaITu>Lm_STZnxhr-m!^X`iO|?eA=d~lL3SREz=85&$otH7B5(B&PUYQ zqlQK<*T>$ceFt9rLVK73d)MZW%~s&kmg@cj?-%@W&de=Nb0Z3ljs(tG@6YZU^Dhm_ zD$X2MS9fuey!1=A`32wbii#6<-uqRm(dZ0?@v+y%WiHjACDGw?k-T)i(>sr3Qbn?f z6uKw1Tb&b1FQPrEOeWJJcO6gwgn&Q|*1iy6KRq~f_c5pR7Wz1L5b1%GW@%2fQuS`e z;>ifpsA`)-(82mif^B?>BxW2?EmIxK&29N|c@j=&RF2G;+qRvMGz6m_;qW+3Lya74 zZRJgYV1^i3T!Lf5@_dwHHFvCm50S9%)k+JrbsW`y*b8Sg>84;s=TZu%(D zA)q~f`aQ{}va0w)+Y^STei?GP@&mHWwNX{u(qYeu-QeYPTe~-)%?o?z{3TnX87Kym zieoL%jqfYC#a)ZQKe4MlI5^C{ePe3$|TbrmE4j7i*SiFDmm~pc`R#m%D$nRn1(YBcP z;)ggU`|<4_?y2)FA$x0*Ax5bLuBJy30<*)}SKTMpi$^@2idAnD4!dKyx(Lq1Htf1j zKjX1D^@Ztef({gr%w8S9mLJgnoM;|=x*8G9WiuQtUZ^+v910uA`-)q0*cw+`J+iUk z>c;)RqnbJVTr2J#cS+l8{ivEDcW-UT+2P=!8Ri{ax=!m{#ssg}7C#l!tR;#^ zZf~VmruoSBD8}V$lNyq^JbiU{W1_TG#TL|IWlVZqqDKuUQ)97gT}_{{ zLRg$0n=l>5Wwn|-mSUp~iJy_FjZAxo(Fr?^jG4CWA-Sn*oo+vqXlsZWS8G-ymx*(6 z^P}A{bW18ihO`SsEw1jV>9j+-bjk}+?uhpDAN=b&KLp0cs$F5|dcBH;Pq@;-=EPK! z2&?{yCgz%99wf4CGG^LoK+6bbHn=`r@HXD z2&xq|@0Km)uAZx(c7?F%XhcFjrK_BLZSe4k=m_HvKVl!(mvTBc-;AIoTeWKGWl3?y zI8^sIHMGO(AMC3QAQ8pxhSE!LQG^cW$l2_M>?GVv{rLL ziyE}A9KfsXzGq8+SUXi&Q9UDUs<*1mV6Yf4V`?>9te7*I1d|+N;t-xs;JFM6-7AoxR_Ut8Tk`C4Rc?T!nNbXUUAM%f86$6|^4@kP4oT|MDS516N z5&WP}=i;6D9a#}k@nE{1>$Ej0iPz*7}Vtq15&?n4{;GLEu7Rw{1!gOLo zYmYooBWa>2;E-}b=av7MY(P7q^LkqV4F3GUDb~AdD>O4KR~2V>H&LyYO8&Vo>YKx! z8L)|hwzhO5c}vfY+$M6Dx#UuykH_EHlXnLTnPwG5>PsSv@14ev4MHdV)Y2xhY#Rx< zD)k~_C}atCy1s10+SeSSLhx8uifkHNtYBXjdy0}ySBD*kAFLip@LXvy7>vsv>3+>~ zwa#VG+jqb$OwV`2Or&gr*@@#|yN@F*aYLHf$R<1sHb0$wS zMwGe3c863Zf=XfYiQwm@_DCw-?nw_+k1D-%17VEY+4wnxy5Y(f1~PiinU0Vb`l(|1 z3rhKuEg)Om+iFsQFjk;rL!tRY;0W#rynw(y&MJ#h*%F(3BLw3RtKDJlMi#fug)I1oK$!5E4b6tyPV?T(H*Shet}B9QejM z9G1nnGsl0MDYyz8(I&|%V<@CF$}mvZG)k2+OTD%wrOC*^DX|hh{z{5t57JN*?)yn5V*cW64OOlP} zC06E@jQa)BrTgsHp40?;3qPrGBw3VH-&$>EX9uvl352{3HuPwF3ERE&| zMD7_A|74Ohy>}huxR?h>-ZMOa<5sj;BWc$|kTYF9I5ZwS_}s#*otO?B{B(;nOQ^nz z_BtrGk?n7np~R}nHP?GFWM>s;UsAsp$)Jn2XRNp+sM8%XW@0jhy^EbnEoBC>cD=&I zz+awV$34-V?{zKIS*{NG!Q;Gth6ot)(z%<3ie&0E}x2 zI;Y7{TM&uiGBd@Gv3S=vH23XOok98Ih+-<$3}3%3VoBQ9n5hwjRvW@67?-3$+g?j{QdkE5X9hQ1{EhO#dBm+MXxhDu&) zIa|kV5y)1sG{{P}FuKkp5yd^$Li6&ovc-61h-Z1^bH!b{j;(S!+uaE?1EHqgjb&>7 zsAy4)1=C|#3`lTB^46;Bod-D?F2`=TkfZ4z3DMI%J#5H|W)vQ+_K#vQtr_23X!gMw zucY(LEDQso6DP;pYy(On$&hn9D2xs{M#_SXGdWQC=H+^Z5zPp;*&a;Pn;G+&*Xmn3 z;owinGkh$3KZOFCl;F1FNrvEWYR?^99E>!4>S1BRk^dg3g|4Dmh@(E-D%)Fhkg!B| zZrAWIe5m<}9)#^#a6HPM9K<4i+N?+vQRQ&YFP^D#?J_Ec=Zegc@kl$AatIacc~)_+ zQMjB8tM$tHWu3&wutzw8^sVoaQpICbitWlt$xduK7J*W+GshX3y)Y>N8<$aoz;#^M z+rlW)uR(0SEaW{iN(-s7Ifn%q-NG; ze7d)btH!*f_zk%)I+OglVQhFCpZnP5_lz^#3N2T(njO#fOIH{Vv_eSaddvZ~5So)D zbTGbgIWisHaalqpeOE57*OVNT3>ud&sV_EE=z11y7i!Vc>Gi?~1S#@fq5rUoK`IgH zgFOPtV8|OED<03gkGBu1Bc9my;@(gy0Dv&9i4tSvJxV4xpA8h?UlVt-OVJKK`L6%b z{u3~hh6SD|iF5Afqpk9oqWV8ZP5L-x=(cPi|ClG3NmeUYuA>CH*cS~@o&BWb>YGxS zX(J=~^M>8tA}HT!^>SNpogHlz=d&lGUPhR*K(@a=EmhDetTdt&-}SjX9mtt9p}POW zo!$>eDEj=SwX`C_L3k*G2}CM2H%)+3k3I?eB&|=$$I#InnyKJ)zH){{`Ra@&Ml}YW z#C@>f)~JU`tx@D|$@bU4M&>VC@u4kh28W+14cl$HZs<$w9QD^Gj(*%rZuSY4Tzb;^ zQhO+a+Zb=Mmgnu)c;Ls#^)gb`D6Wael!=?xDirCN%OvRdjgzK_aVV(CygyuzEV(kA zi;n3RCt5MuTh33iF4Sx!EnE^SjY46hef`UMN|I^KebhqjlciNcV@HV2kxsH4PCSNqV8KX`?Q z6IV=SeDY#)PEVOq+gA{%)>Gbaf({uNY}srvXr32sPI2dNXJMvz1J8%4OSLkV?Sr^k$9Od{^R|83ekD5X~Jek^`=> zl#jPwJH9ac5R9V+sskTNx6)W`>RMdY1Sl+r9NAR}8SBbGg7#)5b*&%0Wr?$kta zbS%~}`u5bUuyUQB(5Wg7&sn=i>(s7Krbme)8gm0{=#&Dcaxh~0#!$E<6Q@K$ffK%4%&+bLFfO&+FQp()i(d*f*_@WA|N1L(nyDd zpdj5{O3KpRB?2lX-Q6u6ODRe>?9wIOQVT2#d=Gg0+;~5q*K_~=`2FK+eX)Daxvn!a z*UWp~GXt!8LOb2#h4T2xX>O{usmP&E6qm#H7PKnYez=WM4Pj`&T%*>gC97@qmX5S^ z@5Izl&+=3}{cTw7ronZ817%9I#VFWd;U(9uY%>4+L9yh^5mdOxacr`HWB;S4#OZ4p zG6}qZhL+>It$z09Zd#e!z|bF0dEFKqZz6Y*F0};8xtZe?6(J*K70*&Bm#&GU3vIf) z({MCw0T9nCZ`HDhdw#~mV;EO{+?Xp`VV3JOXlI-AoV6chY^SZ6IiTY<=oGszJLG#7 z*fgFV8Sxq*rA6UWxRjaMYxfea3g3BCPN1C8#jI~;epFz84uEWSD}9ARMnLFrKven% zp#{kI%bb{G)VU-I4i#aC_l>h1ood4J=EWq;+g>oH#m*3oSm!-aEp3=uu9&~qn{S0( zH~FfshGUb0gulU1KYjSo2l%q}Z0aP~Sw4%$V6qIbGowytHLVsgEOFNHawhC#Eul9tG9w8YOI zD$#q>3~Z6eZLQ!LGFDM~UkJXs#S-o0xG}-l6Ans9YL(N@IGmvkPA!RtMax5!iH{&g z#p5O{txHFM=RCQ+sBP_3qOohMO<@|UqYFM5K#A5Gwd|O?*N0W@_(ugHaI$`z;ey?5h^c?tPEDmp*zEjON)$pRZcArNL>uGqSHJ!0*5H zG|@zLC|CaIamo*`gk+u_u+zoHE4dNys6;~YMMt=pW`&8pQ!75J<{JiJ0^V%c1C{~} z<>8(!WK)|)G^1A2P`Ys_KNF)1WSfZwsLl`h~71F*rWL+Rz0P% z2yFo0(Z^6srZnx-Nl>3vbHWf>9roOelgx3>3^W$_kfRkx0t~QXx%=oKot9`&aa|lk z!n|VbD?lXLOk%ME>Y=nk`j;FmuIHIVHi7cID)RzDFwpa^#=S9Hmx^CT;V`GZc~%k^ z9$0roiU~}Ff-lJ110#q?&2eAkD9~*Qr-d!cIY2+~Hm#UI?r`s@C%_g4<0 zLyNmoe(_yy<`p8oqQDqpZbB~Y_RXs1XmQoBwxukS(i!8a8tbs)wc9bJ8aZ@U(n`5< znOt9!3Aq}Bk9U29s*(Q)M*xI$f4SoP{+y~Vy|dZw$!crI zIy>(R;_7d+!bU=#+V{=aoTdnN07g73M|~F5Aq}pw&fhdu-r1#X&hUI9q1dLs7q$se zW%Uivd$Qey*9`RgY8pKwaa`-w93iusUZ!!BN>*oiJS~07Gdc0qN4t~FK1wgrVjr|G zf6Ke*<{@cz9)Jb1Tf!HsOF^hRpH5Uab;m@K`;Y1y?Nn?|DrOZUvarNlH>+()ja|o5as8xl(Ws zbhTk=Kz7{F@-yhrj`*@(f^zKZ%bT{2YjEfaRlvIw{IoVNdLEcmH*NY6sHiQYtK2U5te#ZcuJ zo?%*$Q6g!~43mny#95AS>Olo_;9Ve*5POd*Fkri-fb|3bDGM?m!1YB}#s73rG#<}s z<_|^I7g+4D?p=M z$06nS5LrtCNUJa0oJ>(0Tl0=Exn`vIMN$O3x97f`Ic3|LR)Iry>NJa^a*$rY>g^ z=WLDj&WtBm>}ag@=_k=^{0T}tHVzh2{?Ecg34NG+c1uu`xcw)*g@SV$(}c{zn+uVf z2k`)>9^qU=l6_jI6>`>1Xw7Tb(PR9^K?|53Hypf(bi)T|1^MJIPy;`589t=mF_Tie* zn9)Fb9z`cS$QlMyn7(r!h&12O6fqT$)Qe^UQWUo9v0msOu!Iy_g??wf320wcM1`vq%5n?E<8v zpkS>juNK8R@MS6U1zdV%-9ajsj{~Ij`V>H7G;WYH#Eo2Y`pNsSp6Z zAdJAYKlO4Wv+Us>8Cvy{V_<%%D9QIH`XQXSqBD-*N7phG08vJ6ZncaGmEGC+qh*@PR`0<^qzZR4mJp-M$)-bmdy(#9KF^`E1mgLFNq?+Mece{)4u6 z58Xwr;2_J%da|jP>Z>HV814JphAACvACp$PQ#f+9ga91=mt~kwkntFDFKFx;SRdcaN_l&dU$vMr7S=AkZZzzrx3o8`MPg> z_(qm`?3xR7tw`+`i~w_+7rapV@F$s3#N$4BCVpxP_tD4SKLwg{JVe%e9i{lk60XbA z?~S^PREkdL>7YV=E!_DVj`{l&c@w6K+3wP@+s@Sg?l*A*S@Ax=yX=3gnsxsVw#4r{ z{{0lYsv=YnhmG56L8bkvr`Hk?f8X{$pWeUq(Nqrk0#Z!N!2F+=74g7(m8Iq3x%V&6 zDH4U+@YbANiMi{Q%Ku*bd+%TT%-x+Y_U(T^{r~kzkw8I-S>d7``+q(n@Rr^IM9ze^ z{BTvQ{y@8H*5bOx{$Bf24doE;e-bvH5XST3%&WG_1Y z{RtTXy4-DN9)bV5#_#9)>kmF_WG6iT{c`_!KoM$WptT4F4)||1_&+cE6H`x#83+Ae zHUyyf1HEY;r!N!ESp5qF8K4$3#sP#~g!I2L$8@e(>sqxUoB#4nA1=u8|A+M!<>UYR zdV$6lKmKLwZXnMCY;~;I0Fpm;^lwf7{plgVGyL~u-Uq%Fe&b)Hg9oxlj;ZJujC6XI%A<4Ryt{QAZ`A>g|B z%*|tr6lmt25nD4NnXpg**|0uyNr3Mdo7pTE$zROe6ZOPC2NcgmUeu~xn!;y45yPmk zq?qa*E#xV#9mg$OkGaGcO|N|38s=sNgbp!*gf8yxxpL8Mn+1Bs%Xx#T(+z|;CcO;N zg~q)Ry_>*P-mqeTz%7JJz@#yT(#LJdg}OGB!>C$r0pzniM=t!h%+>jv@BK+j+?Bm7 z0;mTK?oTChn+bVzyh&6HN)b0hy)NCP1wPJiQOIC~$7+Fn+?n`ryEcy}UyMdVQ+c^p zz=TBYPD|@}s>(Za01BkD?k$9nj#LDDEx_v!3s)}AgTxE+Ljl^Pm*=hQm*GR%UMPw6 zSwL0~t5kGBoHd}%e1xDCMm7fCXx_r53$f3)0ZNx0iJ=MoFrY(=zpJb3;r8K^;TQE9 ze=-erWdR#E2>S*Z_NsF==OTFckk=48LIeRB_r%dpXyyJPoItZcE^{cec20v_$s>&m zBu_YuG;N$Dc{GB{+=;`%V!xT#{b1d|xpB7mXdz4$Cb_Ph_i7#p65=k?HXxkDAFYAd zO*82s^pDQI|DWYbgA}gbrtp^s85KnuOrDEPQp7swH{3* zndFnUXfWPrK0||*C1ReB!a_`mIfwiqrKgB$OYJ1^pVUTQ7aAN+tmeRW5|Xbjju(So z6+mvHaq_TujU#7DwOdQ!S>wS)c%Md^iH?z_g32(a5>LHA4&pM4{=#80T}XWb@dBcJWin9tlA4Pib$QQ-aS zR^Yul^2{@5X*p*8l#^Z&fL=NZJFPteD#&&U1RZ?>J@|);oUHB^X=RIVsFhZq=?4MS zpBYucf;S7Iu|H5y zCm=t+L9N%cC;xKjZ0R%Q=LtIq03t2-$JA;XQfs`4+T3$7s7dIrEDNCAje;7zeck-K-+iU2inb8R8cIN!%H5Xv2q6NtMEO(?R2kuPQn8K3QShk*C z=qxx3H18KBn z$uOEA{N(uf`56xAIywpjfPkME`5J_MnBSLpl&pzL@#13?$?PWk&gpD>X#CW+H;IM! zak?G}-5)hWVEol%DA>3=YV`FAYm$4-Mj}hQ`EAWawNkU?HlIYK3KQ>PM6`IX7Uv^A zx41Qe2drx4_PJIpfzKQGj!r*GvT9YnF$O^kCq;Z|Ef8BrsZ>JT2Q#vXJe8KCUp@Gp z+&tku^tt^>YFtYY@vw&k^+Gh)wgm{?5cdGTur2j8#L;=33};{&vfStnW7Mh5)1y@# zKzNY(8OIN{MAb&@&~N}zGx%Zv@e0%ZIL(RUH2yn<&>ur}kyqaE_Dq+3{A>;heE`?>Mn}@u7cJVu!t5Hos7pxJaJZh^ z&Y7GRENw4Rl~D-p(ixEr30LDe9lsRDxeIR zzU-f!6ANF-Lf1e#zs-yapt(rFDEJ~3N!u3MOVLeDz_zGk`eL`Ru?`34QqV;PQ^wWY9bm>of2=x z=bX>n;KRJ`-24deQU3`Lzik&xrgm9`R>!SjJ!H+KMw+F$|Aj%i z2w)rPn9e|4fQm-x=@BPGOTE$=Gr&loC^C>8n$qq1lWWu&G zWU7q%O$LCo`vAdz(f%^*qnkghwAqq@RsUY+b0S6S$b>(10-&2-CGMY2Z`UvMpqgtU z%fj;Hfm|O@mgBznt*o8Yz5GPFxaSVbv7uL%a{(Tfi!P@U4Ffq&CbAWrXUyVdwAe+8 z(4zysCGUk20-X_#U;VMnM~m5Mq|eVcqVZiT)AoQu9iWh-+NhIK7NJr8JfFK*kHb8jM9^swXzSD0 zntA_%ZD(?HvN1avV26Kb-0_aXjm{Ju1t>Vuy}9OzdjJ>Pq%-1ha4!sS(Ijbzd}~a$ z)&~L={_M5-4Hu~QIW@|f5Obe0Yzuuf;vMHUf(MYx!}j_6k_0o)t1GB`G#K##CTqZ9 z(${b4IG1&&Io^JJA9F|BTQh%V539q91Ty*ulbHsNTh1Xi8E^1yhJ{<-sHO3t-~gT= z$;@h83*#Fli8B@Vf&d}f>gH0t#)(?9j*)F3vELslkQyVRjJye0io-I|H4S<2YU!SA)VdLD3E_*|CC*wb)$oG7{ z?>;nkLs)f6ywc+YP?@!T!Hx>kHRzqi%H;?F6PyOh?ifkC2fmt+tkXKMkXzOqJwsXjV8u4DnJ2%9L|u8#4Xp)?s;Rx-~3gv%*>a z0x!t=MG1R-jRj6D=S_Adr#+kTN3Oa?unJz;c7i@zpT;?Fnk<ejvY8Gxx)eD~3yDvB^7+FYUb&C*m( z)A`NsIl@lXTA%4Y1}bl*pt7Vk+x54%b>7@5rn^r^UrWyj6l}))pg`*qn6X?R)OliF zAh<}^zIN~DPn+TINfBAMwd}tmbOgw1xB98zE;K*8^f;53-}S_?Ryk*&_%kdGNI0^j z9AIhrSaz>f1hP!?zrT1vj*n5hmin6M{-P32tuPW)1x(g_aXtyKeC(Ni2igFsW@zc9 zX2}c{Dx(Pxk0a!$e#lIlU?)uasG|{R3!sfPMH)T_t+|)Vn!*M7u|K>rV;JTm5Q;DB zv;cd_mAe=pY07c7x6 zas%JZ7wj?C3cROOgfdn8?Vizc!=MNE$iC|V&f;7;I#BKZcr)n%@O(BLzL{XTjS7dh z<>!)(_OU%lH5S!HsKYdpDS2`YOTp%&cX36bQ!TXGIUnhfa(-qP0>#hI}mW*nwxda|tizEU2^UV(|z3AN^BWC=~JxLd)-9=61&n0e+pm z)BfA(K<6YUl`G9N$o!Qp2{g56;4GmBAqxo!nGI(JY3YI}d_;i0{%f!lUIZm1@S}rpZ~4?Q`*W}U@4M*F9|Hz9;NA?b zz&#r}q~>m|mC^p>k~s6fS&V>L{hrdLT&DKW|noxb{0iQA| zYfB85YeJUhUheS92b1qN5!Tz9g(nnYkEZPQ5@rtrJ?fH3{}Ap=u{BU?0lER}G>T?# zJdoLp97vJ~DrZb~I zY`&t-X9Fism1p{C(YnLy<`T~AQ!(D`{}4I!Z}h&;B^xe}lyWtsop*Z6ZB8u0wQFJ1 zO{S%7`KOB}Kus_(phzW1UOv(N@(qzj_=x9{0IHh$J(4HuWh|#ec(7}?ad(`6<9bhL z_Xu-VSjomfb)@?!so>|OZ~nLq1+Y&kxVeA7-CqDGIX?<#yP*RA)|y97S&K-Rr8Ep! zA|#g3Jfk(18_=!!%T5|Du%yf4=J&D4v3)w@)dxO8^`|l?LyHVCnp9(z@B|=TpZ7Ci z_D(N$o%e!bFShGKf_KC7A36V2WE*K;6~aiyWVI|i7W>2V(VNvQccpUaZMskAGcNa2 z;0kSgioC;Q*j9Abx5rU{%ew+%VyM*~`P zF^+X%!~MD>qJQ@-@a3ibBqYI*J&gI3sB+5Bt3vf5<*^0GHiuqal z-8V&S7%ktW$$Os-00YaNFRL`*HnmJBILE^7M@Xa+w%j&4OCxfPM|#YK z8WD+$0gP?j$)9kf&?!(vv3%d@iDF0!iQeZAxPd|8d*g#Fn)=6}sbVWlpsD1>QHmPh zJ0|UzV!I{pew&`&zP>($+L1=?k;yHj%m;4@K#@^p6$;`z#7oPO7W~r*@*Yk?8 zGp~f+Vx+2X_`mGyZB-GNguD&`h5bkr61Pfajx}S4u!2?Ig0T*ugg%sMoB3^fVTh+! z`P$V=5eey|UC*V6tk`mrH$%wL0OC4CfEnq%Vb55*ecbIIhNYY;t_?z-=3bg|)8p0MCzq=`N6!nW?~>sfloXLUZ{+fHElIbk zEWsU<32*^T3oY^I6A7?@+gP6_F^eQaj4^~^cywJV@xqz(;yRpgWgUGn&^shHk5$v} zZdZ5lWouTgY3~(c644Hzs9wHiKb(h{1^1MPSKRZ}3U!kL30R>kwgb*62O1*svwqOK zF>b*^YNLe}+SDX$o%VNZe%^LG2(@#xso4#3immLRuC6I;W~wOJ(-4D{wQa*@%81fr z!cY$mEc4obIFR`60MiqlI~Ksk4SyHO54a;Ka}3w5NPI88ttgLM< zR&0C&o!lk|`MwGHBfYGtB*qoWZ^N0hM}k7 z%L}D-4zS^!WOb(!1K9`rhA-ZOB?rS&xK0UucmK82fcWL41?|({5cs?>_ZSv8@4F7Ud zU(ur03!GwNt4hJ)`et%^PG0w4B`(e5uC1@HkkgxaW;v7`zkv*Fn?vs46?2TMm;AGp zlFwcAG5+zMMsjvHv(H^$W$HP2+2X)82dgm+)d4Idt0cOYrrt!uM-XCie9Ol55?=FN zyZ4E8Sh8zO@p0^ah~0jO1NQsFPfK$*7aFN0UrR_k4~7>+1g{STub045*4FUUuTdd@ zi{XffNT4iE=6%?pGG)D^1RtK9P6bQg%58uaILbq(DC>a+92~4^L|TaOB{I`DIU@ukZ-hPDNtvt4^{Q2Hi~Bu@br5ly99lA^M0k<*0RcmL~X!Q~y} z?jarB$bIK{FTC@-3qteJ$)bif2|79~Om#M&aVhJ4n0Yc>-NJjn&b5|@s76i`yCZgz zwBEu$mUmWpx_jw{rWt#_b9~brn!?Jsg(mwkR0Tg*J=R36!lvQL`Fl2#%ZI#N+|@rV zX{ZgYy13AQz`jFO@$s3l=nC-hD#6Ju!8f-80qskw^C?Fx#HaSh19u1h|1dyA*Q_Fo zN`Em?JMKihuKRv?IZ|R{p{&i@8frpAXSk?}1G55Y7%AE6iE&1N8he`7 zor>3Yw%agAWa4lF3m-BRv+>0^z-O>f*x+GZAO9 zon_NBx1R9cx@?>xQ+%UaSgv@7v$Et;i*E{g@7hOK&i#0!sow;su(6z>s#RuD#FM?}d&Bf;xz?>~)IYS)W%5NN z+ykHH9Ngy&y|Q7K|JcBg6@!os)ZTThXlCapWaMm?2JK@v4M(=vD5hk0n)IVLeOHaQD0 zluCLj%IXoCx(Rx^x??U{Axaqk5wKsP=1Y(BQiGVZk!kFEyPg@Cvx4@tFr)lYQ<^X; zloKe!6(>(4BBJ7e1kb5)JipQ+dYm^!7Qg|VsW1Txu^UDi%a{tJw2W|;ZgOz4R@c`x zv)7fh*EJg+u&*e3$cIXjF8N!ef87xYr3oO}@knbC+-qd&_%f|V&&;nBeQ2zx`mD7k zy5s6ZlTi@CD0rf5`PM?^*C8;6f)t-j+xd-J8~QFPnj-o}ViDdN$u@(ewyxTTN0=2{ z^k1r>ZcY?K6QBNJDtNV#7^{rGy3SX`qUMtnM_2E`Vx@Uyh2EuD4kulbgdR5Da4#S{ z3%dbWYhZ^XnFnU39G_OnL~w4-KS7n);9xUv6Rmf^q zLXyzSJ<#uE5$P%Y2K$elWkW{NHYM&g2$S|J8o_6u@;(2uke?l|kKY+95c2Ba_PtA+ zau*4vl6cp+oLt4d^MN0NxjL}6{M!X(eFz^@i}%QG6^jW1^Q4b+EQX=Ma_goby& zsOE73VTb=?l_GP`k?2@YkGawZrTtX#P3%;hC_7}CyQeedkg$uKbUs{LN`Ww%?s(rQVm0ra)=oHcEJL~zo_@LM{LE=YoP?ZQPatbTLv_lSc@PkR>8P^WJo%;d z^hs$#D<)yeh5AEtWzsUMn=`A+HuFpjxjF1P(6EWooU1Zpi!%d@{bDDiiuceRB8&{~ z=Q*Tj=7Y;J8uK%X^UE8{GK#CJ4ZG^O4exBMP1>|I)UsXb#o!D@Sf^Dq?C+za3Gb6J zX=W_yDPYP!7GNcY?l6aAfM%iOS4NHP5Sf7zq#X9NO8rx_J|`!16atmqGJXT#ICAu9 zVLQ(z^%9jsKlr&(Dd_JfmrT@+-7bB|#~Iz**)6waMAOML@gje8R)8E3z;TO1G53l? zheNMij5Ck)Fl({1sGQMjc9Wc~7`Y<$R=*bhDhW5D82)jpeoD|7H83ESBOY2}^8~O# zEvu*+=Yy->C^xQ7ST-i#>Hs-Oz>Dnq%v7X$Up3EP^H zYR1zt`XF)c(Mt3G(z-Z$-?8ZQa_0oF3C;c9OzO<#ut;7|b(lq|mCy%Eil#S@PX1GE@n6&~nj6P&!O<#nBmZH{;_`fw z3;CCmUTJPBZoOe%xeLTS+swVw$_)CJNpF;YwI+dzSKw4rZboQ+}kc>ke;fPTTzu;QYpDyM&JDc8pFMNC*_F| z{ola^aJ>b58*GP&Xh5fK6*Bca^2;Hlai+x`K#(hC8v4}gXGY-4uFa4DbnWrL3b zQ+@@2X@d1*y$8-ej0ZaK7f2{al=x zt?mB)_N7d=1fw%)d>WdVh=`7ULZ!Zv@oOU=*7-QcZ4?^x zSa>&^2cSwK?WjgM2?w7J$s7^fgMz;fcn9^g#xiLoO{e0a7g&t;#-xdWF0s0+>$?-9 zG4gmzNYc``X1=lBCQ4|BayIZxzQ;-44voB8!2(9l$B*apc%$$9#mC0SogJN{qw%~E zoq^glo_&vUX$b1#A?7k~LbX1EG46~mKuvzsfTe{EN- zh_$n~rEMhj7B9)vh};aUPcI>4Jjs)@6FNA-4do|?D(7?XzM_>!LBjY8CV`IN0mlUzpzCP-t>&41vg*WM*bs$}3K$ zg>!9-Wh?RV@iRiyN;LMT_Pt3TK0NKW>5eJ+xeHST-MV$lT>I|byQ+Kl?kP%{7$r`C zB5WOPlW|iWAlRjzn;+1Q$X=4TzJL15B%#Z^Bxmb0hn1%d)U{r4j>d9#8-$JP6UgV? z`_-JYr|1R;!~~jJ_>1H9>iEh@ks2zDPS?s;6??+n`;^<%TENsA&0GL0OoUF7meyA) z88ED}!Ip38UbIhPr&Y)m7w|af= zg=j@QBiW9r2AymL%S;#y7IPy71&hpWZWp`*_FJNqu-(0XWtV)1?|l-P*fjp)Yul>N zSj6(CXIk6sY1O7fU-sS0t?-e<$O~|5mElURr+4_PZLb*q>h@}3pLJ9gNmgWT@qH6z zZj;Ey@l72IYSi9crS*fOyD#6!#`n_duS9vstCzwYF2$%N7z=R#Po(zZW@G#LEfPC> zd$RC=j`HdHT5d`t0`5qV#FWQPC-mfX-&Dosh5w2^yNN>Y%nZewZ=03j>U>^Lk76b{ zvE)*YJ33^%e{=>)#9SQE@#wmI$!2faAJvypu*|xB6+|PUv|S#*#$^(kdy4s*wnakD z){feGTV;7>tgKC0@wl7z-dc1zeN3Pea9He^Urfg>t%K7wpSBui7BCB&Mn_dv za$?`U8;n`ddB7X*L@ObDwwW0b@k5a=)NOopmepLei}KU*a;rdFNI{GAFO!KgfSJeC zW5RqVrg3PC^0L_5qz0F}ju$fZuITeh5BXw;ZOtZ4k4}~r%UlX`90~Vnp7{?d^{?oi&08|U%+Sum<T@{9%MI`UtzwqU|{a4 zIP-ws@TlIfic_WJgQ6{_3?<~o3=+#nOe-m5^z`l$W^FM)nn*>>#m~gP&gCknW$J#F zBpG&nkz60s*tm>sDk~e(?7HYIiP1`&)%o^Qx&B1Ib;H>V+ICNmjBr^iw(#-0@;j4M zVKdG@28Wr#X$YNn&fOqg_Ls)ib)&k!icZhqJ{0ycgiwH z(rkn%_)%Qi(g47T2k@!11kzdKlhYSshMv02${mNtv_p~0db;+tEs81!BZhv;Jim#f|9}4f^H-c)ZyJJgT8l5O03P=!6eeq7hjEAkTur#_h z%Ae`N$OJaoo(eylt|9C0B^(cwX04kV7Q?Nbs61(0U!NzTkx=99ZK%8`b)Jc`Nf>E9 zh}bdr1L5Q2cipwbi(WcB#68U-FKcPGLbT`I9T`y(hD`>x={~j_(QNoj%jQsxLfSi` zZb*XA7zr(aU~)TUXsR(=R@L&Bu+uHT?4v)-_34T-1GTxX5OVzzkpP;3@d^8bAViY2 z6lLU1N+pExY!%;1fFyrCm%gZZGV^o*myS4aKJrtOf8kgQxvzLStbhL5@{~YoWgD?1 z*d1FO(P?xvv)@rQdL}Yyh=vweSXwWq$IZpl1ZxwwJc&1^rd8Kdq{Ur8oFN;*8a!s@ zChC47q-kbo<7E?Pg)2-Xl9%S^Pv$(sX6#PoLtLePJjT_#3K<{Pi6}HU8-4UB?ANzl z1#R_?E8BTkOPYJ;{^k0#d`690BD>=r9Q?bJbnpuRf#I>#_UbD!I4Q}K(Xl`H9HNiM zK=0}P<-C_~OV*=_D<&1C?E~O#A@;YL&8a{`zQRIh;JLIzTN zoV%Ewl4<=AEnK^iy4;JseO959@jEpg)gWnn*2uJkqNi)i%M`g}Piw7Ty_!3iR0MVD zRE9Gu;XhbxJnPf$7e4*i^UmJ+==R;a%uGGPj)|La4-i=>C0jPKTm9^QRKfr+awN^G z6%a|(Dqhs$>mBZfcu`d=Lo+vB%^Z_#V^ca))6tpEdi*VNHf3aP`~?5r!`0V|V1bRn zWcI9*azrM@v~1h&<7O7~!0E_zn~6vZN2y7YXl~O`*j@8e761w!d48H!8a}sFwY$_g zO>_q*2Mu0c80^r=**OH1ljG#l%xTSPy)4ITrCC~5nDwW{WUk` zTo<%%wKZtVk)Q8Qonc~F92)!{7)O$nI1d%`5rCX~rnDFIwon$_gnwXTBSCrj-4qDa9*3A;vo`gQ5Ag(J6pK zQt3xXh>)Rb%)TJoA@WQyL5en!uKM!Dtcf93>^H)ZBazl@O}?@5BT1L0>+Po!ue%k% zG+O@ZT%*~Xq?`ii+S;v4hmy|O^Ya1}$l^C?mZqoNw0i80hmPNAUPQ!4Nj!UUgtL{kk%o%bl_f@psLU&_#~h} zUH*eW-C|^8V~aZrc?UIm)^X!RWfOnyakr=xe~MPMwY`&=YtWa_Bwq@jmJzIm!+sv> zJlav$Ro`|i#EIiXb4nH}8e038FJDTV z$LZ0^%ZHlnT^a~fA<+>HPa9WywI6SYNFB-xojDOrxtNA|BUX53N>nBqR|0NDCc5kEZClQdgYGI-W%8qV^)SVDYs$$+6X`k zaZruy&H*?)f)XFz>10m&tZyC*LEYKD01VRGkV?E`I}i-O0y#S50(8;_uLcLpgl3vb z8xS?UWDTjwk*N9Ni|>wd(;+Jxv?JCImUmE+0|Wil%?0t$DlOeJg0FMLO${DZO=du#MY zW6asbAXTP!qHos>jPguXt;?;+Eh(w4tzCtT>J^og5ZS)_i+L45{;GtX_+vKe z@e45HLO321b?G)OVQ@56*p?2-eo!`bm4IjRPe9(PtZdbS~{HR%ZQtPSzfZT zhwu9MPrJT6qv&H{SzD9ncPSYgKYHD!Juo~QYBCja_cGtoaN0NX2|rk(<%=xye5Gd4 zO%lPaD8=sF8y~XqNK2mHd{=St((V?)(gPG)-L%Gh`=QB^ja&Vjk?-}eaSZ#~E@5+XQpg-i_?1F%}Xhx01&FaHHy zrch)soI&!Lt=_%P^KwMcER!sQuo@kfXpD)R#l%@9EHUVMV!p);{ax>VkjyT&I^=~E(&t+bf;re{Qc*f<0`5TY&-9dklttyH6{xGT$iZFFQ7Oy`!cat>M zJ3E)<`@XhGcR<3Z_a8F|DAp-5)G4M*Ds0s@KID#g#(}yjXLE z-r?5$d9+j;8=ENImgez8ND!;AK$k=BdeufBxTSw|c4(*|yNa)w{32Ri|1l#Yp{lH_ zne~p%26J;TdYGN9)7r*PVG&a-_r)PB(NHbtcX>$gvoz{PtqAW*_vdyZuaa^B=m^!B zIRf=H%=?q437F~OO(Mhpa5w?%C>KTN>=AVmeg)-7m&tKyniP|)n%ii;*AU^=Yw^6N za3|q*0>cK{hIFi~475mSCx_UYgNH-C{t*riXTgqef&lr6dTVrE9&WLgYY-iuPQH(iC9l481Z!1(bZX@?Tx+vp;gf4A~Q^Q_fs|d zuYEzmVUzPM2dF4Q;a273MnGEE-DkLdGGe1!3uf8ZH>qxtL>o+Wc~<`Q*dz~tG+Si8 z8IamMGpVDWE@ZR<7RQ1+Qa|DD__@{L3SAayMmexJX@?Y!($Mtb{YaTfnH4&DUv$Ha zVMEyK$ac^_R`N?rOUv{T;)jM+=%PN4>(X+fm9X{JiHV8HsJ`>?&c)7?Ey7q`xF=0U zOnkJ{QDd`8m#*$Kchhk|6l+{;ky@buKY!(6=U?*bGl;43s4y~WU-rX0sOU1hLyCQ6 zx*=^hV*HfbcxjuNOAG(j-liyEp07W&h|{TXAo?C z0-7a_VSgVNcSRB_ME1`3xMr#c+!yxv_xl_m@$G7GlLRvD#?LxFyEgUBIIYZZde74g zo^r3eOy53;Ou44yuc(pr(pkcTkbkHJFf44XtrJi17B}R4?))qD<Ff0H#^;~u{vYlsa zORJYq_mx@dj`tF+&(=C>6T)ixmAzfm);=iNpOmW#H^Jbkh=YQdm?y}A6VQ&omeV4G zp@PRL74cZh$WG)KSy)0I4B_d~wUInH?;Y3F$Lm!ot!4Sk)?WTZoCb$~)EW9iL^EBN z(9zy8QAV~~SYZ%4V5V~Sm!`g&KsjK_b2|R{Ofq-C*48OHlYM;So6U$sszpp%!lOGs zv6ZyYF(35-!wCG9*!Sx49t%HZ>H?xBqxsG~s2aDr#|4OwVSi{RO`WV(KHr2q`BLtUR{X>*7PoiKcJXD&zYBr?mtoP2S)D44{*e%#c}{|Yns1spVC zY2-6gDI*Nk-_;*aFdF7}6h~?#$S4r6l8qb<4tifHw=TXyd`FDY|0C-vqpIAxt{@-{ zBCWJ^cb6y#NOyzu0R*I^K~w~l?w0NjX%MBmySuyN+m!bmI=4TGmmSV!(9mU&lu4Njm+zpb&mc^f($2xQJSokD5i@#TSB;w7 zgRsm0vEPJRXoecpU0Mgwi(jN+#rr#*;8WPE51_6~RNuH8czHR{Lt@Y0pCaFg7>!nv z7Ejz}4_BC-dW&c|98aHcld#pKy2>T=jgr#mAiR@t*}Rh`)kH&<2J`ldF;V5Pb62!+ zB{?~Y$jQmLa1Md9v#Cq*T`euG(TN^Vvm9Hr+_wQ12eXp)GpZYIZ~tQ;m$61TbeWbNqikmhq?z}zokc7!47$LtFs?d`VZVYU?fbIr|R zv^4B;oQ_;vTUwv8jL&yB&&2C}+81I#vB+DIrQ3yDA{8o*l zs&ML`>^@nq81~Cbi!_}Kt0;;~sn52Jn-0wA_Wvi?dE((Pv9pE>sjvH@=P}`=uog;u z!jN9u#63gvV4XDPxk0#e>!4W_q(swBx|j&7U8}>e!!Qc*ZMbM`Itbq%phTI3@nvSx zgnkb%%X>qnusdpugTqPAcot)dBonp*?&;o>RT*+W2axr*ysyEqBu606 z*iO=SVusdc?-QNGUqXE#V!nCLv zx~t)yo_8xIE4qSJ;w#P35YFO>s1U^~#xP}Vxm?BV!(9R9^u_gcIx%ty&cZkt0ETEcP*gCn~4y?;KX^HvC=6t6jc!z^ytu&yIW{C8K{w?8YIAfb^E}q`U7Z zp`>Bkd?F+Gry)$LRNaX9US4S@|LGe)cP*vJ5zLTfypUx*y&Q=X?e^c2M#kpUo#X1+ zHq|L2@G+lnm`k0BP}2Wc)HPV!$kuTsTvYakU*-GzMxxylUKphfV6wYv_&mFHpsXai zCaeQd(a|x_VnJ9gi4HSWQlZmHnWv@kc95l|C%AN0me>utcqCuK9{NT$U{Pl?H@8!6 zA^5T|C+A@3Xi`s4k7P}VK{KC9KtPxxx1~1_N88HOUV0Hg$2%uJ zf8=0Nm4%3=yh^J%6u!M!gMX7Jr0+Vnuf1)XN3q5qPMF76IACM~igh#IZ@)@}3zpyj zOige0g2qfC{+&kuKVSmX!3nUS=rBlW^1zib3;}Q=hGZMB?BwLIVfUPo)lEU^M3$h2 z+Vkg+l~E>;YNui%R!w!u(W;zD^5^ugLR-q=*Kv%Ds?cb`#KLNQ$%YjA>izC)E#Fyx zbL-<7x$7=rPkH@^VfMPZ0s{f=Pi*@9t` zv&DBjQxIe6YbXK_%8bT($mOe|=LE;ZVlT`tE*=Kkk=)h`w+Wcg7K}l)?Fm2kOqG=w znnubVR@3SpYKZtti-4(+gH8KhJ>WgSW45y;vBpG1wP;+Oj@5ZA5{&^Yvs0P>yeq#- zvts_4d+mdkd7NiMn@Su^-L+jUNKW*`I{Ezj;+v0THh9iQb`Fg?!Y!7x(GT{9 z7^KVj8h`t`?;o5__TClxAer=JeQb4Ds$=We-lbhJ9YfR+_%-TN_wHe+>*b48W5(pg z4YEXgisYD^sviJyED9Fg2HjSPkl5H*u|8Dt_DEV+9p(_GM6fW_&2!_uAuic6Sa#I|kmJoDi_E z`9xAbJU?>2;(5Q{-CW+G~8r(9&Y3@2UXVcd(bdI4`j9&p(7EJ4=6&( z*~LoLU($uZl000WFi#9S0SG#|`&4RnZfRU_T=W0b0xM-^#ok@TjoB{Iyy?cv)0PfKva@^GHqx5Q>i3u&v36}{nH2%j=o-m%JP%l_) zSb%qUAXk}9?Oportg4`3rjvKGpv*z`w zE`O#_r&PJ?9+!Q}&i?LASiXc)b~V~DAT{O(B+5PeDdSuW`n!v1a?!t4Tr!bG%enr*#it0~~B6 ztAs?V9K>h0pV@y^XREjSR^drs_IkF0Wex}5fZ4G-nHZ;NPjiDCRAF$WDQJZ?|F+(n z;&9qU@*n%ExV^i7cyQ3x+8JO-(v*|O!Wqxco12nSo}aEe-Wsf3KQTeZN7oDT3MUOM z{sJ9}@$rd$>#+9Zr@h3`tmM}{tP?&~g}0hGp5i!G_I87{kq}BMRn;ug;0tnYr%~!h ztoGQOV<8ihT$g#wlx(6<&~W6_Gc(k4)btEd-Rb`!DT%wu0F|*63udv$7X~Qkhog>c zK^4JB53zikb>Hsg6i@j=fDn#Ly`Z#y>wjIa-{G~0gq{ulm%)a~4E zHs~};V0dXuFX>@YWrem56?r3HIjSot*-OfPG?q>D_VT`F>4?LgW%ia~I)mNk8a;jx0L5J%+{xp$2D>dYpm?brPQ?irJOr$IqxQ!ZJD%#rd?cWM5d0ru; z!%4xRI2VpyiMC!{pMo2EseZh?Mbv&-!DXyc@Mpk4__d0AQ~E2Yp!Y{%E!t*3LUWv0 zF?jwHiPL=pEvnn@Mvq0VKG?$*K~Ic8$Q_)plk|9&PWr7rugCt%cTxvYoULlC3ciuw-2k8F=(4clUF3v!QK zg!DIXDUukfb3gN-Z!#Mt9cP)^$4u~0{H|!~ysG#xf?h&GWYg>O=E&%FjvIE4BT&|x z!ajt#P^cv00zs`s_$;NE>>-83$;{{Q%bU2oqlWQQs1qAR{C7J8_{g%n>P3VncZk~R&wCM1K$^^gX0B~Cv91FKsB>b`eMtFX3$k~{`SdP6LnoViFu znI~jIPQo$Cwq(QbY9#m}<}fe8MbGkgino0LPz&X))ek_q8R~c-`mv7b-^+(Fk`)Rg zm*HZ3QF^I#+C4DITWqcFs3pFKeCR8AOM+oSxrMg-ggVIYeUPX4lYkN;a#+d_ZBr~R zq!Z9{eB_}<1I-@;u$qoDJCeEk(Ek;*b+0|UpA)IA^w9RdqBhSq3Egm8IO+w+Qs6-I z9nB^kdOgD^-672S4oCEuLBb@=4Z+0R!U}vg4TcE>M@8fvr6+2n)C@;Tz6iQqGi<=t zT>oLBho+I-1H?q}H8P8J@1w_5ZJ8Z{sw7YPQx{G(@5acr9_z}mYn4CQzvq#obFq3R zCMg9$>+_SzpOdFAK3%Zi;glNIAWI~00hsX0-2bbAJ3_vrccu1m)?cNQA7>oK zI7B&Y=|5R;Z{0Qc3eOCj03A9*NE9D!I3#DS9pF|o#b6*Qqn1nMF1-?ih0k1B{=t3m zF5`y(+LtE-lvE;YZ%q+7Z4cWL!^pNG1CkwM! zt1k773ixfKzWKUMHSWW^4ULEZLr!5VsS4O4 z69&g79^GmGi82VuN^QA6_-`;k5ChJ`L*Q<*M>M`=g%>&r7Ju!j7!KZ!Sx>V#eTW?~ zS@6S;iyyepM(8iB-+;jlI{20v9MN-|`tEy6^i)6?9@Zew{T7g%^L*?`g6;>_-x6Yj z+x;a@Z|wT_!gV8{7r5;JM6XGPed_)EdTrs3;nsJgobl$C(46=p3WiP?mn`b5W5MB3FIxLS;WBn&ZYpT0SUM?N#JcU zB4-)hsjPPjbk6opkQjZ!xI55KY>9y_)Mt>-!lL04-|YqCcAj+NvbOLr;z2S>Z}gmH4MW+_0`n% z?;EgiOhR974(H7utHf)P!R#sg)N9TKSxGJLPkpHfLG9+jKKE3UlTa2*fKl?VeVn_oq#{l7G^2Oo!;eY{2H!3tMZMW(aR}n#XU)DDb zJ)_7yj`9b@xV9%nlkjN~pK9a8$S7F>oA`kty=Y^d)$L)6Fn|heN%-`? zV>O^OONWih9FxC_4lT-3orQq8uWP`eh6o%gRF??-ezyNO``xsjfA&a0J0Fs|V%ymH zyPe?khLn~QJ2cPjc)y3=4Q1B|_^Xbe8uunsSB%6qRJ=76V`Hu&{?hl;AoM(MXG?g; z;HNUUiQ)QydmWhCP7B)u$rvGFq`LI#=YI9VFpA(-p3pe z$@u&czW1bc%`ob#Jg6?psF# z=j70n9qHB{r^?~UJgXNOHQh2ZLU# z)byV;L${dc;FnG>A|imx7`8^e(M0;XgZyE9ui3b%){)ZH%EQudWsW|jOMy0lh4Tt< zK>jrTasJ?>mQEKH_0-13=JKFZaCf(HGJ4xNT)W`Ij=Z3byQE~+Vp!_WR!c%b0sDnO zt+M?54;Oh?Y){n(%z&~<7vEV^pqSVKXU#q*uSSQ<=kt$qXaX+&@Aq-aE+Bx;MP9oz zuq1fAT^ocqk}5A2=X`c5A^EMh*Wc}A;Ba-7#)A76k&ng5&sP@bq6d?``eJ#>Bm7H?lvJCmEZmhCY zx)C286+uqP7&JwdyxV{#;I?Nm1mJ5k+ISxQcSdfPU%qhw#3N#Wp>qF`ewg6V@4m7r zClwi)qibj(^J6wbs2uQ%a`^UbqO-STER%^Uifkt{J+y>rh(L8riB zuQy13B6*RrknoN31vT&bX!`dP#TxYPwEdZ{VrT-}-4hZ^aqc^eD(BOyw0XyYpIVjkw9h(gr+V#qs7>SMHOAEQY8DG?+&adV z{)We^xplVkZv7hS@3)AAS%dZTdtT9rg1J1^^c>rn@KxNSdv<}Kr zHZ0Jm)cNNzN?u-G4<9~sKW)4E@vOdf(oF#|#bD;6frX=xP(sRQqnhK&tqTR8bZT$= z^>K=#KhNVM5)jH`JClkeS%teB8v2^Xmj0Y#^aj6<2yw9cq5bI1>2}f6cVPzlte4^D z+V1-LSKYfopm#y3{rM(@dahtHl`&~J8J>sMYSBL~@mt)yh_(?p?c01o3h8r1`-Od- z8F*MDfnkc1kXMQwzuXaV`R~&tCSlLcLBbOljJb>9FF)h?_da#4EJ@dJBxCAS6` zjYJ;kFT~s7#_>UHi?2iSl@%0%57Q+Kt+%@c9Lvp)hDKkIlLd0O01k^kjo?TdxQUdK zQS-Ujf0SYIbQuYC^{7X1k3>r%nT&Ja@CGLV)@{!(NhyhWnMBt@_~i0(7wX^}Erz=9 z2!HZWHLP09X-kYIGP3i04bO^8zjWfQq2Syx{HS2Rgt_RTpf*_{+08tlRL*B-b+Qp9(W@ zEPh{s!=Q61c+O=k{52gYpcOG3j9M?jE}@!^cIajOg6+-MN5PE-#g7e-fDn9ud*N~p zZV@NMct%5J3VFw8#bh^5ay0OyOZ<4xTYApk}! zoe|PLKQ|3K$)Ppv9F|kHN`HIG&=q z(TD`N-npiHjEo$qope+Hw4~i$$(lTrOf^;2Kka_OzkOSGPfvZSQI`HtUfRtGirOWl zrk)l|39xOx2u$jny+sK|;3FcVK^_#G&!LGQ7+S{afZ6Kg|M21Z%zKXXnA=nZNIK+G zT#g{VzfYXO86hQ}iksYNoQZYkmDd@Z#aev7a?uuc#nsa|i9rPOT|#ICM~Ziz{98Ex z`ScvdMQ>P4#7O?-3q$Xs+}vC-qS~uJh-p+Axw$lXRKbXsZcq%Jxe^^nUA2nkv#A|h z@w4N7g3oQX2&=Z_O4MCX(`0W3#;46;3=XgC+_lqQ_8XiPqs32#meR+69UTdHf7cog zCav4`5B~j{>^`A-0Nnsx_&o59+BXd>{36oli!sxHSmaP0>t-uSC={Za|s7P+O!cJoV0G=32qcC6aH z%YBET;(QsY;^uM@+Da04W9+G~iHRFoPn?VRKe9!uwIYsP$DG}#smtnY_fk+#uyjwF ze0=4IZqX>UeN{7nzPubTVmS(7@hzKjTstw=35>Mb+>EQQ z|M~gz$!L{-`_AzQEUK5+4X)`4PP@{+?!wqCyYtfk4=#stWd%d$$w}W{+YXGH z&fKU6PtgbfAUeyja!jA-oJOvzujj0OU<=Z+%Jaj~pR5x<$wWyKSeV(F@eH_)l75XK z-_GxVy@-(Z1QXDVQ+za2`C;7v(dr{VVhH{F?XV_(WU`+-c4Ar`zCA(6(z2^5-u!Yh zMddZo>>P%Hdr4qy;DZ5u|4VJPw4ITqE09*LHYYu4O-tPbt&DcxZ?_EG_nT8wE*39- z#AFwKLeQB!9-s5S6sDomdfy3Ju_u+H*9`rQKZoymM+T_H9(ej7Zf{1XNBH%JZ}bHX zib)s)Rt;+@j!CJ^+R~OC&wCr`vZsKCo2C)!1i;7#M1!^mo!yKC7_yNjm!4sD_-L+J z)6NN}PD{&WPzcK_Wx_cG!wbzGZiqkJs9fk3!`)$`+A3;KH0=M7PHkhx1Dv-Ie}2Y) z*WWc|1lrfT9)A0xTTt-gW0IG?u->J<3?JKS>EZr96}hYH1sPh4OQGQ9R1t7|^2b9% zI;U^jjLH0^*{;fJYiWepxz~ceG5Y`T34f#Is%0(S^Ky(^?c(F_87$3%{h_$vAK%^s zqZ7h`F(_Ym#OW-wDG2N|aMz++<1g)JUVDMFiR^mosCAK{24wN3T=2X^b$pKr(`&}5 zPfdrRPme8R$Fw_)ISAo#fgUN+z>Y@bsjzZ2iJROHdg~)T0{aT~%)ev{fjX7FB{*d48thBsv z=yM(wF%HYf%ng}eSmC#ve33E&D7n+^QTTh=@lziv zZk_Zk>7Cz2g{I)AR263@Cnmapte%WJc`;mPBj6Q|7~p136`%00cwxu| zV_#OTOjyg{6@4r#JBk`G3l$k0u$nzE!F=kp3Fk16` z!-?{un{ujiomn2|OU@EBU@PglXujS3{i-lVA2^aAQPlLR#P#dORhXtr&r!(h=#b+x z*m3K#yb|x?$l`r6FfcL}3^KFewRgp`YOl%gB2BBSt49YZLJ$y<%QwO*rTJ3I@{Z!1 zx4OT;$z%)N-2Mtg0iNsd@anmOgvZpuSf{b{98hB}Tpy-6G7Q{BR?zquR&={fo?FUg)pMl3pr-6aC!NJ2iqvS14vhhE0>g7%8qSvkD%xh2Tf_>3 zA$lbxWdvLir~O3nG0RkEXEN+k7P`70psoT3lF`o4l2hq4>4u#4a)xSc{{CDYEn@SA z^VaCz+&_#>B9q=+*~rMsqGw=mIw--(%5n_$OCY&+*>5z7olLvO_mZCRn=kZ(?(hO< zVwigBy3BYW>+R|zxS{&y=gHq6WUtdb309zo=5{gvPNR?c%%{QywcP?+%&ts+8zgSIu)6Ix&R22e>i)k zPU9=f=niTlL66*t2fKpb4!U*JE9LL2Cl#OEbp!hIGBV=2)q>@@`S^Y-TZBi3_Kc0O zQlqSDkvW}?1x|AJ>A^^2PER-_`uK$$zpP-adamxa7kJrRTFAniv-_diZ~w=;H@**u z#3?=piW+HNb8#Eb5)=ryieyN6*kO2BSD;(tAiaeGeR>ni#ytJ{-)A)zA}KO*B-3KNyI`|#Jkodn2bM867edYRfB-U$R0G(a&i zzoCCtj8}oDdCO1nEW##Ew;%k(_CG+HJv10r#UaY@)gs_=R+HT|ECOK>)ZRJURtqB~ ztt0oTK^-y@-7hX4J&RwTOESeuWyW$x1ak6#>oz)0y_F<_g6R|mh}+1r^?%H>x_Nlp zV*9j9%B*7(rS(?ys}aJdZh=8%wR{J)*M}_F21@;&D+H4&cg+wrk6&E3U>s^6Q3@(A z$}dlK#n0!aT{K4X>J@rRwsE%$YMrhydcXiW4f%+onOxW`7X~42x3LO$~Z_gCW7O(yaCGp(RUVEpdYL)+X zDs_D{h{EOH2MI7E9EAI0Y{S%c^f=#265xMeROEq$-OWBPDW)5Q#H{7brh8Fiu+F#kbGFs2Yykd$9u zVL^%_dowx$7P=2!ZPesA&3|1x+%%!d%iZ4%p*v>34cMCX0LxpOOvJvN(Cf$M1d1X}KQa5O;ZD!wk#6^t^aczOQ^5#^%wB#U1%`6uP3kG+Ajtlh41NO#|NJWBOQeONmr~u+E3*6_k)-3dQ z*B*feZDa<8zW#-nS9Luy@_pXTr05A+PI;3Ov=Ui<8hH5VYt$>-Y31I6ypxmthUp?k z*Ajl$D+H=D${g5846y`dEPwjji&IMZEzE>BCwo`6;8gA*TrKn)-2gGIUpV{MGbeW` z)Ng3=m4TqQDxd!3B8zh|l0xLBf`@eIQS+=zRq*jvD&?I!{VXSjUK@R>5iyH*2mc{R@`xfcQXiH1GW_S`V_qcW z{InSwTlp(Sqp#w9Dpo1UbNVVS8*W5>%G$Sx>)(}W-!ZI^tlp>-VHI@Cw1ReGnkDUY zG*8mk4|mPnp4%yPyp0Rv>AU^(@X$jDS?Cw{)yBuIWKm?bwg8vC9kI?^>V09CxE>6> z`J4i&u3n0|)lT-X(oY&Bn-gv6(XexUX8>7Aq?_Kz4L*S4#|lUoO=uyOW9P4Swf*l> zFI{9$0HS-*TT$dN$%@@jVms-}HtE^lr4sc&jWWV~qe%TZ8)L;afu!6Inbazd;}OI=PL zssZ|P4cd>(d(S(M2t)ayi<@)YGOUs~D$Ji8#K5gMu@soi3mK!)!Si~k8YK=moL9Gdi5usxSK%Ctysg@k`x@J42b?2Z--CK_Di#@=fkPtu~>qYgmt(Pm^Ry$X|`HYQOb( z8`kZSARk5W@V+^&?Y(QIkg3x&obQu8#Q$}66ZDn{K)$M`Xh2oHGuujESL7w-s>TiH|$MF3Lq4SKuBC*bT zq2)?oAQl4DDnXfL>#*;q6v%S_h10yyt-JnH)U&YvoG{Qzqn6beFFpwM(%W;HQ}J%i zE=m6jm9b?^FcAe*g>(bv3}x@^jY$Fwh|#*)serOqvU(kk^I%G4!7e#=VIgk5hY_{^8B(p zxRFPMD7_tKn2;C_3sLqO4+%YdCOqk=*bpsUBk-V z1D%5Vjc0px&9@fHB?%R}-V8}Z$i`bFBOzG<^L8bL#XKY1?d|P49}bmhY0WZZwG=`t zEd8^-t&Jz2$OnZm9eJJ+xt{xYOWQLn+`VPsOyP)$C50sj_BFAqPYfv z-cA35FOBH82FGLs1Y)~@cHZs=o-h@cVqj@W0S zqXn^x9M3%72-zWy`PL^hEdWwVhpus^&lH%~p>=n?=D|*Z(oAN)YWbsNN!IX*;DTKe8 ze*w#o#X`E(B74AJ@YH=2V(>3Hhe=fsy2LoTTAz625&Y_sbDOey-N;!_PtAS>x;Kzi zSENq+D()gb4z@Dy?Gc?Z_bXU))qFjE{x%;(Y@rI+jLN#gbq2{1?Y_3X%f-lflII#h zpXtSG$~9w*;~WdPKf2;d_RpwT^qzCyd4HlZVlX%wul zKbuUcHcY8^&C1QdBZg;FrT?H}g(`)qrlfVSopcly7G}aWTHZz9+_u4MRmZHj8#=VO zQ~qqlc2CDCmgX6YL4;-lf`ZI!nrh{{gJ2m)FN>q`i|Kx9%EOK|F@IWjEe0G>Xz-hN zD@^EviGpNj35;r1Szmo!c?kEtEA?z^YCYkVTDx7TwAn*xnEOSpL8mH`(fjbYs0*e@ z$f?PfIgV@wfwxBVvHHKSn}B8}P+aRt!tv()VC9~#D`kgeC|}%eAQ8|M35yeKv+2J5 zPJpI!#kQ){>XP-!$R@+~i|k4mxrLc!+Wzk>%bez`(#9u3&?&=1)TZ`5Z?Q$~LY ze8(23A1%yY*0dqmH4iSLGr@R`*laO!ij((Aa8qzvYl5DHBqOynip2qEBZ_C?xdh*N z-Q2fHtzWhyM946fz_$8F9U-_GM)?wfZMHd=(R$Mn|LO-rj4;3xns9ZU+NP?v=~$lj>j3&Af|s zJ?h|`kkfYCJ`&_OUlJtROOJNfOD4HC+iRPA08=4R*K4A4Hd4wD_CwFb|HTfgp{vB^wSQA*a|ogsz3Az<0mL9sS|mjF0GjPIbQ0L!9v=u8-{3t+r!iKBB2r~5*e$J z18eK7UA$}GZE7zUzj01Fm2~Xk|7Z-E@8|gT^V$Hcp&y?T_EUZkTeFWrhI|o9LwHVE zAY(C3+>HQz$YXucRJkk4i5$~w)^Gao%zsTQKQY_Ng4M>y;e~!h7d4#`(nN)fb2rA# zQ-784{AJJp45??=6hAf!+7wedDM4NF5Ht4vL??_Prt5PgipsQ1R+FFmM6y}8TGr;F zeF%DQb0+WCVT95wxEc*4dXB~&yhW3KIucWU7j*mcRgTT-N2(v+(mK*EQfZEQb>RVP z?-ZlS8x19L4JcGjc%g(W7}6bn+$AfagkpArN`~UsxY(g9*niI)oy(_sFzt{AXOV`RplWzeNdvm{Q>lYjS3=r^_T#-(T zI5;w6)M6fIohXNCc}H$L#ICgOE^b{5Ah?p>l$DDQ!)SThUXfx_S$6V|s{fl@hwx_qNLnnY{=9m8w_qz<}>i-T2UO^)Y*V;Y${{1`o zlxy%n(ozbJPGn?{*WspMeqM6%FOm3_{rxvl>w%kq0Wm!G4O=;cD1yzj)iAtsmQt%? zy^^ELk9}bD`3oq3#XT5QTr<(@NAu)>%}-U2eo58+6MEoP*n*`wkVAFup4V1je9EkA z-r1>Sc(!N8v!6VX`93>iFeeU`4Gt_%2ooOni7gw?#<1zdfpyesAsjxE_Bl?N%(st} z*jjgf{$-U=_M-t4c9y8)ol=d>)L)|+X)&S>C)5_^kY8J#mGd2_64xHNPo`6eCiID4 z^h<}UBE=!aJk^tL?$Sl3;P>c|SV+2Cpj8!eg6pk$;9-z-rAl<4%-a?U4HyrcnE9Ig zEE#{yUkFETc~Wk&SNJ>X;z#T_at!L-4z55qNVoIatJfA!`fwEOmE|EWld<%Lk z5_EQGh$ltbBTx@>Q87 z_$jW5!}#%3PBN`))@RZihrd2_9&Tt5x!HDZluZPOMN!0m!{0vJT8npCl`HSqWq5w) zTyNZ4j!;KUWBk-5tEJ*#I^IJ+{&LGHIeyN|e6v0O&+nG>rVuJE&U%>cLi7;gKHj7k zTzXHJLOR}peV0>s_%O+batT z_vTC!R(Dc=?ia-Uo~{3Z=Q~pLpIi06o{Mloy=1*U@GTiFxEc2I-_vK75x+-`xF7FB z(S#<_)%t$P-ni7C>d6sIg|sy{Ctq&TQ=?`LRiDkvpZNOj=KH;vNdn(B<>x0cC5Hf0 zp-lXK$~9vIp#k`CdwD{ruy^!RRpt(_thI*T&aR`wOFjsHu4nzzstk*1NUhRhtRh)& zqY`5!D0MbDmI7SVj>*g-pChFZK2~kI*U0hBP8Q0S_sdhK^@Okl_4{x_{J@h8XZYAv zuP6SINwfM?0e_Ld_*iLS#HE0$l5Z^k*cP4p{69Ni z@}Vp7zcjvofnQ0XN-*WjET9{xR?Thg|C+F+K2H-hp6aHBfB9ADQ_Z6Tgi&}XQC5=f zQlaQ)f__KXZ9;*qu>+A$l;nV>XgIa28|luvC^J(q+QjSG8gx z6EpeKLTKS8YNPu=H@(>unJI3sGN>FlIUC~TxGLtwaQ5W*XSlr3E>a~TMf8o*T}i2F z`$ylRMj_xzb9Naj9*m#Rfyad3ZQh*v zboI4wj|J|z`Kk3$uAOtGa9{VQ2y9$IH+g;ihdDdX`Vg1-K%^Uh?#q2DDb=i_^kS~O z!%kHoCo3K;hUm_LdX~a4$>-3Mtb5<32iM-W?lF=%IF4O9k3>6hnXF#e4|?>E&+v0x zsj6x#I{(2+`nX9eB$pL!StXStukZw?YIioOq!zCDAfjG`p?bdc@&c~0Dcam#Lrtxv zOtR)=CyfZ&7uFKO7WK-~HC>U)bO>A&y4c|^@aua_(RPW$+}y0GjYChaH(_MMo@MR9 zl@zZbTnbLj$?nk9c{s+U-7d@ivTga{CjDfUVV`fPpm*hVktPDh^;WAt9xk`v%0)3e z7Bc?~UwQwgfh%8!mO%&4rom5bf_xv#t{opD+kmQyYjOF9xy#3_aL57zlnyX|2f+1W z$|h#-U+P)v*GRta_mwm-GJ*?u6Jd3JaU_taSXhTUf$Y@aap`4QYqy;sETZkU02?>b zo1_kC6Z_K6&aEwdI92C~O?~GviIi{n%Z-&8wZY}uj_<)8%NHM0V@%SN#(nfVE7iw=UeEdynv~US)-IIJC2layYF$)`UByxc<_wo| z&tGj-J(l%Y{B*8Aw{;$;lV6=1_aiMiRp5O3iO3gZB&zRH{~gnt1dy2=9P5|DFBR!O z#@irVpDoNK~$kE5<BA@7&o zv)HKxTYFLC2UpO8Va8wN1QG?o<6)0ySsib?pR%&BMAN&s{8f>C{vEYo#N4TR@@hCH zMj)+&FZlhW^OZ%`z-lcmSM_r2XHb4Jp!hVrF4289|IfJY-k1mbz}wXoQR9E{2;}hL zb@dbMQ?*rk-N`Y5tf!zrLf0^{7}>O-J+jSHPGp~fUmup+Zs;Y?QqD79NuXtne^qHx z*z{Mpq381C*o&TE!2D9j-6Do-=EJDn0@e`U#rhnSp)`**{sJ3Yzw=8OAO_ywT)?*e z{m|{`;z~kN5*ihG*tUFvjq-WUY^uDYiU$&v$N#e~(u6Py_domo%)7QtFFBQHN+hy| zOl%=rT6w+8Z0gm;8A{Giz#p^ZH#w6g{dmagUcz~t%f&pg%58p1ru#G{(m#6w zd$FWU&~+g0@wNroP>ItAzORZ5X*gZx|D-}3o~EyMk&RgUHY5apIw#&Q1#Sml^&;D% z7iu5k%Efam$~B2DR75JK&3?6eo^#w}6Wc7;yA@XL0T04_K)jM|=})?xeyX&9kEn-|vU0Y1^=$Zp z;K+s^veI~&d){9NGyeA`0#4=!w#(i6Qaco_$&j=+0?fLWs?lU${1d<86Ub|7Vh(yz zA^$7$Zu~59D@;Ek#|7CtAo>jzsxHDz|0C?J3g7A9Sq)5tIVh$TIfdPh$&bE;1X#{UG4|O#7%4Iw6|i6}2?{W55JV@reaWAOJM=ma-uT+d zB}jHbtIL0tOxtd(R{hl$-GUm}>k_W`(BHd&Sgnbj2=HHvHc;84-_Q*zsi>TFiNzDL zLg*|pq~N*V|5=WH_iLl((bg!qDfH_`GBw^%Hjv1pW-Z32=f6DeL)-NP8N5T><$MuZ z+8b`UPD@rjCR`R)=k2!{8K#u6R;}MxjJs3YHq?(7X)Or-{&U&4MNOjWboyta#9^wi z%2fHH*H}nz9p_Xh;lnVyXB(=hmKUDxl7DXc5YUc8pTtr;goC?>^d&eL>4BU&ZuoVjJN!ZRpm43M;8>sre&iRsNMtSWvUxS zMM*8QOq?T6@iz`o4!^)vHmt~HghE1JAy>m1Zb* zlh6rkK!j<2?^1u;kf&L`QjD{ZLjeB>aYT^li@ogfmE*>z5aX67bWSKV2XoN~De~JE zNVYVlXg_kFz{2t)lFD?)Uzzk0N=Qa07PpmAe?AEz48)nP98RJlpe6W=y3{0rwac?o zB~O7eqwo1byYB4uBy{n1!yAQZDHktntYr_!B&XPp0NvQ*z#_*>K2M#3(U>#o4(KB}#c zdsh^X2H5aB@_#l^+gy3N4h+oX)VwLEF2i=4cEAYBPN#A|*Fh`emNTf68~*e^2^aWJ zbVF!xZQ^XaEC`!%JtAqG1;XkO?D|AuT{g>~zKLqRWGU2ze{EEw4&_56bCe|fJ)PCW z?Za2~d{ChiYenR!zHKsuH_R1Q#?aCK+Cc2dO-KkgQK7Yfq=;k{n+~AHj^?V2p{RQc z8~gY5QoQ;dL&rhE6^e+%pSdhIBagWM`Z4>K5DU+%;8$#8e?*I#%neuIvP??9N-6(r zA&szjk)*~{#0L9*IG$C#dSzd9LeH!ciLLhMv^^s9L^UT*9$)E(Y_6r{(0TaZUm=O= ze+j9VQ-sYIO_E|)bO*?BP26aOyw;`TkagKxiwxhiT9a|c;Z(Bl(zj(`GPRJ z2TdNomVUbzG&9%(y)RRyOb;nZl$Ju^*dVDblESVJBCI|^q^XJ{m!dR1o2 zlg~C3cIdAkDVZ_%z5If!MwrO}+m%k`pI*J)TO@c<6++}V6l){RqlbSnyjsKU-&t`w zi=IWxDEQ0L9S_BJEjCtUN*n^oTB)JrILJ9vwS7l}4;HM~BS+wf$9 z2lMbA{kctSdg#H8;bfO5DQ%;Bo9BsM74;%I43x0d!xI~lAh-BjA7hU{yJErOq~$HXX*)xPY5>UKLF2fGh;C1(Y;L@ZUGD@<gg;X6pDnFS z@QrZ?n=wu*I=%%1&T#M$5%t3Y+wd`SGpZCdQc3%*_1{0ul|pAO;56U4;84uQd|}|@ zl}7PQ0$G=qbN||UH{wX%lVugubqBQdKsRF{L9v7>+WaYo{AY(xVAxSS7EMuZmTP%|3}$ZhgG$0eGAfvbW2IM zNOyO4Nq2`dNJ>kWbT`rnk^%zK-QC@t`&*pjxktVCzUO{C`;YbPJ(+9HvBn(v8)Frk zp{csNF5s$axnnQ-i~erFQ)CzmItYEkfB8^oGE766*Ri5aQ_^9e%k=yMORgHG|2L0e zdX;EjJv5Y4qxw`DvCcN*l4qPf(xPAX(3C9vMe@;6Rv_RaNE6XukI{$}NyrM+UTP;& z%%iRVb!`LRCx~LKV>8^p2GK9d@Rv_f$ZS~aXTFy8^M~wC+Nucl@4=zXiq(BO@M7Kw zYbrCI*bxn~Xy;oI$K*yToldGF)O_n6zSp^w>UodSW#3SaQQkJ{+q=NZZB%>vfRT3` zyh09|G~A0 zuR}8`5U*TCsM)5_`N5$Ok_)(etv@mn`=*r?m=$RxQPJlt^UIHJZtZ@DmQ7PsRD6ea zYxEkfu$w7bD#{dif;K51a&zU4My6i(jG0ZxzP4JRc`foOpeeEvc$4rM`LZ+p4 z90Q$!gHC@YWtAywy@C0eh7*1_M8zNmNx=OFGb6p{QO>P&T* zC)(VNXk;{pkBPUAQTR+!F}!QlewfTEh;AlJbVj$sG{l%{$!=7;kc68fS2{`QqqMP@ zGfRCaSyFs}hj=E9*~bLdl6Aa2VRtj31R0z$;TV}>Q!C{~EPAK!nFi`sJ>eQ#33oY2 zn78YZ^k*GeI%w`N2f!)_!bL*HWRiO%Xy|_=anGge94RmE*@08jQR~Ku-tI=Xe z3uMK34jb1(GF4}d;P&_IM4^`#pZA~dPAUlXXRtoUVRN}T8+Bd2T<~f*&lZ@I1erHB z+qR-ca7(V|%dp$YYVtF~^$yd-V%cK9qz~a=Du5T7Xh1wQd2x>UglbQ~Cn%XK8~&5+ zjy1Jz>Fx@%O8uv2I8b2f-k|%enqe_}d6Jh}-W%Q$LHQL%Xe5e5Vy_R0;o~YAoN+`b zbum`fWSEvw^7KBOsS2-s_R{eWJZPy9-rs6(7D0NDkdQMpDKH)Ro;IB*p6Gh>zFEe~ ztC=b}V=(wo_!9e`tP-DEI8>L?w1|pLtvG&SaF8mDSa4Q0&C^$~YSNC_;U3 z{mZYQHQy-nOf|=@*W2CZN%Zph=%X=+=>bt2T9#Hgoh&!LW>~XN+77HK6?28*@ch9L70&Br)w%w0lQ z($ir5Ib79#&tiq6X5zGO%U7q~eC)6tbFfc5GWijsJCp*(`eq@K74Olg3>X+u!$=rA zE7-6hN0>NbBn(i{^n?Ok?f`&>1)8+EaEyqaidzyyt-3?NP@^yJeb|u^@gO+ac3B8r$7-wUc13D=J4uz1nXgeV$9H!=b@3q#2+r z^=08EuK8vNB>fO%s@w+T0&z!TWlngsU@B? zHf=|Jie-6%bG-nrYsF;^}dkhpL8<u!IG|5b=oO@)DEFerM^M?^(iqX_q+)(Nt(dBNLgyBVyzW`m!*ROs@qadROcV@=k@wdF6=UCukpoM!1ua-q2jH=%dT)e1w5=N~w zTr4i=EHUB?=kbZHkBOFhzkJ`i#V77rn_CEwZM9i!Hu@fE;BI{GwtDW|a&vQf=W9hS z!@YhsI;cXqjYmYIbNyBg{iIFD^E@55w8dX^%+zxdmNtG#BSy`Ev&JjaUJc(Ob z=@w4FN=Zx{V{iI$YSD9XsM*{O?eBQ>qZjByf%G-WJnEiAW%7%ZNStC$T=!hqh=7p? zPp1fDXi7yxhhS?JRMpfNiN;SXMk@HZV_PYAHPtLoa=b<&xCnM{#-yq5 z<|QrQmBchcinHrK?5kHDNlRh`Puv9`qnt}dv^ZLwovAkw5Pih%)VA^Mkw-&J(%etZRTqYL&l9n^OIt3CK~80}<;D-bUmnoOK$O zku8%d-F$2{JHJ0-2kG8jtP2yX?0c71Obx4~3F)Av`-pIS=J0+vNkJ3giqdAM{v_qH zW}ZQ3hfq+Q#oGqNcgGpFlR(ZPok{OC-o?X;a3CH{>KmSreUzqsyDyE*FFsEm>IRO= zD=VujDBNw#mc=I!oXz@?{Kl^zQRSpk*!($~xV<=O(}yJve`GAZxc@`OGT0v+TOc3H z>#tq_lr%IU85%TPlUoLuaB?2Rl-W9mjSq>__flq-Ovwc>&vd^POT@%NcJJVs-yYPT ztW=KRu*8P7S>C?5?VY{mJPtpwf1{vF&vEmiLcK-f({VUaHWn7Hlt}9fwHxTs0hJ!GjpC920?Fq)PbfFLCD>tZybZ1mneb4Ylv;YC|B zwYM>09i67OjQy~E0c@iMuahV~N@Mor!x%>#9B{uTVqgFx9&huUUh=l4kt_E%rtRF1 ze3I^NYa@P~S%ReSm?bR)vZP2oPDW9UbIsk9l$xI{%`c}aOD*5QZ{|@-F)UR=;u;EO z7(l)}m=7$D4u8j=aUp{yCL>+?sZ(}=Q~pK%4i?zk)EWb-@G*^9XHt~HCgBD_)URSS zLn(4_;7CDwU00d-20 zsU;Dz43*M7Qkk!pog~n-@blG|%@o}FBz(e1ve^=~=MBfyR;pw3zF<(Yhz!5R;S+(y zC*ovM@JSz7&QfONMnEHHbT_Bu>I{r^>|y$_e1W>PjTO~K4D+2H90PNL9kF#vS%+nWtulz6?9eYNf%c!!kl#At3{=c zvJx2g99R35a1*h*R&36Uj*had-_+HIC^X~j^=b2+r+zQziw7$vad7BFh!`+%PyM@c z97529vpq0`M@?t(_cxt5wVqeaPLtGA$QT1gh7wb679VkUAB{{NbC(4zS!zd)i6V^z z^6Nl};hE4@^GhZcaf4ou(N`m}A3g>!Txe%*G5mTYe(a0S1inC+U#Xho*ubO~X7ll! zBAJ0(=Qrh5CT3cSC*DUh-sT#fvy#2wiIqkg!;!Ra@ilCZcEbcePOlOD& zx2$Ht?U0iPp9H_ zNK`&;ia0aDO^ZKq#a^T<{8(n(W9V!agMJj302k^(-+Q*kzU1ymGyK6R2rS-i)kusJmG(PCC9?ATU>cggB4k|$_B8%xRv7)1lG!LujXQUKLRw;<{ zHDq>EPL+|DHKTR69HPW=>>)FIS2+<7d#rdR+b$-Vpa)pU>+(O4Nl5eH`0}=)5(x$N zA?CFOcA*;i8IGYG`2}x7p%Mz-21C_mtr<)19JHLO(V%!E4sM0JjF5dI7g1m00AQ_sJ=Pt;Tqnvz zVrJB{XnRgnG;+Qd8vDM*`*oVh`@(-Q`8Svp2W6rfW>RLL_biV-4xWY}x`e+*F(Al? z4s*wFF1Dnrq_Z`ORV}uM6I>}oW!(#N9#eH&!x<{O0-Qt=j!(goq`XB6gYlj6p@-W) zL%5w;pJ>r2Qmsk?F*N;=3I$T%*bQbe(Q+Jh<%NcnJVv>_xP2qmL;15&4jT3%I}RMl<9$Vj%;lUG^o!KY?W&ndUp2odd5w z+S?>Ixi^u=NmQPWuxPa%&8U%CWGbD>;R;3YXwM>~nK6NZi}Di^F=S>oFH^b$Z4h{9HHa#LvF_Ogz?D;uMT6G@0nGLh(&7R&D9 zQZ&~-l4J3cODduiW7*q!Ck1$G7o&143j|f;if&)k-G-Fyhm_sC57MIjmFjv zzG*Is^W`U(aBj`Wuht!-n#$(4VoG%Agh=J#&z4o#j*gun;ZLP0is9}-%YWHHlvDTZ ziiLAR;j0ue3&mVmRzvsdD^5I_2E}s4xn9a7JOO6^Z&!I*OV?+nZz*!X;TjZ|Ine5I z%*+LvD47LP$i~_zoZo|!#cab^d|gHY5EEwG$SgK#9hRAzwXcN?2npdS&}gv8Ae(Z6 z63`H1z;$V!w0E1Km*<1}T!Y-`4Q>=>h>X=doW`I4fN z&@K{TEy1?0;LBn~Gs>T+ki@N!v~Sd~$5L4CoUp}y2AMmJO2o(li4p_6@X*|#>Trws zUov$IC@CE00Q+Jft5?H;gM)LF`^6wmo)cmQh3({0S9+bFUyVAQoac*)Lgy1urlpEc zX9XTkPaYEOaoU|!d$?hLY4u+8!xtcc)EMBsK5bgnjlyPQm{PUR+`e`JL83&}DyH|U zsm)ZbQZ)6YOxal;Wo9;0FT++GjZWFvZX8Y%e_^Pisg$)*)_jcNCCThJvtcni*mtI=G4k6f&V<#kA|XnT;;>=lJN; zJQH!xg~5)95wBp#RT!TM7FZA+riZg&&&^0Uqv)uo5a{$Zw8Vse$4+_H;j5?6_ZX#r zen^K;by}B7l#T@XQ#K#)FAhk(Gc+#Uc!qEu^{a z3t!RWGVaXMM`OwUn8+%Vr?}-12xa+2*nd8|!u1)8Y@?P`wKy1K!$(U4u&P)IqB#82 zET)j42j8(Cf+#K)|Kfqwl3c>XS#6Wr3C@xl7RjcLH#XKKtgK_qW0SLcXm;%gKXRx( z2XuU`uim}!rEl&Ca4lqZV}X1LA6!9+l$)t*$%loDotKS`oGZ%0^8pdhpuJ-YDeh+L zS&4H)s)DT~&NgWnqJiEkc7BFe80e=^?G$J04q~LaFDUy3jPPp@HC3HAMQ?uznfQdG z$iXwcgVfA}25`23;0H9)SzuaG&>9Gqlq#LHX{6kB_T%lTFGul_MklY_Q z-^NJ9L>G1C7jzNKUbq>YP*N--qq9)z8e^_nrG0~v)khcZeL<;W5#ry`r+}v)qB(p~ z?QUJ$PYKyw67QCr2yX2793DP-dfFhhT|=~$u@)YT(aZfWfHg9W?S)ovgv72HG8yH~ zIxZ3!0!V+E0nV9yYk0@Kx`JueBEjCp$hdBP#BDN5S-JyR;>S+$R})Yi_R>ZfF3p9b=*%dwMEj*V5bI^v|- zd-wE1B%@YTa@~|DeK~|Pflyu^(i^%4G?HR&G!ntDG|}1;rqq8P;|xKFDWM?CtI6f3 zZE9E_q{F^%$QT-QTzLdHzjs!|Ar)C>xLe8-Ov6@!`^74(*6|i8q6Cmz_nvHzFbKX& zBp4%#WELLLpg{)sQFzGAp}k4#Fv6kxBV14oB)D;4u`c{yToMTpPs6tRT{Q(^TZ-bZ zB7{G@B>*je3Tc{ylZtT4>-3Uc<_CUY=0#7Q9M!9~62&yQl)quczsFOcz?$=L^1MC- zY@)n-jHwNU@B#8&tFbihWa@FIAEv=yNTDBhd;bdwL~#{&P4p{?K&l<>sk zzDAKx|Bq2W9%q?1nv$RtLK*abzx7`azM3Wj!}LKES-vLTzy2QIk3Tswa%ev}7)pRJ z3G(F`cR^@&#FIHfBvL>XP)fM@TJ!Oth)K$Z(!hpJ3)Fr$XVifIxAnlNdOu`IATu+} zwaYc%!l-=;4+sbV%AgNMNva{h87Po~5BXCiG5rS7>R)t#pPwVh&v81?PCyh<73+WM z-J<`Op#Kq{4*SEjq z$iHpnuN46QX)5n63z}N7-uvmh|9biAo>=?oKdu$YRPf0t?`N?0^Zz_lPC~LM*!#cU z@F)4Cfg%S-laXg?#(MILp#LwM%N6pDGIt^UuLS+q(lUg>@Rb0V7=};z(|=}IHARH7 z|8mElxw4uFfSAoZX|w+M%m2KVqiurae>+0|THW8zwwfZt+78i4D9F1tLt0EMD6%sv z5h+N<2j(5(&dL!oQY!uTslB*v!;t>)?8#xO$oMo2b*-w8v<{&qEZH^xHldF#g>Ko02DuA{3E8X;O(~lHl=`0woXQKGtmuaFn;F;K4l~2zB6>Z zAH^&`ZAa+C6F^RU%!fnx_uqaR_ISBP1(r(@q~&Oxg89$XMkdNof(Z#SrNH^S_~<8= z_~p{46y@t#gcfPU>93jof7>}246s~ZbL3N{%*_ArhJ{J}d5*@BbxifBtssB^XX&K&H8t zVZ`V^6CvnHm{NAUe!0ebE-h2Ce_k}q5%c&fwNKQ0b~69O)jlxDKm-TIHET%fx1pl4 zx_4LN0Fsu|?L3q0w;zY;Gve!mqRa!iM;%;UPs3^pm7kfkQqz~J-|p%)RQy!T_IO~H zzC*-Hi~|{g*OpU3eSP>;xkaVTpUZ7GIS-rEH8e(yJ8;5`hcyYP&?Tt8Shpv+o~GNZ zNBWmF;SylVLONmYyKP0WH?Hm1KD3zS_hp>5vFbeBB!d*%T4b~X@Kfm~7n(YfYb`Y@ zPBu?Y{Oo!DfL{N^a{7vVa79FU8<%Un2?<>EjEti;;f$tZ}u*Zt^M)RA@ZvvePa)?`SbkM2wql?%l29(g|GqPDkH9o@1taj z0$ZE8^42D)uGW(4^o>g^#?G`ZM%F;TumP>>&;?iG3_b&cb^=_l&C5IA{YE{B+!NsS zl3yL7z*K(2dFe|7#?3Grv(Wg|spst{=luLCJ2T~RWuQWPftGa<+oBBW>{ns1WeVok z8VQsM4JfFnxV87Io{l!%^}u7^FXpOhd)5eKvRQ2;0_`&Ow))vNOHLnC6-y^*DZS^o zsT(@FlclLuHcbn>qB1t~pVzzd@uOHN?dF+T)81Y{j+Ivpu-V*ReEJVWQv^zOXpgqF z{{|=faB58DK~9N;V<~GX->1{_9K^)9ZzOIG`|r~CdcCPX(@aS2Z$&Zrkj!GWaoi)9{MOAnux?2n;@#ca=(T zJRi~jfMT=ha`CeUuqWTq&;@lDV1*-*Y#)^1JNcjga;}jS!OfHI-ANk^SoX9o8_xjvQ)1HVb(?aRIfK- zbl&OiPM!h+S#S1oQ1i5ycV{e08yYh+`3Ho4Dw_pFeh@eqPDZDDX6#GtR_>zGQn&$2 z7T`5mEHWv!T>JGWAr@(#7suYO$y3?F>~U~*xU@pW6PJ4)~Vr(#R?IcL$KHYWnP;tca`c7sW-QAlSAWR>mIl#CFr*ohc#-n4&Uv%q|eGiMg#-# zEkao3^~ZP6nZDEfFR0?Hvk3W#Lb|Qz(!~PQ72J1QO=pau^?u6^NS-%j@UoHq z!T>{h;Jh^V7k#6HxpCt>E{9dQz0I|=R;!&dSX@C`JW5KjpzF;;qO`;;Ba4YUj{R6{ zjj(upptKD7`fD%UHK(lC$#kG%UJAu#ZKAGZvr#4K;#Pc8lG6xme&0t=s9Y`UNkEK7 zRUZFl(GZG2zPewPcV9X5Iy$(&?JeNQA3dIFyB@sTg_IcGfjrqUW$N#(ceKcp;%P-o z&rm763bAlF?$bO1Ivw{&yILQ#NJv~-PL(?$dcsj<6aK6Ud`yvD_Rk?_Yi)8DM}}d_ zL+KiY4kiG>`>?o{ktC9h&Emjj1XLE#pLLU5Zv*H1 zexeN#Tt+&xxeIh|n~W+9^}_#mD84)y02z!JK;5~ha}^3xt+krP;DKF>|KxR@t_uRg zaLm>e5I~XLqnqhK1?qCI&J;~fs=HRN*G4;n3hWTLepbi;h+9>*HSwf#E$cdw!4<`5BH$NlnV z*E+r|@E)ER;TMZmsQyV9QG?qAujz)!jnM*mx~J`Q9OfJQ>+4gUwB9oKa6Fck0nVwm zzRbnut1{=2&|doVjtGv^4^@=>s4SkKc91|>V8UFr>nii4&cI>qf?azcL5nEr{3u;W zaTC{Jn5~7N>%R`}pSu_)$%qFxrzsE6On@08iB#2bpKeFwVsSZ=yJ4Wz(zxu~alA;} zOm*!1qT_Yifosz=<5o;%{^snhe3iPT@T*RF#Ibn2jt-zqGUpe zMveaPECDt`XN=6pixtU1SFV1LH9eMp_;MfF`=YfsS~jR1t|8oCkuXm7{YuQUuWg=_yBRd zwE@oUZmbC3OZ~hDnR!9Cs-w~TJY;)RCfmYCo>&xgylIo^YirOB#-?jv)VUU3ke8YdQk2_7^Z-O_ropC zPd-+^0uNOv(OY2$QKZQ6eiVpSd)>Hh$H;Qq4|kY=HbWy^{fr0Ls&+L4Fn%?CMP>PN z737W1?s@Ba{Gbt!`eM-zcT8K2lKNdQn^;{Rt~a%q?Cy?Z^Vo4EB|?*F=feB4xc%=a zq!bsQbH4!pm5sw5%Y|3+PcotLZ_ifC7Ts2#f}Cg`i=ey9YVVzie&hOTK6Q15292O(|V<($+0o5)`b>k zEn@!9rHFvpSoAd{uH}uZD?V*OqwYeRERR;Be#aow@oLaAaar0qVl&1JU=O6oh}Lc1 zofL!SZ0~8SD{=6|Bn$5^dk5P-`6LYX^Z?jbskDh*1a)=pu88E=s3>uE>xkRhMb_#v zjTgTKa0uZbu9D2^)4 zyDuu4A^&7Aq_;Gy!7IXi*ZiOXL%iFCxUwIKVbr!(9JC_!+OkO2{b42mI;Sco$DtPwo}?I zet0s_VJVtGvk(MUtr`OppGAC<-y#v;QXb2iDaW?=uV*3o{|O^d0{&|eGr_asV?abH z#|8SYw{g7i2P7IX+E@1@7%)3Fi10U0Y}$!gg%FUo2EaWITAN5a^q4E1)8A0($jjqT z$fxSKeD(IR>%6j>t^I7!(=}jY6=d>_x0gsD6AL1|{>j2S(Nr{a0YV|L##@2+0t}B( z*xOQ}m&FmLR&|eM7;h@VsbuZCYy~6i6={q^QyzPtYp^}-RDHJpQz>OIh)#pTi;#$W%@$bG5BM+x_-UmI(CJ~Zk= zlLn@!SMiA~r^~yK;C|yAGuY>Um2CfX z>t8R0%Dt056_Nqr0~0R)u4nl69qX!_>*rw+jMIA)(b3WL54YPuf%IpGgJKgcUhBE{ z_}rCB63Wuj%B|sdkY$ZiV`EbauAsZ_F4?B-fZ4mZccZerw^tjqUe}v)Za0MhuKzw| zPrIo`L+hR3{6W^dP2;tMy1E%3gQWnWJ)~6Ldrjj-cH6H@v1@1vvyMw&o0W@IJT<|9lV(wC~|lf_r~rQ|Z-Z z=f6_s!vqm7L^wxJRmZ}PV)X*v=ZTLT&x5Tvig&=D)?*~(7*-*frh}<|+xykSmP_Q^ zky~;BzXdjzA%@FA*R_3TZO}?fI%JAQp;8muWgK9+uiXJ{=ey0Y4LUzGtd>q^LevK! z;HE`JQV`%rib+c&eE;%LpFGHO%Eit|$>7>oP|>dAcK-l<&_h2(w@D1<^Bw(r-yQ=f z*I&dh%9+1-?4r9n`xa;tL*VKlW831pP!#}n554HQyTtDo&gCrg_c87VeXgm_us(bj!E$Pxhc_0p$y5Ss&fuB_#OhK>@4 z*`CJ#jLE#8QMSotye2_KRrNYS6bBrcoX-o&&FCSSiSUDlwzi$^eLxLRrEbvmLX;hi z*Yn7yE6wFZ4X$fP$<3~JQfvi7)`-)FM0MwTa zV^lqK>(5Yz0)#Ig3FmQl#8x}k0C|dW!t^e!C$TlycJbuu#WNNmAgqCO4xR~0mO`r& z2)>zo$}yD)zU(RAS|e`4!^x&P2LJxyI8ZXWz>6Wn?@^e~(DGFwhm1Un+%E-#{!A1Z z8Og`Utw4kWMh1Xb-_F;MTTGVaL4 zjc2M8?U&UM$y7it7j zketuk!Dep_8Jpr;{5s78aD>*BT3tFTg}c2T>eBers;al9ayV9=r~3)^LSZas-)8`< zV)i8=ZN&s*asHd;X$X2g|Je@(!^;3|E*!qU=exv&xg32}Ct7{NZ=K#8t5aV~!t14J z3&u`fQp?2yZNy3fq`#+-VJ;U9-(-Fpuz1S758+eZT(C5ggPMk%^3n-7t5@*xt)$}R zuz#3d-!h&+j+>?=Vfk?%R@0@sc7E~rI1_#NfN871-s!Z!zDNJyFlt)OgcE=QbQa5% zf31*qnb~-^#J<_jwmYYI_~p`xJsqy)=46&BD6HAMH!=1V)^wXZa}5pE#LfQM=)f8z z&+T2dcXV$8NQm7$&h6w_5!^`+6&#s_j~e4>z5lcIJlfG`Aku0QQZ26&p3bCvFdUaK zn8no5jse|&d^OlbkaI~me@QSk*xQA@n&x(LUAw5pb$unsJXCsi_ROt%cE3?=Hv*tg z6V;wSUoAS!?r8b6_T1}Rx-?#|)>C_*Y1a9NhlX@**A8ZTnLp%tkAoPXU@M4`CR(}y zCQ8MGa+`tu!9?U-lx)I#Fz?Tg0e^;0JjsavF+lF4h6-So@g6>yT#HnDfZA)p$f;@O zwrx2~MY=wG!1qwKwx)~tdS7lSuCM>{-s1s4ucM&Guv*QCu?q*5`HKVsz4Ey|O2ux9 zS5j`9VrGU@!m3~3?0+gOY&@7r4tS1j{F>Bq5NKg`LSWWi?VTYYVNVs8!*ABQYW7gO zR(v{e&pVSm%9(&~=g0RRZtbFbofdSQpWCWMLASu20a~i7b8%I**@o1SSC>H*2*+VI z-dT$Yc!mvhIjaD?J)L-vVeRZ%bZp&TrFz$MT>ju$>n8W{ow$?IoE$?5*2Lrip54Y( za%pkd-nj;wLqLL7>aWwwmpHkU zg=s;{1%c{S86G;OO;%g%AmXuCPKBKZV^tvQ#%2qq8H`YfH&^&u8+9|=-pQjXefM#NY*YUT&V2 z*M2`2F93o!Z!o$@Jz4R$zb=RC6(s^xGz!3;uj(vn7xwO#zc*f6PN;ZRO*ise<7n*I zx!2mSqYg; z#qYyq-lK{A#y!EIm*LUf2OTQ*S+m^Eyg=3S4Yi(YM{E6!MjE@}_X=5_2b;8wh{3zt z;1-tq2)!FtY(T%Bw(-ew^8e7U@5v$agYW>BegIbkH0%@KKg_lYnk76K^j-+O zd1s!PzI`4`v``O)gzB^y1^&?43e9>Oe6gEZZvRp1x^1fb7}~3`Dw7LeSNQG0GoW=u z(yQew$6Xc44u2rdiSKcxofc2PYPE0$c%LJS1kU?CI`w<*X|iB`Kqkr{L^i{<)Vgsw z-u1Kze&lGHOw$}6v7_)F>iD|}KR9^Q%ZyRiRn71X6}D9yhMiFDG8;?F3^uE14>&~{bB=9=%@dzz=l zr8So$YG+)SDM5SIDe%xv;BkHQl3iLYez6QswXNI^zmq|zX)-uuJ%&6BL< zcE%j3&C_o?x~f_kFczQL-RPP?z6N zo~xhQcu7d$EUpzZTp!*XtzNDL;jHuWUYcq21wVEolF038F|@r2%$3AwK0di6pow1W zP}%_Ooz26%XJ#sD*9AWx)1elNGYga?uQ+>Cp_x)G&UffusL3yTV`;d@gnnDdi(Z>2!uoHZ3fi=JW|dnyU38urX1IC|QCcCnr}V0~K>DN}{ryeB0FVz>)L3 zK1rtx?5ug=JonyPi^#fEXD}dfL{3R#rLJGFjcU%=isFe+>7&{lYAIx0T)UU`sUjGD^zn3|0kWD=UOW<@(z9R%=IJ1Sr?_&&UCd zHmQ*&@-yd@ZnpL)!x-kZl*D-HqE0o%-#3PA5T|92Wm?-0S0)wkdtIu zCrryZ!&FEUUFjdZB~cQ=0RaPW{1O0W%IE}eW{#1M;k%86$5ZyYv#pI`B z(#FtS1Ogtaj91dE2`RAxiKL_pUp4u_Tu-Az%hb*`R%#cn__Bw;Cvrt4Oh%wcZ;-iS z&FOgEi?ai^=SCmnQ@C`NQ<}6Y&!$?}A5R_7{O!}RTg1(8w|Mfd=MQH^Ju-mL@KLBz zIPY)ZHnopwClS;oCA}ddME*mQgLyxOe)RbtmC@u){M$}+37SThp{{TYn9ESOQfZDG zZ2o%&(npfe`29kXZCYoj)KY zVaPbYztAK(09tByz7nud)wYvPVUQWfueywX3D*$nG(J{|P4kDju0VQ1vUSB3qc&f9Rk>i=y2xa@aopRu-Od>6&01` z=N2fxWgxSE3ko8cdo<3I{e=Nb5i=)fXRj8XmJoP>l-q2Hqw1#~wt8c1bz z{q57_(5DNIlmR<~E=94uoS3}eEHN)BUb`L2w+HXU%0?AAz3#395pXAZ5EkmKPPg+@ zrTqb=g}t2^CXLV@fbYJ$#~Fdg;@cw!VSNBUIAyt@zHOcOZWCd0Fpc5f!R_L+<@`75 zOv=>vMsMVmgpzISg2zeH+}v`642rbTHhDoWcFvtM$3 z%a5D&(p`iOU5vU_R#pZ&6;Z$iCrZJRr+ZLINA4nXW2QI{?=S%{Gw71H-$vlh=Ap zk23LDD3Hw&^kFkIboH~rTS3zN_%T+9boenhx?Amq9Ef{)`v=Lg(=A^wo?PDhLD9n| zkP06e3lY%F5pAv>cx9Js!BopVT5a09-SY?HK~o!Z^O@bU#_Y+ea_6sB^8w6{uar#@ zkNVQxmCl!ZcENRdmuSx;jd$I43V)RqY)>Y8S8v|I2=bicW&As`bUzyTaw3B3jKtAa zRj(D#x!?M8eoRC<$0q*a?eRr}^+|dc^B=x!@;Y!_>f`uCM96AP%52O=kY*_XZOksE=*0Dp)_v#%3Dq**slIGI zCnQgTP-?1TQ24}{HdM2#QmEA9c@K4DN97=a^Y&tQXFY<)spYQM?F$~$dn485@Ct)2 ziyDnIaep(7@8s!jhL_8yI@r%da9Tx}J<46=GZ%BhNlD#3L2vIr${Q3uiFheDSK7F; zv};3iwY_O{6715p@AR%=ZMW4gLR?Z(l~S22CNJOpHg%F0Xu<{NUjgd_gAQ~$b2^~O zD8#MS!1Ge9<38Ya53z2#CqO_r2TQHgpatFU@B*z)Gx$>)Up4ja>{xg`xK7Vurd`lW zwP3M1&PY)S9v%b1&+iQd1EiA8`0Vyd04U?`a~6)LTD>9gsMqzmkX4qw4S>4=p2s8Y z&0+H(cIMk7`U-9IEG4F!E2{=LB6z5ai;5St&+2)F)gI%WH={%dJ_nRT&5MEhIU2iz z##nQGeR{&Mh3@6hygJJ)bYW;u9y?;_!g{K z00KI+0?EwH#^;&o1VVT6!~i@X;5nX*9Sw*-{nHs8eXWhXL{B_z zvZ+Fsap>?^Vcykym3aI$o8tZwDA6kBE0X}_DG*-{5t2H%-aDMn3(OkX)s1($l16_< zG)cwR1T!okXD)B$0L;v>I{fZq;N*Qlh&~dG@SheAtT4I}Arc@U2)sT`21`ayyer29 zN2VQWrj80ac#X4|2LKAKP#(K%{}d%3iUSUItgzfC>U74GAN`U4{5gvL@jt&Gvbqf= z@iAIh*QmVs4T=vMTu^@^;%KSI2HTpcX6H+qV7b)Lxr$HhftKFpU~c$uAeEERWB?n55bQsEU|b<6l;B+lQ<%yA)6c*Ck8QMf zLRny0j67BfgqZg~e*`eV=w%THBb5y%;{SOxFk=b{AMhGga0A=_u+-nyLg*7I>Mc+c zdD*9K@IQ!ycc_LIRwG@1H(PTwpNc9>)Y;|2*`M zxjw`26+m$_ON?6{XY&5jUjHD5i_0xT#=YnzlfS8*0%jqD`Upp**;#HjHSK8swZY|k zvye7WnWJA>>WnV7H_y+@phFc)Z7>f2M?kvYd$`Vb>bGT29mK9sOHGt-mae~yIH(;w zyiU1(UA83T%^0gd zOj~~o2%kY&V6}sTDeTc-F^QIhgJ;NB!Xcx4;{v_1z9rK9L{mOFVkBm#kTrF$widK$ zDhRdDM~mM|NAAmfl|@_wpMwteso!@Qm()?7Djzrz+W7N>iI_Y=DHrr+MCP=03H_U0 zd@7cXRo`JKPPNWk=`E!KBaaKMVZa-PAP=)0(J%gGU>yK{uk4GOG2=r+QH%yM}36` zX98XM_~U9dH}j~G^85OG;#K8%Sreo?hm$<7;)NYW3&tULtP5h$=KA7#Iv3uc`oJP2 z71IDk1KH$xymhA&N-PwfNYg9JfNIj@rhgx|YC(!gk8LMC`ByIh`opV2II;nynalZG z+|AF6Yz^o44WMViORpWKf+#B=P)KDYWD(0WH=P&j%R}K=<72l3|2hsxd$4S;pP<4F z(S|0IY}N+94ul2uSBXxRrca-2O_+T_C$n4|b1mu$N>HIA?h-rGS|+pA;X1qRHM#5< zJ#s!g)Jo%RjL*~+{Lr%&FE)n1z*TV=q5a|`w(aqV9Y7bhfe@m<#my2t%97L?e4EJx z2@)3g0uu-e%j5G3&lhK&H&+E}uB~vA;jiZrOLnZNbFSR|PX@H>-|oB_7eXUXQ!{iu zziUl`irgv?zU&9)-r5dR!uVnlBy>yRo#HI5N_l^{#sWtxoS>s|kO@vhSS> zC`;#6;Bkobu6lg@@mTFK|KfR6fY;51xUJz!> z892A7o+{jD>{ibsR~m|qm4tjwusvHztJ3Xy-h=x5@YOQBGTv>J?A8$^RoGSYus*4L zOhZ&w2femQ9zF_6-5cJE%06ndyw{*&9(hD@)}J!I5>-hT9!MKLC zB|EuJWE4fQB?hDC^-21FjJ;!cW!ttc9NV^8v7J;TZ|_68W-uEc*4-;~K-ub{nlOnSelgXE`s%CWY|)6e zHdw}*kp0so*^O7hMr}#Mq}S{xxg{87Q|fW!Fr+w~1-ocF+=rJbk+qy{3&`%IirmOv-cP%XpGdb?-1u|eMLfUWcqh0i-BC?D{y)`EvN{hhRI zMxX-~Izk8x%O3(vy4&R(n`0gfcAB+SlV;ZPh7Y?b88U8o8tn_sS5`!`yNBI#A^hS+ z?l${(0HG>U)j4ud6gT17@h>6!%njPD9jZxFT*t+Dq@kv0zo*-YQhheNj5R9I*tNkU z4tvApkH?7?LU>)7^C|tlDht?HR$cLFoZdc@3kOb)%^0H zklQXo9x~+iuFcAPAq0(0{Ft9&XPe`HFt+0cw?ch~s3Z zRsJUZ7A9gta7s2k!De4Q!tDF~E(>lYVR}W0Ff?d8h_EDz^nTt>^Net$At1ENifIYu zfv+%pdkYsrcI0;SkGNta(R~GqGiZ-v2n-I$U_$I~Z&TkG$QtH+O3 zDqG)FD_RXNUI~*)BK9mF{AoefP-2f%3z@ReVA&X|B%tEJm=_X=^mpur zu>vYBtd(N$9x3P$tosdc?#3->GmCC3?L|v-khM_XaO0D_ttHL7*M~}uld;)9gm4X!LFm7QvBKuH-><03EqwejAe{wI zy`L8zTZLJ{f4sieRq^8y=^8<+d-Kn2tHQ;;#2)3oY~yV>+Bea>0ix zLX9So(1=8SXCBuh#!%jJa=*f@CB4e{s7!XrsCSD`F6&7bDWoF_|J0f)2m)gN!r^=8 zJv->KJ3)>5dl&JYUmZ70A05;w1_rqUTPLQ1CEVV-segjRrTL$TVE)AB_Ls~+(U?&- z<@!N_Qz$OyQFDrxEj-N@Cv94MEW5q}aDTQtekp&npjp~nOHX;IdL<2!0L`os$&_taQrq0AP!5DA<8dEuJ;U;}`Ol2NP#19-T!Js|{4Fe-#=K zkYHD;*o->XKg^{k&ePp)vNM5EFKe{}g;PYW0G--$qE~&$fVXjhPc4-q&A0oj3|g1N zl8Yy!uj08r3bb8M*h9n+7ZZ_=#As}9uwNGmP~e>;-e2VZ3MwdIz(t~g5%ZQ4+GwD zvh2_v$$;knM7ESh`7l|Id*xzU&bg?8uGW?Z7sY!Xth0&Y_RI0#lL8%++Q+N7tXL#?-46AA+zdm)o$;! z9jJYC$l2xk_+9Iy_UjoHhZnif`4VT2=u;xt@S?)g3Z9D)NT-d-C3;t%fbDJ zsY{W327X@@XZGWf{9h5}KMrbSK2NCS|NNoOeM|8lzxWqGgbr#|R!jIFru_FupFtqn zU(Z!l89?OP|K%?JgG^E4VIN`VX;k5V$@{zhuaKlLkYKCM#~7i1FY3Rh{I5@SAxPAM zvhWJ68Xu>!|5v)e?_Pw55Bisn_*d2P?*{Gpgio;9G;CfVui3GAhLcu( zYd!u46t%hD(I$)~QsMuFOt^8E9_PRRzrPQdum_b%*KRw$3yeFYFhFN2w=e#0vHrV@ z|Me-1JG4C9!~D9sRRQEUb-zm7KfnChfug&kyyvR)r^5eK?M)&u1Llvzti)8f|2~<2 z{{A0h2vuPDjw44*UoQUNE$-jm-c3O;10t%Ia5W8QV&x*S|HUs*PGQ`iVCTl5Qy|-x zYeoL&bpQQTpuh}#EtOQq5oyvO{uitG>_AB(!eLbgxp*+@`QHTfkA?mv*gyZ*8~`$@ z<_s=I^^bz;|2i|2cGyd~hRru`ZN7i$-+vfzp`hHv*hz&+|MP2$Znq$4ot;>g6yuqO>|)*2lzI@jVSxHiOS)yw1t5pV@M+M~0%Tivg~7H8Z{>2<|j zO|j&{|NA7m1%V00T){6vnsvl45P4#NcrN>ZN?opxhm8{>4GxV^h)!-$dqZSHAk@ud zfP@814E?KGJZ2YYJR&rrhy=Y+`pcP39^L8piFjWZ!{UO1+?5O64fO-`^YJ)snsI0M zy*xZ7viN*<28Hxxck2eMW}3|dUPFk4JmxAqf+mwKjaZ#tWS4VLi$ZJIr@sEOy34woew6a@=3gdo}<{l3`Zsk2*h1?Bos$0rVAcD z-B@@s?=dQGzYuparIQHvG8^{_`aIvx?7^!R%Qrjjw8G`^kx9N6nMX91suTgR7@l7q zF6`GjYrP&O#Jk34`)J(C5=;F)X_ERpU*t)~T)lTp?hGL>+Ur@%^6%_nmRas#ZTO_n zHo}H2CKM|&>OETAXIL&)>8r!~l0idAMd3F{rEm(29PR7?r+F6Scb3j$ZRP)aAezwY zvBnWtxssXGB)_Q#e^oGE{Y!BR& zYgSk2HH;niSFgqhEmm(pM2W@2p>sw;$Yy>T-xMYn4Mie<|CLbKt%}d?AO_S0wW8!` zXk?;#o3-JYjk1_JpP!X^I`XT@7GtTkc${C8S;aqfL7#bDyl2i9E6S&GFJBjm!YMQu z=<-0JerylOCVkR|ieUn7KAp^Z^E$c~cftF}5IQirayppMy&uX>pp++KF)7fjHqdWz zas*O|SKRCxFgxrrzw`V&h{oq4GT4=jA<7bP^L!1SZhR)z1whF9S$MV7Th6GL>(F^z zCjvFq_wBh0pR1}uzsc$*IjRQ@iSBLyw%I54M%sG!RB|@~YcQ_As}+n-g?5w~=gEP} z1Tdg4Xy+%*gFa0hRk!g~z>Z`s0|4`RLhSZCM#dMiT+z_;`Fy{hX0%E@d~O%@wu^Gr zw5tl^9L)Sx%K24tat2+#K6lGWd!yL8na6leJ?8n}$Kf#P!Jvy?$93k0Wbb8*ajvNF|xx8Q4I{={z3^vD_oB-`$G6X_;7_ zWJDI5GG}Qr8M$$=qY@>~ya5ND0Pn#A6DBZuElf4{gk*64u*u9;w`D z0$jpJS66tXVxkP9=}tb)34WP8sI;`=;>G=OVv0inuUlxYB(QFg5`bS9!yh)Z%^osz zD?FvLC??CFEt2+0(9vWHUcjM1KF= zx0$Uf6+W)$4MqvA>mVpP)aQE;iJ|cB4Mk32HUF_tIa45=fFvRV&VFm^cCpUkejN{y zLAE(49Ig7)inUZ46K1X5%$3{*ce)JG6Vlq8jA}u~nDdQ49C-gH#JJH}iuwCY8s@qjHK@yzWA~d`>14M{SW7BFAtY7glvs)rFnhObj(Yz%EcOe zRO2TC-VTOq&nnRimAqJVIwE^Sd>RCM-@(tEr@EE3YEc#C(h{hozx!#|eX(B5rCflx z?bhfb@dRm=?&bhyG>3RxgJc2K-#=fO6f$Q^$5Z?+#wzn^jO@g_?J3V8{~m=3z~f{j z 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�ac<{w14 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 literal 0 HcmV?d00001 diff --git a/site/static/img/wasm-extension.png b/site/static/img/wasm-extension.png new file mode 100644 index 0000000000000000000000000000000000000000..6bac9f46a3f4065a10c762a77c4a1f61fc3c8b69 GIT binary patch literal 55654 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 YS{vWMmOYgh@vH>Zlc$vm6wclFUkn%RPXGV_ literal 0 HcmV?d00001 diff --git a/test/benchmark/benchmark_report.md b/test/benchmark/benchmark_report.md deleted file mode 100644 index 5821d67840f..00000000000 --- a/test/benchmark/benchmark_report.md +++ /dev/null @@ -1,992 +0,0 @@ -# Benchmark Report - -Benchmark test settings: - -|RPS |Connections|Duration (Seconds)|CPU Limits (m)|Memory Limits (MiB)| -|- |- |- |- |- | -|10000|100 |90 |1000 |2048 | - -## Test: ScaleHTTPRoute - -Fixed one Gateway and different scales of HTTPRoutes. - - -### Results - -Click to see the full results. - - -

-scale-up-httproutes-10 - -```plaintext -Warning: 29 02:48:14.546][1][warning][misc] [external/envoy/source/common/protobuf/message_validator_impl.cc:21] Deprecated field: type envoy.config.core.v3.HeaderValueOption Using deprecated option 'envoy.config.core.v3.HeaderValueOption.append' from file base.proto. This configuration will be removed from Envoy soon. Please see https://www.envoyproxy.io/docs/envoy/latest/version_history/version_history for details. If continued use of this field is absolutely necessary, see https://www.envoyproxy.io/docs/envoy/latest/configuration/operations/runtime#using-runtime-overrides-for-deprecated-features for how to apply a temporary and highly discouraged override. -[02:48:14.547222][1][I] Detected 4 (v)CPUs with affinity.. -[02:48:14.547235][1][I] Starting 4 threads / event loops. Time limit: 30 seconds. -[02:48:14.547237][1][I] Global targets: 400 connections and 40000 calls per second. -[02:48:14.547239][1][I] (Per-worker targets: 100 connections and 10000 calls per second) -[02:48:45.249233][18][I] Stopping after 30000 ms. Initiated: 63756 / Completed: 63740. (Completion rate was 2124.6656043338644 per second.) -[02:48:45.249278][21][I] Stopping after 30000 ms. Initiated: 13846 / Completed: 13844. (Completion rate was 461.4664974622843 per second.) -[02:48:45.249278][19][I] Stopping after 30000 ms. Initiated: 79156 / Completed: 79137. (Completion rate was 2637.8965707344582 per second.) -[02:48:45.249494][23][I] Stopping after 30000 ms. Initiated: 20224 / Completed: 20222. (Completion rate was 674.0621055130861 per second.) -Nighthawk - A layer 7 protocol benchmarking tool. - -benchmark_http_client.latency_2xx (176582 samples) - min: 0s 000ms 358us | mean: 0s 006ms 272us | max: 0s 070ms 623us | pstdev: 0s 011ms 440us - - Percentile Count Value - 0.5 88291 0s 002ms 898us - 0.75 132439 0s 004ms 643us - 0.8 141270 0s 005ms 297us - 0.9 158924 0s 008ms 790us - 0.95 167754 0s 045ms 185us - 0.990625 174928 0s 054ms 206us - 0.99902344 176410 0s 059ms 482us - -Queueing and connection setup latency (176621 samples) - min: 0s 000ms 001us | mean: 0s 000ms 012us | max: 0s 024ms 204us | pstdev: 0s 000ms 165us -[02:48:50.882431][18][I] Wait for the connection pool drain timed out, proceeding to hard shutdown. - - Percentile Count Value - 0.5 88528 0s 000ms 003us - 0.75 132525 0s 000ms 008us - 0.8 141340 0s 000ms 009us - 0.9 159044 0s 000ms 010us - 0.95 167794 0s 000ms 011us - 0.990625 174966 0s 000ms 077us - 0.99902344 176449 0s 001ms 431us - -Request start to response end (176582 samples) - min: 0s 000ms 358us | mean: 0s 006ms 271us | max: 0s 070ms 623us | pstdev: 0s 011ms 440us - - Percentile Count Value - 0.5 88293 0s 002ms 897us - 0.75 132441 0s 004ms 643us - 0.8 141267 0s 005ms 296us - 0.9 158924 0s 008ms 788us - 0.95 167754 0s 045ms 185us - 0.990625 174928 0s 054ms 204us - 0.99902344 176410 0s 059ms 473us - -Response body size in bytes (176582 samples) - min: 10 | mean: 10 | max: 10 | pstdev: 0 - -Response header size in bytes (176582 samples) - min: 110 | mean: 110 | max: 110 | pstdev: 0 - -Blocking. Results are skewed when significant numbers are reported here. (53988 samples) - min: 0s 000ms 035us | mean: 0s 002ms 220us | max: 0s 067ms 391us | pstdev: 0s 006ms 778us - - Percentile Count Value - 0.5 26994 0s 000ms 766us - 0.75 40492 0s 001ms 732us - 0.8 43191 0s 002ms 062us - 0.9 48590 0s 003ms 222us - 0.95 51289 0s 004ms 958us - 0.990625 53482 0s 048ms 125us - 0.99902344 53936 0s 055ms 597us - -Initiation to completion (176943 samples) - min: 0s 000ms 003us | mean: 0s 006ms 351us | max: 0s 071ms 114us | pstdev: 0s 011ms 455us - - Percentile Count Value - 0.5 88472 0s 002ms 960us - 0.75 132709 0s 004ms 735us - 0.8 141556 0s 005ms 408us - 0.9 159249 0s 009ms 012us - 0.95 168096 0s 045ms 185us - 0.990625 175285 0s 054ms 288us - 0.99902344 176771 0s 059ms 691us - -Counter Value Per second -benchmark.http_2xx 176582 5886.05 -benchmark.pool_overflow 361 12.03 -cluster_manager.cluster_added 4 0.13 -default.total_match_count 4 0.13 -membership_change 4 0.13 -runtime.load_success 1 0.03 -runtime.override_dir_not_exists 1 0.03 -upstream_cx_http1_total 39 1.30 -upstream_cx_rx_bytes_total 27723374 924110.39 -upstream_cx_total 39 1.30 -upstream_cx_tx_bytes_total 7594703 253156.20 -upstream_rq_pending_overflow 361 12.03 -upstream_rq_pending_total 39 1.30 -upstream_rq_total 176621 5887.35 - -[02:48:55.883737][19][I] Wait for the connection pool drain timed out, proceeding to hard shutdown. -[02:49:00.884904][21][I] Wait for the connection pool drain timed out, proceeding to hard shutdown. -[02:49:05.885509][23][I] Wait for the connection pool drain timed out, proceeding to hard shutdown. -[02:49:05.886786][1][I] Done. - -``` - -
- -
-scale-up-httproutes-50 - -```plaintext -Warning: 30 09:50:00.929][1][warning][misc] [external/envoy/source/common/protobuf/message_validator_impl.cc:21] Deprecated field: type envoy.config.core.v3.HeaderValueOption Using deprecated option 'envoy.config.core.v3.HeaderValueOption.append' from file base.proto. This configuration will be removed from Envoy soon. Please see https://www.envoyproxy.io/docs/envoy/latest/version_history/version_history for details. If continued use of this field is absolutely necessary, see https://www.envoyproxy.io/docs/envoy/latest/configuration/operations/runtime#using-runtime-overrides-for-deprecated-features for how to apply a temporary and highly discouraged override. -[09:50:00.930463][1][I] Detected 4 (v)CPUs with affinity.. -[09:50:00.930476][1][I] Starting 4 threads / event loops. Time limit: 90 seconds. -[09:50:00.930478][1][I] Global targets: 400 connections and 40000 calls per second. -[09:50:00.930480][1][I] (Per-worker targets: 100 connections and 10000 calls per second) -[09:51:31.632205][18][I] Stopping after 90000 ms. Initiated: 132984 / Completed: 132975. (Completion rate was 1477.4999671666674 per second.) -[09:51:31.632253][19][I] Stopping after 90000 ms. Initiated: 184337 / Completed: 184323. (Completion rate was 2048.0327871912564 per second.) -[09:51:31.632393][23][I] Stopping after 90000 ms. Initiated: 119466 / Completed: 119458. (Completion rate was 1327.309400356773 per second.) -[09:51:31.632493][21][I] Stopping after 90000 ms. Initiated: 98553 / Completed: 98547. (Completion rate was 1094.963746763342 per second.) -[09:51:37.272832][18][I] Wait for the connection pool drain timed out, proceeding to hard shutdown. -Nighthawk - A layer 7 protocol benchmarking tool. - -benchmark_http_client.latency_2xx (534940 samples) - min: 0s 000ms 324us | mean: 0s 006ms 013us | max: 0s 080ms 613us | pstdev: 0s 011ms 899us - - Percentile Count Value - 0.5 267470 0s 002ms 663us - 0.75 401207 0s 004ms 128us - 0.8 427956 0s 004ms 645us - 0.9 481449 0s 006ms 891us - 0.95 508194 0s 048ms 232us - 0.990625 529927 0s 055ms 224us - 0.99902344 534418 0s 061ms 255us - -Queueing and connection setup latency (534977 samples) - min: 0s 000ms 001us | mean: 0s 000ms 012us | max: 0s 026ms 379us | pstdev: 0s 000ms 131us - - Percentile Count Value - 0.5 267828 0s 000ms 003us - 0.75 401520 0s 000ms 008us - 0.8 427993 0s 000ms 009us - 0.9 482090 0s 000ms 009us - 0.95 508250 0s 000ms 010us - 0.990625 529962 0s 000ms 132us - 0.99902344 534455 0s 001ms 630us - -Request start to response end (534940 samples) - min: 0s 000ms 324us | mean: 0s 006ms 012us | max: 0s 080ms 613us | pstdev: 0s 011ms 899us - - Percentile Count Value - 0.5 267484 0s 002ms 663us - 0.75 401210 0s 004ms 127us - 0.8 427955 0s 004ms 645us - 0.9 481446 0s 006ms 889us - 0.95 508195 0s 048ms 232us - 0.990625 529929 0s 055ms 224us - 0.99902344 534418 0s 061ms 251us - -Response body size in bytes (534940 samples) - min: 10 | mean: 10 | max: 10 | pstdev: 0 - -Response header size in bytes (534940 samples) - min: 110 | mean: 110 | max: 110 | pstdev: 0 - -Blocking. Results are skewed when significant numbers are reported here. (176008 samples) - min: 0s 000ms 036us | mean: 0s 002ms 044us | max: 0s 073ms 248us | pstdev: 0s 006ms 744us - - Percentile Count Value - 0.5 88005 0s 000ms 734us - 0.75 132008 0s 001ms 534us - 0.8 140807 0s 001ms 802us - 0.9 158410 0s 002ms 792us - 0.95 167208 0s 004ms 190us - 0.990625 174359 0s 047ms 886us - 0.99902344 175837 0s 053ms 557us - -Initiation to completion (535303 samples) - min: 0s 000ms 004us | mean: 0s 006ms 072us | max: 0s 080ms 633us | pstdev: 0s 011ms 900us - - Percentile Count Value - 0.5 267665 0s 002ms 717us - 0.75 401487 0s 004ms 204us - 0.8 428258 0s 004ms 732us - 0.9 481775 0s 007ms 022us - 0.95 508538 0s 048ms 248us - 0.990625 530287 0s 055ms 298us - 0.99902344 534781 0s 061ms 421us - -Counter Value Per second -benchmark.http_2xx 534940 5943.77 -benchmark.pool_overflow 363 4.03 -cluster_manager.cluster_added 4 0.04 -default.total_match_count 4 0.04 -membership_change 4 0.04 -runtime.load_success 1 0.01 -runtime.override_dir_not_exists 1 0.01 -upstream_cx_http1_total 37 0.41 -upstream_cx_rx_bytes_total 83985580 933172.12 -upstream_cx_total 37 0.41 -upstream_cx_tx_bytes_total 23004011 255599.85 -upstream_rq_pending_overflow 363 4.03 -upstream_rq_pending_total 37 0.41 -upstream_rq_total 534977 5944.18 - -[09:51:42.273918][19][I] Wait for the connection pool drain timed out, proceeding to hard shutdown. -[09:51:47.274994][21][I] Wait for the connection pool drain timed out, proceeding to hard shutdown. -[09:51:52.275699][23][I] Wait for the connection pool drain timed out, proceeding to hard shutdown. -[09:51:52.277010][1][I] Done. - -``` - -
- -
-scale-up-httproutes-100 - -```plaintext -Warning: 30 09:52:08.843][1][warning][misc] [external/envoy/source/common/protobuf/message_validator_impl.cc:21] Deprecated field: type envoy.config.core.v3.HeaderValueOption Using deprecated option 'envoy.config.core.v3.HeaderValueOption.append' from file base.proto. This configuration will be removed from Envoy soon. Please see https://www.envoyproxy.io/docs/envoy/latest/version_history/version_history for details. If continued use of this field is absolutely necessary, see https://www.envoyproxy.io/docs/envoy/latest/configuration/operations/runtime#using-runtime-overrides-for-deprecated-features for how to apply a temporary and highly discouraged override. -[09:52:08.843704][1][I] Detected 4 (v)CPUs with affinity.. -[09:52:08.843716][1][I] Starting 4 threads / event loops. Time limit: 90 seconds. -[09:52:08.843719][1][I] Global targets: 400 connections and 40000 calls per second. -[09:52:08.843720][1][I] (Per-worker targets: 100 connections and 10000 calls per second) -[09:53:39.545479][18][I] Stopping after 90000 ms. Initiated: 60881 / Completed: 60878. (Completion rate was 676.421921590257 per second.) -[09:53:39.545649][23][I] Stopping after 90000 ms. Initiated: 77409 / Completed: 77407. (Completion rate was 860.076458993874 per second.) -[09:53:39.545773][19][I] Stopping after 90000 ms. Initiated: 271217 / Completed: 271198. (Completion rate was 3013.300631521137 per second.) -[09:53:39.546416][21][I] Stopping after 90000 ms. Initiated: 123329 / Completed: 123321. (Completion rate was 1370.219189626365 per second.) -[09:53:45.183623][18][I] Wait for the connection pool drain timed out, proceeding to hard shutdown. -Nighthawk - A layer 7 protocol benchmarking tool. - -benchmark_http_client.latency_2xx (532441 samples) - min: 0s 000ms 359us | mean: 0s 006ms 029us | max: 0s 090ms 263us | pstdev: 0s 011ms 870us - - Percentile Count Value - 0.5 266227 0s 002ms 698us - 0.75 399344 0s 004ms 228us - 0.8 425959 0s 004ms 757us - 0.9 479199 0s 007ms 042us - 0.95 505821 0s 047ms 982us - 0.990625 527453 0s 055ms 048us - 0.99902344 531922 0s 060ms 678us - -Queueing and connection setup latency (532473 samples) - min: 0s 000ms 001us | mean: 0s 000ms 011us | max: 0s 023ms 204us | pstdev: 0s 000ms 126us - - Percentile Count Value - 0.5 267630 0s 000ms 003us - 0.75 399579 0s 000ms 008us - 0.8 426987 0s 000ms 009us - 0.9 479713 0s 000ms 009us - 0.95 505878 0s 000ms 010us - 0.990625 527482 0s 000ms 040us - 0.99902344 531955 0s 001ms 391us - -Request start to response end (532441 samples) - min: 0s 000ms 359us | mean: 0s 006ms 028us | max: 0s 090ms 263us | pstdev: 0s 011ms 870us - - Percentile Count Value - 0.5 266221 0s 002ms 697us - 0.75 399332 0s 004ms 227us - 0.8 425959 0s 004ms 757us - 0.9 479199 0s 007ms 042us - 0.95 505824 0s 047ms 982us - 0.990625 527450 0s 055ms 046us - 0.99902344 531922 0s 060ms 678us - -Response body size in bytes (532441 samples) - min: 10 | mean: 10 | max: 10 | pstdev: 0 - -Response header size in bytes (532441 samples) - min: 110 | mean: 110 | max: 110 | pstdev: 0 - -Blocking. Results are skewed when significant numbers are reported here. (183352 samples) - min: 0s 000ms 033us | mean: 0s 001ms 962us | max: 0s 084ms 799us | pstdev: 0s 006ms 606us - - Percentile Count Value - 0.5 91683 0s 000ms 726us - 0.75 137514 0s 001ms 420us - 0.8 146685 0s 001ms 667us - 0.9 165017 0s 002ms 581us - 0.95 174186 0s 003ms 882us - 0.990625 181634 0s 047ms 661us - 0.99902344 183173 0s 053ms 168us - -Initiation to completion (532804 samples) - min: 0s 000ms 001us | mean: 0s 006ms 088us | max: 0s 090ms 275us | pstdev: 0s 011ms 873us - - Percentile Count Value - 0.5 266426 0s 002ms 753us - 0.75 399604 0s 004ms 306us - 0.8 426251 0s 004ms 841us - 0.9 479526 0s 007ms 164us - 0.95 506172 0s 048ms 007us - 0.990625 527811 0s 055ms 113us - 0.99902344 532284 0s 060ms 801us - -Counter Value Per second -benchmark.http_2xx 532441 5915.99 -benchmark.pool_overflow 363 4.03 -cluster_manager.cluster_added 4 0.04 -default.total_match_count 4 0.04 -membership_change 4 0.04 -runtime.load_success 1 0.01 -runtime.override_dir_not_exists 1 0.01 -upstream_cx_http1_total 37 0.41 -upstream_cx_rx_bytes_total 83593237 928810.08 -upstream_cx_total 37 0.41 -upstream_cx_tx_bytes_total 22896339 254402.76 -upstream_rq_pending_overflow 363 4.03 -upstream_rq_pending_total 37 0.41 -upstream_rq_total 532473 5916.34 - -[09:53:50.184479][19][I] Wait for the connection pool drain timed out, proceeding to hard shutdown. -[09:53:55.185619][21][I] Wait for the connection pool drain timed out, proceeding to hard shutdown. -[09:54:00.186411][23][I] Wait for the connection pool drain timed out, proceeding to hard shutdown. -[09:54:00.187569][1][I] Done. - -``` - -
- -
-scale-up-httproutes-300 - -```plaintext -Warning: 30 09:57:17.615][1][warning][misc] [external/envoy/source/common/protobuf/message_validator_impl.cc:21] Deprecated field: type envoy.config.core.v3.HeaderValueOption Using deprecated option 'envoy.config.core.v3.HeaderValueOption.append' from file base.proto. This configuration will be removed from Envoy soon. Please see https://www.envoyproxy.io/docs/envoy/latest/version_history/version_history for details. If continued use of this field is absolutely necessary, see https://www.envoyproxy.io/docs/envoy/latest/configuration/operations/runtime#using-runtime-overrides-for-deprecated-features for how to apply a temporary and highly discouraged override. -[09:57:17.615763][1][I] Detected 4 (v)CPUs with affinity.. -[09:57:17.615775][1][I] Starting 4 threads / event loops. Time limit: 90 seconds. -[09:57:17.615778][1][I] Global targets: 400 connections and 40000 calls per second. -[09:57:17.615779][1][I] (Per-worker targets: 100 connections and 10000 calls per second) -[09:58:48.317519][18][I] Stopping after 90000 ms. Initiated: 141701 / Completed: 141691. (Completion rate was 1574.3439371558425 per second.) -[09:58:48.317519][19][I] Stopping after 90000 ms. Initiated: 142076 / Completed: 142066. (Completion rate was 1578.5110584940758 per second.) -[09:58:48.317557][21][I] Stopping after 90000 ms. Initiated: 169359 / Completed: 169346. (Completion rate was 1881.6218249909482 per second.) -[09:58:48.317726][23][I] Stopping after 90000 ms. Initiated: 73891 / Completed: 73887. (Completion rate was 820.9651798075076 per second.) -[09:58:53.963287][18][I] Wait for the connection pool drain timed out, proceeding to hard shutdown. -Nighthawk - A layer 7 protocol benchmarking tool. - -benchmark_http_client.latency_2xx (526627 samples) - min: 0s 000ms 368us | mean: 0s 006ms 116us | max: 0s 085ms 676us | pstdev: 0s 012ms 060us - - Percentile Count Value - 0.5 263331 0s 002ms 692us - 0.75 394974 0s 004ms 203us - 0.8 421305 0s 004ms 735us - 0.9 473966 0s 007ms 143us - 0.95 500299 0s 048ms 519us - 0.990625 521693 0s 055ms 746us - 0.99902344 526114 0s 062ms 912us - -Queueing and connection setup latency (526664 samples) - min: 0s 000ms 001us | mean: 0s 000ms 012us | max: 0s 019ms 310us | pstdev: 0s 000ms 136us - - Percentile Count Value - 0.5 263616 0s 000ms 003us - 0.75 395207 0s 000ms 008us - 0.8 421679 0s 000ms 009us - 0.9 474013 0s 000ms 009us - 0.95 500359 0s 000ms 010us - 0.990625 521727 0s 000ms 127us - 0.99902344 526150 0s 001ms 624us - -Request start to response end (526627 samples) - min: 0s 000ms 368us | mean: 0s 006ms 116us | max: 0s 085ms 676us | pstdev: 0s 012ms 060us - - Percentile Count Value - 0.5 263324 0s 002ms 692us - 0.75 394984 0s 004ms 203us - 0.8 421305 0s 004ms 734us - 0.9 473966 0s 007ms 142us - 0.95 500299 0s 048ms 519us - 0.990625 521693 0s 055ms 746us - 0.99902344 526113 0s 062ms 910us - -Response body size in bytes (526627 samples) - min: 10 | mean: 10 | max: 10 | pstdev: 0 - -Response header size in bytes (526627 samples) - min: 110 | mean: 110 | max: 110 | pstdev: 0 - -Blocking. Results are skewed when significant numbers are reported here. (178472 samples) - min: 0s 000ms 034us | mean: 0s 002ms 016us | max: 0s 085ms 700us | pstdev: 0s 006ms 753us - - Percentile Count Value - 0.5 89237 0s 000ms 732us - 0.75 133855 0s 001ms 469us - 0.8 142779 0s 001ms 727us - 0.9 160630 0s 002ms 687us - 0.95 169550 0s 004ms 042us - 0.990625 176799 0s 048ms 060us - 0.99902344 178298 0s 054ms 435us - -Initiation to completion (526990 samples) - min: 0s 000ms 004us | mean: 0s 006ms 174us | max: 0s 085ms 700us | pstdev: 0s 012ms 062us - - Percentile Count Value - 0.5 263496 0s 002ms 745us - 0.75 395254 0s 004ms 278us - 0.8 421594 0s 004ms 818us - 0.9 474295 0s 007ms 277us - 0.95 500644 0s 048ms 537us - 0.990625 522050 0s 055ms 818us - 0.99902344 526476 0s 062ms 992us - -Counter Value Per second -benchmark.http_2xx 526627 5851.41 -benchmark.pool_overflow 363 4.03 -cluster_manager.cluster_added 4 0.04 -default.total_match_count 4 0.04 -membership_change 4 0.04 -runtime.load_success 1 0.01 -runtime.override_dir_not_exists 1 0.01 -upstream_cx_http1_total 37 0.41 -upstream_cx_rx_bytes_total 82680439 918671.00 -upstream_cx_total 37 0.41 -upstream_cx_tx_bytes_total 22646552 251628.21 -upstream_rq_pending_overflow 363 4.03 -upstream_rq_pending_total 37 0.41 -upstream_rq_total 526664 5851.82 - -[09:58:58.964269][19][I] Wait for the connection pool drain timed out, proceeding to hard shutdown. -[09:59:03.965258][21][I] Wait for the connection pool drain timed out, proceeding to hard shutdown. -[09:59:08.966199][23][I] Wait for the connection pool drain timed out, proceeding to hard shutdown. -[09:59:08.967339][1][I] Done. - -``` - -
- -
-scale-up-httproutes-500 - -```plaintext -Warning: 30 10:05:50.181][1][warning][misc] [external/envoy/source/common/protobuf/message_validator_impl.cc:21] Deprecated field: type envoy.config.core.v3.HeaderValueOption Using deprecated option 'envoy.config.core.v3.HeaderValueOption.append' from file base.proto. This configuration will be removed from Envoy soon. Please see https://www.envoyproxy.io/docs/envoy/latest/version_history/version_history for details. If continued use of this field is absolutely necessary, see https://www.envoyproxy.io/docs/envoy/latest/configuration/operations/runtime#using-runtime-overrides-for-deprecated-features for how to apply a temporary and highly discouraged override. -[10:05:50.182352][1][I] Detected 4 (v)CPUs with affinity.. -[10:05:50.182365][1][I] Starting 4 threads / event loops. Time limit: 90 seconds. -[10:05:50.182367][1][I] Global targets: 400 connections and 40000 calls per second. -[10:05:50.182369][1][I] (Per-worker targets: 100 connections and 10000 calls per second) -[10:07:20.884276][21][I] Stopping after 90000 ms. Initiated: 93542 / Completed: 93539. (Completion rate was 1039.3210789690352 per second.) -[10:07:20.884582][19][I] Stopping after 90000 ms. Initiated: 180072 / Completed: 180060. (Completion rate was 2000.6570635127619 per second.) -[10:07:20.884601][18][I] Stopping after 90000 ms. Initiated: 101770 / Completed: 101764. (Completion rate was 1130.7051560639559 per second.) -[10:07:20.884602][23][I] Stopping after 90000 ms. Initiated: 140594 / Completed: 140587. (Completion rate was 1562.0708005282022 per second.) -[10:07:26.528168][18][I] Wait for the connection pool drain timed out, proceeding to hard shutdown. -Nighthawk - A layer 7 protocol benchmarking tool. - -benchmark_http_client.latency_2xx (515587 samples) - min: 0s 000ms 352us | mean: 0s 006ms 243us | max: 0s 103ms 198us | pstdev: 0s 012ms 343us - - Percentile Count Value - 0.5 257799 0s 002ms 698us - 0.75 386691 0s 004ms 155us - 0.8 412474 0s 004ms 697us - 0.9 464029 0s 007ms 193us - 0.95 489815 0s 048ms 922us - 0.990625 510755 0s 056ms 528us - 0.99902344 515084 0s 067ms 747us - -Queueing and connection setup latency (515615 samples) - min: 0s 000ms 001us | mean: 0s 000ms 012us | max: 0s 020ms 386us | pstdev: 0s 000ms 125us - - Percentile Count Value - 0.5 258414 0s 000ms 003us - 0.75 386829 0s 000ms 008us - 0.8 412620 0s 000ms 009us - 0.9 464074 0s 000ms 009us - 0.95 489886 0s 000ms 010us - 0.990625 510782 0s 000ms 123us - 0.99902344 515112 0s 001ms 602us - -Request start to response end (515587 samples) - min: 0s 000ms 352us | mean: 0s 006ms 243us | max: 0s 103ms 194us | pstdev: 0s 012ms 343us - - Percentile Count Value - 0.5 257802 0s 002ms 698us - 0.75 386698 0s 004ms 155us - 0.8 412470 0s 004ms 697us - 0.9 464031 0s 007ms 192us - 0.95 489808 0s 048ms 920us - 0.990625 510756 0s 056ms 528us - 0.99902344 515084 0s 067ms 747us - -Response body size in bytes (515587 samples) - min: 10 | mean: 10 | max: 10 | pstdev: 0 - -Response header size in bytes (515587 samples) - min: 110 | mean: 110 | max: 110 | pstdev: 0 - -Blocking. Results are skewed when significant numbers are reported here. (165055 samples) - min: 0s 000ms 036us | mean: 0s 002ms 180us | max: 0s 090ms 710us | pstdev: 0s 007ms 128us - - Percentile Count Value - 0.5 82528 0s 000ms 765us - 0.75 123792 0s 001ms 622us - 0.8 132045 0s 001ms 906us - 0.9 148550 0s 002ms 915us - 0.95 156804 0s 004ms 402us - 0.990625 163510 0s 048ms 775us - 0.99902344 164894 0s 058ms 019us - -Initiation to completion (515950 samples) - min: 0s 000ms 004us | mean: 0s 006ms 300us | max: 0s 103ms 215us | pstdev: 0s 012ms 342us - - Percentile Count Value - 0.5 257987 0s 002ms 750us - 0.75 386970 0s 004ms 235us - 0.8 412760 0s 004ms 784us - 0.9 464357 0s 007ms 323us - 0.95 490153 0s 048ms 945us - 0.990625 511115 0s 056ms 580us - 0.99902344 515447 0s 067ms 813us - -Counter Value Per second -benchmark.http_2xx 515587 5728.72 -benchmark.pool_overflow 363 4.03 -cluster_manager.cluster_added 4 0.04 -default.total_match_count 4 0.04 -membership_change 4 0.04 -runtime.load_success 1 0.01 -runtime.override_dir_not_exists 1 0.01 -upstream_cx_http1_total 37 0.41 -upstream_cx_rx_bytes_total 80947159 899409.36 -upstream_cx_total 37 0.41 -upstream_cx_tx_bytes_total 22171445 246348.42 -upstream_rq_pending_overflow 363 4.03 -upstream_rq_pending_total 37 0.41 -upstream_rq_total 515615 5729.03 - -[10:07:31.529027][19][I] Wait for the connection pool drain timed out, proceeding to hard shutdown. -[10:07:36.530025][21][I] Wait for the connection pool drain timed out, proceeding to hard shutdown. -[10:07:41.530832][23][I] Wait for the connection pool drain timed out, proceeding to hard shutdown. -[10:07:41.532165][1][I] Done. - -``` - -
- -
-scale-down-httproutes-300 - -```plaintext -Warning: 30 10:08:14.153][1][warning][misc] [external/envoy/source/common/protobuf/message_validator_impl.cc:21] Deprecated field: type envoy.config.core.v3.HeaderValueOption Using deprecated option 'envoy.config.core.v3.HeaderValueOption.append' from file base.proto. This configuration will be removed from Envoy soon. Please see https://www.envoyproxy.io/docs/envoy/latest/version_history/version_history for details. If continued use of this field is absolutely necessary, see https://www.envoyproxy.io/docs/envoy/latest/configuration/operations/runtime#using-runtime-overrides-for-deprecated-features for how to apply a temporary and highly discouraged override. -[10:08:14.154484][1][I] Detected 4 (v)CPUs with affinity.. -[10:08:14.154499][1][I] Starting 4 threads / event loops. Time limit: 90 seconds. -[10:08:14.154502][1][I] Global targets: 400 connections and 40000 calls per second. -[10:08:14.154504][1][I] (Per-worker targets: 100 connections and 10000 calls per second) -[10:09:44.857305][21][I] Stopping after 90000 ms. Initiated: 120108 / Completed: 120100. (Completion rate was 1334.4432137912584 per second.) -[10:09:44.857423][23][I] Stopping after 90000 ms. Initiated: 54818 / Completed: 54817. (Completion rate was 609.0765799271705 per second.) -[10:09:44.857655][18][I] Stopping after 90000 ms. Initiated: 191822 / Completed: 191822. (Completion rate was 2131.3440462977055 per second.) -[10:09:44.858450][19][I] Stopping after 90001 ms. Initiated: 150329 / Completed: 150327. (Completion rate was 1670.2767089192257 per second.) -[10:09:50.501933][19][I] Wait for the connection pool drain timed out, proceeding to hard shutdown. -Nighthawk - A layer 7 protocol benchmarking tool. - -benchmark_http_client.latency_2xx (516703 samples) - min: 0s 000ms 339us | mean: 0s 006ms 221us | max: 0s 096ms 722us | pstdev: 0s 011ms 937us - - Percentile Count Value - 0.5 258358 0s 002ms 750us - 0.75 387543 0s 004ms 382us - 0.8 413372 0s 004ms 969us - 0.9 465035 0s 007ms 728us - 0.95 490869 0s 047ms 640us - 0.990625 511859 0s 055ms 703us - 0.99902344 516199 0s 065ms 038us - -Queueing and connection setup latency (516714 samples) - min: 0s 000ms 001us | mean: 0s 000ms 012us | max: 0s 019ms 922us | pstdev: 0s 000ms 135us - - Percentile Count Value - 0.5 259348 0s 000ms 003us - 0.75 387676 0s 000ms 008us - 0.8 413511 0s 000ms 009us - 0.9 465475 0s 000ms 009us - 0.95 490880 0s 000ms 010us - 0.990625 511870 0s 000ms 042us - 0.99902344 516210 0s 001ms 596us - -Request start to response end (516703 samples) - min: 0s 000ms 339us | mean: 0s 006ms 221us | max: 0s 096ms 722us | pstdev: 0s 011ms 937us - - Percentile Count Value - 0.5 258360 0s 002ms 750us - 0.75 387541 0s 004ms 382us - 0.8 413365 0s 004ms 968us - 0.9 465034 0s 007ms 726us - 0.95 490870 0s 047ms 640us - 0.990625 511859 0s 055ms 703us - 0.99902344 516199 0s 065ms 038us - -Response body size in bytes (516703 samples) - min: 10 | mean: 10 | max: 10 | pstdev: 0 - -Response header size in bytes (516703 samples) - min: 110 | mean: 110 | max: 110 | pstdev: 0 - -Blocking. Results are skewed when significant numbers are reported here. (170680 samples) - min: 0s 000ms 034us | mean: 0s 002ms 108us | max: 0s 090ms 091us | pstdev: 0s 006ms 769us - - Percentile Count Value - 0.5 85340 0s 000ms 756us - 0.75 128013 0s 001ms 581us - 0.8 136546 0s 001ms 864us - 0.9 153612 0s 002ms 914us - 0.95 162148 0s 004ms 492us - 0.990625 169080 0s 047ms 810us - 0.99902344 170514 0s 055ms 848us - -Initiation to completion (517066 samples) - min: 0s 000ms 003us | mean: 0s 006ms 281us | max: 0s 096ms 731us | pstdev: 0s 011ms 939us - - Percentile Count Value - 0.5 258539 0s 002ms 805us - 0.75 387802 0s 004ms 465us - 0.8 413664 0s 005ms 058us - 0.9 465360 0s 007ms 867us - 0.95 491214 0s 047ms 654us - 0.990625 512220 0s 055ms 760us - 0.99902344 516562 0s 065ms 097us - -Counter Value Per second -benchmark.http_2xx 516703 5741.11 -benchmark.pool_overflow 363 4.03 -cluster_manager.cluster_added 4 0.04 -default.total_match_count 4 0.04 -membership_change 4 0.04 -runtime.load_success 1 0.01 -runtime.override_dir_not_exists 1 0.01 -upstream_cx_http1_total 37 0.41 -upstream_cx_rx_bytes_total 81122371 901354.66 -upstream_cx_total 37 0.41 -upstream_cx_tx_bytes_total 22218702 246873.09 -upstream_rq_pending_overflow 363 4.03 -upstream_rq_pending_total 37 0.41 -upstream_rq_total 516714 5741.23 - -[10:09:55.503051][21][I] Wait for the connection pool drain timed out, proceeding to hard shutdown. -[10:10:00.503918][23][I] Wait for the connection pool drain timed out, proceeding to hard shutdown. -[10:10:00.505088][1][I] Done. - -``` - -
- -
-scale-down-httproutes-100 - -```plaintext -Warning: 30 10:10:18.056][1][warning][misc] [external/envoy/source/common/protobuf/message_validator_impl.cc:21] Deprecated field: type envoy.config.core.v3.HeaderValueOption Using deprecated option 'envoy.config.core.v3.HeaderValueOption.append' from file base.proto. This configuration will be removed from Envoy soon. Please see https://www.envoyproxy.io/docs/envoy/latest/version_history/version_history for details. If continued use of this field is absolutely necessary, see https://www.envoyproxy.io/docs/envoy/latest/configuration/operations/runtime#using-runtime-overrides-for-deprecated-features for how to apply a temporary and highly discouraged override. -[10:10:18.057130][1][I] Detected 4 (v)CPUs with affinity.. -[10:10:18.057143][1][I] Starting 4 threads / event loops. Time limit: 90 seconds. -[10:10:18.057145][1][I] Global targets: 400 connections and 40000 calls per second. -[10:10:18.057147][1][I] (Per-worker targets: 100 connections and 10000 calls per second) -[10:11:48.759104][18][I] Stopping after 90000 ms. Initiated: 209011 / Completed: 208988. (Completion rate was 2322.087186024952 per second.) -[10:11:48.759212][21][I] Stopping after 90000 ms. Initiated: 49528 / Completed: 49526. (Completion rate was 550.2881062569155 per second.) -[10:11:48.759225][19][I] Stopping after 90000 ms. Initiated: 82374 / Completed: 82369. (Completion rate was 915.2096671136363 per second.) -[10:11:48.759235][22][I] Stopping after 90000 ms. Initiated: 83103 / Completed: 83098. (Completion rate was 923.3098082183817 per second.) -[10:11:54.582462][18][I] Wait for the connection pool drain timed out, proceeding to hard shutdown. -Nighthawk - A layer 7 protocol benchmarking tool. - -benchmark_http_client.latency_2xx (423617 samples) - min: 0s 000ms 341us | mean: 0s 007ms 208us | max: 0s 137ms 691us | pstdev: 0s 011ms 926us - - Percentile Count Value - 0.5 211815 0s 003ms 194us - 0.75 317722 0s 006ms 279us - 0.8 338894 0s 007ms 606us - 0.9 381256 0s 016ms 511us - 0.95 402437 0s 035ms 002us - 0.990625 419646 0s 062ms 793us - 0.99902344 423204 0s 082ms 636us - -Queueing and connection setup latency (423652 samples) - min: 0s 000ms 001us | mean: 0s 000ms 014us | max: 0s 035ms 192us | pstdev: 0s 000ms 210us - - Percentile Count Value - 0.5 212529 0s 000ms 003us - 0.75 317890 0s 000ms 008us - 0.8 339187 0s 000ms 009us - 0.9 381367 0s 000ms 009us - 0.95 402534 0s 000ms 011us - 0.990625 419681 0s 000ms 121us - 0.99902344 423239 0s 001ms 867us - -Request start to response end (423617 samples) - min: 0s 000ms 341us | mean: 0s 007ms 207us | max: 0s 137ms 691us | pstdev: 0s 011ms 926us - - Percentile Count Value - 0.5 211809 0s 003ms 194us - 0.75 317719 0s 006ms 279us - 0.8 338894 0s 007ms 606us - 0.9 381256 0s 016ms 510us - 0.95 402437 0s 035ms 002us - 0.990625 419646 0s 062ms 793us - 0.99902344 423204 0s 082ms 636us - -Response body size in bytes (423617 samples) - min: 10 | mean: 10 | max: 10 | pstdev: 0 - -Response header size in bytes (423617 samples) - min: 110 | mean: 110 | max: 110 | pstdev: 0 - -Blocking. Results are skewed when significant numbers are reported here. (142751 samples) - min: 0s 000ms 035us | mean: 0s 002ms 521us | max: 0s 105ms 373us | pstdev: 0s 006ms 732us - - Percentile Count Value - 0.5 71376 0s 000ms 888us - 0.75 107064 0s 001ms 861us - 0.8 114202 0s 002ms 248us - 0.9 128476 0s 004ms 006us - 0.95 135614 0s 008ms 008us - 0.990625 141413 0s 041ms 697us - 0.99902344 142612 0s 064ms 958us - -Initiation to completion (423981 samples) - min: 0s 000ms 005us | mean: 0s 007ms 283us | max: 0s 137ms 756us | pstdev: 0s 011ms 948us - - Percentile Count Value - 0.5 211995 0s 003ms 260us - 0.75 317989 0s 006ms 396us - 0.8 339189 0s 007ms 761us - 0.9 381583 0s 016ms 687us - 0.95 402787 0s 035ms 110us - 0.990625 420007 0s 062ms 932us - 0.99902344 423567 0s 082ms 649us - -Counter Value Per second -benchmark.http_2xx 423617 4706.85 -benchmark.pool_overflow 364 4.04 -cluster_manager.cluster_added 4 0.04 -default.total_match_count 4 0.04 -membership_change 4 0.04 -runtime.load_success 1 0.01 -runtime.override_dir_not_exists 1 0.01 -upstream_cx_http1_total 36 0.40 -upstream_cx_rx_bytes_total 66507869 738975.37 -upstream_cx_total 36 0.40 -upstream_cx_tx_bytes_total 18217036 202411.25 -upstream_rq_pending_overflow 364 4.04 -upstream_rq_pending_total 36 0.40 -upstream_rq_total 423652 4707.24 - -[10:11:59.584852][19][I] Wait for the connection pool drain timed out, proceeding to hard shutdown. -[10:12:04.585643][21][I] Wait for the connection pool drain timed out, proceeding to hard shutdown. -[10:12:09.586270][22][I] Wait for the connection pool drain timed out, proceeding to hard shutdown. -[10:12:09.587436][1][I] Done. - -``` - -
- -
-scale-down-httproutes-50 - -```plaintext -Warning: 29 03:03:53.663][1][warning][misc] [external/envoy/source/common/protobuf/message_validator_impl.cc:21] Deprecated field: type envoy.config.core.v3.HeaderValueOption Using deprecated option 'envoy.config.core.v3.HeaderValueOption.append' from file base.proto. This configuration will be removed from Envoy soon. Please see https://www.envoyproxy.io/docs/envoy/latest/version_history/version_history for details. If continued use of this field is absolutely necessary, see https://www.envoyproxy.io/docs/envoy/latest/configuration/operations/runtime#using-runtime-overrides-for-deprecated-features for how to apply a temporary and highly discouraged override. -[03:03:53.664682][1][I] Detected 4 (v)CPUs with affinity.. -[03:03:53.664695][1][I] Starting 4 threads / event loops. Time limit: 30 seconds. -[03:03:53.664698][1][I] Global targets: 400 connections and 40000 calls per second. -[03:03:53.664700][1][I] (Per-worker targets: 100 connections and 10000 calls per second) -[03:04:24.367434][23][I] Stopping after 30000 ms. Initiated: 46577 / Completed: 46567. (Completion rate was 1552.2191046582072 per second.) -[03:04:24.367829][18][I] Stopping after 30000 ms. Initiated: 42300 / Completed: 42290. (Completion rate was 1409.6315198541051 per second.) -[03:04:24.368549][21][I] Stopping after 30001 ms. Initiated: 39600 / Completed: 39591. (Completion rate was 1319.637581142412 per second.) -[03:04:24.367485][19][I] Stopping after 29994 ms. Initiated: 14551 / Completed: 14549. (Completion rate was 485.05192262626696 per second.) -[03:04:30.226109][18][I] Wait for the connection pool drain timed out, proceeding to hard shutdown. -Nighthawk - A layer 7 protocol benchmarking tool. - -benchmark_http_client.latency_2xx (142634 samples) - min: 0s 000ms 350us | mean: 0s 007ms 306us | max: 0s 108ms 011us | pstdev: 0s 010ms 777us - - Percentile Count Value - 0.5 71317 0s 003ms 525us - 0.75 106976 0s 006ms 682us - 0.8 114108 0s 008ms 188us - 0.9 128372 0s 019ms 436us - 0.95 135503 0s 031ms 845us - 0.990625 141297 0s 055ms 373us - 0.99902344 142495 0s 080ms 084us - -Queueing and connection setup latency (142665 samples) - min: 0s 000ms 001us | mean: 0s 000ms 016us | max: 0s 030ms 452us | pstdev: 0s 000ms 301us - - Percentile Count Value - 0.5 71653 0s 000ms 003us - 0.75 107071 0s 000ms 008us - 0.8 114147 0s 000ms 009us - 0.9 128437 0s 000ms 010us - 0.95 135540 0s 000ms 011us - 0.990625 141328 0s 000ms 076us - 0.99902344 142526 0s 002ms 365us - -Request start to response end (142634 samples) - min: 0s 000ms 350us | mean: 0s 007ms 305us | max: 0s 108ms 011us | pstdev: 0s 010ms 777us - - Percentile Count Value - 0.5 71318 0s 003ms 525us - 0.75 106977 0s 006ms 681us - 0.8 114108 0s 008ms 187us - 0.9 128371 0s 019ms 434us - 0.95 135503 0s 031ms 842us - 0.990625 141297 0s 055ms 373us - 0.99902344 142495 0s 080ms 084us - -Response body size in bytes (142634 samples) - min: 10 | mean: 10 | max: 10 | pstdev: 0 - -Response header size in bytes (142634 samples) - min: 110 | mean: 110 | max: 110 | pstdev: 0 - -Blocking. Results are skewed when significant numbers are reported here. (42514 samples) - min: 0s 000ms 036us | mean: 0s 002ms 819us | max: 0s 097ms 796us | pstdev: 0s 006ms 582us - - Percentile Count Value - 0.5 21257 0s 000ms 979us - 0.75 31886 0s 002ms 264us - 0.8 34012 0s 002ms 802us - 0.9 38263 0s 005ms 232us - 0.95 40389 0s 011ms 347us - 0.990625 42116 0s 036ms 845us - 0.99902344 42473 0s 061ms 714us - -Initiation to completion (142997 samples) - min: 0s 000ms 003us | mean: 0s 007ms 382us | max: 0s 108ms 048us | pstdev: 0s 010ms 803us - - Percentile Count Value - 0.5 71499 0s 003ms 591us - 0.75 107249 0s 006ms 808us - 0.8 114399 0s 008ms 328us - 0.9 128698 0s 019ms 615us - 0.95 135848 0s 031ms 933us - 0.990625 141657 0s 055ms 416us - 0.99902344 142858 0s 080ms 097us - -Counter Value Per second -benchmark.http_2xx 142634 4754.58 -benchmark.pool_overflow 363 12.10 -cluster_manager.cluster_added 4 0.13 -default.total_match_count 4 0.13 -membership_change 4 0.13 -runtime.load_success 1 0.03 -runtime.override_dir_not_exists 1 0.03 -upstream_cx_http1_total 37 1.23 -upstream_cx_rx_bytes_total 22393538 746468.86 -upstream_cx_total 37 1.23 -upstream_cx_tx_bytes_total 6134595 204491.32 -upstream_rq_pending_overflow 363 12.10 -upstream_rq_pending_total 37 1.23 -upstream_rq_total 142665 4755.61 - -[03:04:35.227368][19][I] Wait for the connection pool drain timed out, proceeding to hard shutdown. -[03:04:40.228272][21][I] Wait for the connection pool drain timed out, proceeding to hard shutdown. -[03:04:45.229345][23][I] Wait for the connection pool drain timed out, proceeding to hard shutdown. -[03:04:45.231026][1][I] Done. - -``` - -
- -
-scale-down-httproutes-10 - -```plaintext -Warning: 30 10:15:03.038][1][warning][misc] [external/envoy/source/common/protobuf/message_validator_impl.cc:21] Deprecated field: type envoy.config.core.v3.HeaderValueOption Using deprecated option 'envoy.config.core.v3.HeaderValueOption.append' from file base.proto. This configuration will be removed from Envoy soon. Please see https://www.envoyproxy.io/docs/envoy/latest/version_history/version_history for details. If continued use of this field is absolutely necessary, see https://www.envoyproxy.io/docs/envoy/latest/configuration/operations/runtime#using-runtime-overrides-for-deprecated-features for how to apply a temporary and highly discouraged override. -[10:15:03.039299][1][I] Detected 4 (v)CPUs with affinity.. -[10:15:03.039310][1][I] Starting 4 threads / event loops. Time limit: 90 seconds. -[10:15:03.039313][1][I] Global targets: 400 connections and 40000 calls per second. -[10:15:03.039314][1][I] (Per-worker targets: 100 connections and 10000 calls per second) -[10:16:33.741189][23][I] Stopping after 90000 ms. Initiated: 258274 / Completed: 258258. (Completion rate was 2869.5316753816987 per second.) -[10:16:33.741450][18][I] Stopping after 90000 ms. Initiated: 35338 / Completed: 35338. (Completion rate was 392.64273862987994 per second.) -[10:16:33.741587][21][I] Stopping after 90000 ms. Initiated: 196085 / Completed: 196074. (Completion rate was 2178.588477687607 per second.) -[10:16:33.742737][19][I] Stopping after 90001 ms. Initiated: 35313 / Completed: 35313. (Completion rate was 392.3594516123065 per second.) -[10:16:39.388073][21][I] Wait for the connection pool drain timed out, proceeding to hard shutdown. -Nighthawk - A layer 7 protocol benchmarking tool. - -benchmark_http_client.latency_2xx (524619 samples) - min: 0s 000ms 363us | mean: 0s 005ms 917us | max: 0s 093ms 347us | pstdev: 0s 011ms 507us - - Percentile Count Value - 0.5 262326 0s 002ms 779us - 0.75 393472 0s 004ms 063us - 0.8 419697 0s 004ms 496us - 0.9 472160 0s 006ms 384us - 0.95 498391 0s 046ms 964us - 0.990625 519704 0s 054ms 102us - 0.99902344 524107 0s 058ms 939us - -Queueing and connection setup latency (524646 samples) - min: 0s 000ms 001us | mean: 0s 000ms 010us | max: 0s 019ms 046us | pstdev: 0s 000ms 096us - - Percentile Count Value - 0.5 262650 0s 000ms 003us - 0.75 393534 0s 000ms 008us - 0.8 420137 0s 000ms 009us - 0.9 472311 0s 000ms 009us - 0.95 498511 0s 000ms 011us - 0.990625 519728 0s 000ms 076us - 0.99902344 524134 0s 001ms 276us - -Request start to response end (524619 samples) - min: 0s 000ms 363us | mean: 0s 005ms 917us | max: 0s 093ms 347us | pstdev: 0s 011ms 507us - - Percentile Count Value - 0.5 262310 0s 002ms 778us - 0.75 393465 0s 004ms 062us - 0.8 419696 0s 004ms 496us - 0.9 472160 0s 006ms 384us - 0.95 498389 0s 046ms 962us - 0.990625 519707 0s 054ms 102us - 0.99902344 524107 0s 058ms 939us - -Response body size in bytes (524619 samples) - min: 10 | mean: 10 | max: 10 | pstdev: 0 - -Response header size in bytes (524619 samples) - min: 110 | mean: 110 | max: 110 | pstdev: 0 - -Blocking. Results are skewed when significant numbers are reported here. (158918 samples) - min: 0s 000ms 034us | mean: 0s 002ms 264us | max: 0s 069ms 718us | pstdev: 0s 007ms 119us - - Percentile Count Value - 0.5 79459 0s 000ms 812us - 0.75 119189 0s 001ms 826us - 0.8 127135 0s 002ms 141us - 0.9 143027 0s 003ms 136us - 0.95 150973 0s 004ms 425us - 0.990625 157429 0s 048ms 680us - 0.99902344 158763 0s 055ms 619us - -Initiation to completion (524983 samples) - min: 0s 000ms 005us | mean: 0s 005ms 969us | max: 0s 093ms 364us | pstdev: 0s 011ms 505us - - Percentile Count Value - 0.5 262493 0s 002ms 832us - 0.75 393739 0s 004ms 131us - 0.8 419987 0s 004ms 571us - 0.9 472485 0s 006ms 477us - 0.95 498740 0s 046ms 983us - 0.990625 520063 0s 054ms 147us - 0.99902344 524471 0s 059ms 058us - -Counter Value Per second -benchmark.http_2xx 524619 5829.06 -benchmark.pool_overflow 364 4.04 -cluster_manager.cluster_added 4 0.04 -default.total_match_count 4 0.04 -membership_change 4 0.04 -runtime.load_success 1 0.01 -runtime.override_dir_not_exists 1 0.01 -upstream_cx_http1_total 36 0.40 -upstream_cx_rx_bytes_total 82365183 915162.15 -upstream_cx_total 36 0.40 -upstream_cx_tx_bytes_total 22559778 250662.41 -upstream_rq_pending_overflow 364 4.04 -upstream_rq_pending_total 36 0.40 -upstream_rq_total 524646 5829.36 - -[10:16:44.389294][23][I] Wait for the connection pool drain timed out, proceeding to hard shutdown. -[10:16:44.391101][1][I] Done. - -``` - -
- -### Metrics - -|Benchmark Name |Envoy Gateway Memory (MiB)|Envoy Gateway Total CPU (Seconds)|Envoy Proxy Memory: 4lvn5[1] (MiB)| -|- |- |- |- | -|scale-up-httproutes-10 |78 |0.39 |7 | -|scale-up-httproutes-50 |87 |1.87 |11 | -|scale-up-httproutes-100 |167 |8.9 |20 | -|scale-up-httproutes-300 |673 |167.86 |81 | -|scale-up-httproutes-500 |1358 |10.98 |190 | -|scale-down-httproutes-300|643 |5.59 |84 | -|scale-down-httproutes-100|455 |112.24 |55 | -|scale-down-httproutes-50 |147 |150.17 |20 | -|scale-down-httproutes-10 |120 |151.69 |12 | -1. envoy-gateway-system/envoy-benchmark-test-benchmark-0520098c-dbf5d95fb-4lvn5 diff --git a/test/benchmark/benchmark_test.go b/test/benchmark/benchmark_test.go index 7edbd215cec..5ef46c08f56 100644 --- a/test/benchmark/benchmark_test.go +++ b/test/benchmark/benchmark_test.go @@ -12,23 +12,13 @@ import ( "flag" "testing" - "github.com/stretchr/testify/require" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/config" - "github.com/envoyproxy/gateway/test/benchmark/suite" "github.com/envoyproxy/gateway/test/benchmark/tests" + kubetest "github.com/envoyproxy/gateway/test/utils/kubernetes" ) func TestBenchmark(t *testing.T) { - cfg, err := config.GetConfig() - require.NoError(t, err) - - cli, err := client.New(cfg, client.Options{}) - require.NoError(t, err) - - // Install all the scheme for kubernetes client. - suite.CheckInstallScheme(t, cli) + cli, _ := kubetest.NewClient(t) // Parse benchmark options. flag.Parse() @@ -45,7 +35,7 @@ func TestBenchmark(t *testing.T) { "config/gateway.yaml", "config/httproute.yaml", "config/nighthawk-client.yaml", - *suite.ReportSavePath, + *suite.ReportSaveDir, ) if err != nil { t.Fatalf("Failed to create BenchmarkTestSuite: %v", err) diff --git a/test/benchmark/config/nighthawk-client.yaml b/test/benchmark/config/nighthawk-client.yaml index 90375d8c05c..a9621d3fb72 100644 --- a/test/benchmark/config/nighthawk-client.yaml +++ b/test/benchmark/config/nighthawk-client.yaml @@ -4,8 +4,6 @@ kind: Job metadata: name: "{NIGHTHAWK_CLIENT_NAME}" namespace: benchmark-test - labels: - benchmark-test/client: "true" spec: template: spec: diff --git a/test/benchmark/suite/flags.go b/test/benchmark/suite/flags.go index a07d2d4b010..f8ec9ca168b 100644 --- a/test/benchmark/suite/flags.go +++ b/test/benchmark/suite/flags.go @@ -11,9 +11,9 @@ package suite import "flag" var ( - RPS = flag.String("rps", "1000", "The target requests-per-second rate.") - Connections = flag.String("connections", "10", "The maximum allowed number of concurrent connections per event loop. HTTP/1 only.") - Duration = flag.String("duration", "60", "The number of seconds that the test should run.") - Concurrency = flag.String("concurrency", "auto", "The number of concurrent event loops that should be used.") - ReportSavePath = flag.String("report-save-path", "", "The path where to save the benchmark test report.") + RPS = flag.String("rps", "1000", "The target requests-per-second rate.") + Connections = flag.String("connections", "10", "The maximum allowed number of concurrent connections per event loop. HTTP/1 only.") + Duration = flag.String("duration", "60", "The number of seconds that the test should run.") + Concurrency = flag.String("concurrency", "auto", "The number of concurrent event loops that should be used.") + ReportSaveDir = flag.String("report-save-dir", "benchmark_report", "The dir where to save the benchmark test report.") ) diff --git a/test/benchmark/suite/test.go b/test/benchmark/suite/options.go similarity index 79% rename from test/benchmark/suite/test.go rename to test/benchmark/suite/options.go index 17138dfad25..13a8a82a3c2 100644 --- a/test/benchmark/suite/test.go +++ b/test/benchmark/suite/options.go @@ -8,14 +8,6 @@ package suite -import "testing" - -type BenchmarkTest struct { - ShortName string - Description string - Test func(*testing.T, *BenchmarkTestSuite) []*BenchmarkReport -} - // BenchmarkOptions for nighthawk-client. type BenchmarkOptions struct { RPS string diff --git a/test/benchmark/suite/render.go b/test/benchmark/suite/render.go index 44485b88bfe..dfe3f130ca4 100644 --- a/test/benchmark/suite/render.go +++ b/test/benchmark/suite/render.go @@ -12,95 +12,70 @@ import ( "bytes" "fmt" "io" - "math" "os" "strconv" "strings" "text/tabwriter" - - prom "github.com/prometheus/client_model/go" - "github.com/prometheus/common/expfmt" - "k8s.io/apimachinery/pkg/util/sets" ) const ( omitEmptyValue = "-" benchmarkEnvPrefix = "BENCHMARK_" - // Supported metric type. - metricTypeGauge = "gauge" - metricTypeCounter = "counter" - - // Supported metric unit. - metricUnitMiB = "MiB" - metricUnitSeconds = "Seconds" - metricUnitMilliCPU = "m" + querySum = "Sum" + queryAvg = "Avg" + queryMin = "Min" + queryMax = "Max" ) -type ReportTableHeader struct { - Name string - Metric *MetricEntry - - // Underlying name of one envoy-proxy, used by data-plane metrics. - ProxyName string +type tableHeader struct { + name string + unit string + promQL string // only valid for metrics table + queryType string } -type MetricEntry struct { - Name string - Type string - FromControlPlane bool - DisplayUnit string - ConvertUnit func(float64) float64 +var metricsTableHeader = []tableHeader{ + { + name: "Test Name", + }, + { + name: "Envoy Gateway Memory", + unit: "MiB", + promQL: `process_resident_memory_bytes{namespace="envoy-gateway-system", control_plane="envoy-gateway"}/1024/1024`, + queryType: querySum, + }, + { + name: "Envoy Gateway CPU", + unit: "s", + promQL: `process_cpu_seconds_total{namespace="envoy-gateway-system", control_plane="envoy-gateway"}`, + queryType: querySum, + }, + { + name: "Envoy Proxy Memory (Avg)", + unit: "MiB", + promQL: `container_memory_working_set_bytes{namespace="envoy-gateway-system",container="envoy"}/1024/1024`, + queryType: queryAvg, + }, + { + name: "Envoy Proxy CPU (Avg)", + unit: "s", + promQL: `container_cpu_usage_seconds_total{namespace="envoy-gateway-system",container="envoy"}`, + queryType: queryAvg, + }, } // RenderReport renders a report out of given list of benchmark report in Markdown format. -func RenderReport(writer io.Writer, name, description string, reports []*BenchmarkReport, titleLevel int) error { - headerSettings := []ReportTableHeader{ - { - Name: "Benchmark Name", - }, - { - Name: "Envoy Gateway Memory", - Metric: &MetricEntry{ - Name: "process_resident_memory_bytes", - Type: metricTypeGauge, - DisplayUnit: metricUnitMiB, - FromControlPlane: true, - ConvertUnit: byteToMiB, - }, - }, - { - Name: "Envoy Gateway Total CPU", - Metric: &MetricEntry{ - Name: "process_cpu_seconds_total", - Type: metricTypeCounter, - DisplayUnit: metricUnitSeconds, - FromControlPlane: true, - }, - }, - { - Name: "Envoy Proxy Memory", - Metric: &MetricEntry{ - Name: "envoy_server_memory_allocated", - Type: metricTypeGauge, - DisplayUnit: metricUnitMiB, - FromControlPlane: false, - ConvertUnit: byteToMiB, - }, - }, - } +func RenderReport(writer io.Writer, name, description string, titleLevel int, reports []*BenchmarkReport) error { + writeSection(writer, "Test: "+name, titleLevel, description) - writeSection(writer, name, titleLevel, description) - - writeSection(writer, "Results", titleLevel+1, "Click to see the full results.") - renderResultsTable(writer, reports) - - writeSection(writer, "Metrics", titleLevel+1, "") - err := renderMetricsTable(writer, headerSettings, reports) - if err != nil { + writeSection(writer, "Results", titleLevel+1, "Expand to see the full results.") + if err := renderResultsTable(writer, reports); err != nil { return err } + writeSection(writer, "Metrics", titleLevel+1, "") + renderMetricsTable(writer, reports) return nil } @@ -110,41 +85,19 @@ func newMarkdownStyleTableWriter(writer io.Writer) *tabwriter.Writer { } func renderEnvSettingsTable(writer io.Writer) { - _, _ = fmt.Fprintln(writer, "Benchmark test settings:") - table := newMarkdownStyleTableWriter(writer) - headers := []ReportTableHeader{ - { - Name: "RPS", - }, - { - Name: "Connections", - }, - { - Name: "Duration", - Metric: &MetricEntry{ - DisplayUnit: metricUnitSeconds, - }, - }, - { - Name: "CPU Limits", - Metric: &MetricEntry{ - DisplayUnit: metricUnitMilliCPU, - }, - }, - { - Name: "Memory Limits", - Metric: &MetricEntry{ - DisplayUnit: metricUnitMiB, - }, - }, + headers := []tableHeader{ + {name: "RPS"}, + {name: "Connections"}, + {name: "Duration", unit: "s"}, + {name: "CPU Limits", unit: "m"}, + {name: "Memory Limits", unit: "MiB"}, } + writeTableHeader(table, headers) - renderMetricsTableHeader(table, headers) - - writeTableRow(table, headers, func(_ int, h ReportTableHeader) string { - env := strings.ReplaceAll(strings.ToUpper(h.Name), " ", "_") + writeTableRow(table, headers, func(_ int, h tableHeader) string { + env := strings.ReplaceAll(strings.ToUpper(h.name), " ", "_") if v, ok := os.LookupEnv(benchmarkEnvPrefix + env); ok { return v } @@ -154,70 +107,35 @@ func renderEnvSettingsTable(writer io.Writer) { _ = table.Flush() } -func renderResultsTable(writer io.Writer, reports []*BenchmarkReport) { - // TODO: better processing these benchmark results. +func renderResultsTable(writer io.Writer, reports []*BenchmarkReport) error { for _, report := range reports { - writeCollapsibleSection(writer, report.Name, report.RawResult) - } -} - -func renderMetricsTable(writer io.Writer, headerSettings []ReportTableHeader, reports []*BenchmarkReport) error { - table := newMarkdownStyleTableWriter(writer) - - // Preprocess the table header for metrics table. - var headers []ReportTableHeader - // 1. Collect all the possible proxy names. - proxyNames := sets.NewString() - for _, report := range reports { - for name := range report.RawDPMetrics { - proxyNames.Insert(name) - } - } - // 2. Generate header names for data-plane proxies. - for _, hs := range headerSettings { - if hs.Metric != nil && !hs.Metric.FromControlPlane { - for i, proxyName := range proxyNames.List() { - names := strings.Split(proxyName, "-") - headers = append(headers, ReportTableHeader{ - Name: fmt.Sprintf("%s: %s[%d]", hs.Name, names[len(names)-1], i+1), - Metric: hs.Metric, - ProxyName: proxyName, - }) + tmpResults := bytes.Split(report.Result, []byte("\n")) + outResults := make([][]byte, 0, len(tmpResults)) + for _, r := range tmpResults { + if !bytes.HasPrefix(r, []byte("[")) && !bytes.HasPrefix(r, []byte("Nighthawk")) { + outResults = append(outResults, r) } - } else { - // For control-plane metrics or plain header. - headers = append(headers, hs) } + + writeCollapsibleSection(writer, report.Name, bytes.Join(outResults, []byte("\n"))) } - renderMetricsTableHeader(table, headers) + return nil +} - for _, report := range reports { - mfCP, err := parseMetrics(report.RawCPMetrics) - if err != nil { - return err - } +func renderMetricsTable(writer io.Writer, reports []*BenchmarkReport) { + table := newMarkdownStyleTableWriter(writer) - mfDPs := make(map[string]map[string]*prom.MetricFamily, len(report.RawDPMetrics)) - for dpName, dpMetrics := range report.RawDPMetrics { - mfDP, err := parseMetrics(dpMetrics) - if err != nil { - return err - } - mfDPs[dpName] = mfDP - } + writeTableHeader(table, metricsTableHeader) - writeTableRow(table, headers, func(_ int, h ReportTableHeader) string { - if h.Metric == nil { + for _, report := range reports { + writeTableRow(table, metricsTableHeader, func(_ int, h tableHeader) string { + if len(h.promQL) == 0 { return report.Name } - if h.Metric.FromControlPlane { - return processMetricValue(mfCP, h) - } else { - if mfDP, ok := mfDPs[h.ProxyName]; ok { - return processMetricValue(mfDP, h) - } + if v, ok := report.Metrics[h.name]; ok { + return strconv.FormatFloat(v, 'f', -1, 64) } return omitEmptyValue @@ -225,28 +143,6 @@ func renderMetricsTable(writer io.Writer, headerSettings []ReportTableHeader, re } _ = table.Flush() - - // Generate footnotes for envoy-proxy headers. - for i, proxyName := range proxyNames.List() { - _, _ = fmt.Fprintln(writer, fmt.Sprintf("%d.", i+1), proxyName) - } - - return nil -} - -func renderMetricsTableHeader(table *tabwriter.Writer, headers []ReportTableHeader) { - writeTableRow(table, headers, func(_ int, h ReportTableHeader) string { - if h.Metric != nil && len(h.Metric.DisplayUnit) > 0 { - return fmt.Sprintf("%s (%s)", h.Name, h.Metric.DisplayUnit) - } - return h.Name - }) - - writeTableDelimiter(table, len(headers)) -} - -func byteToMiB(x float64) float64 { - return math.Round(x / (1024 * 1024)) } // writeSection writes one section in Markdown style, content is optional. @@ -271,11 +167,21 @@ func writeCollapsibleSection(writer io.Writer, title string, content []byte) { `, title, summary) } -// writeTableRow writes one row in Markdown table style. -func writeTableRow[T any](table *tabwriter.Writer, values []T, get func(int, T) string) { +func writeTableHeader(table *tabwriter.Writer, headers []tableHeader) { + writeTableRow(table, headers, func(_ int, h tableHeader) string { + if len(h.unit) > 0 { + return fmt.Sprintf("%s (%s)", h.name, h.unit) + } + return h.name + }) + writeTableDelimiter(table, len(headers)) +} + +// writeTableRow writes one row in Markdown table style according to headers. +func writeTableRow(table *tabwriter.Writer, headers []tableHeader, on func(int, tableHeader) string) { row := "|" - for i, v := range values { - row += get(i, v) + "\t" + for i, v := range headers { + row += on(i, v) + "\t" } _, _ = fmt.Fprintln(table, row) @@ -290,42 +196,3 @@ func writeTableDelimiter(table *tabwriter.Writer, n int) { _, _ = fmt.Fprintln(table, sep) } - -// parseMetrics parses input metrics that in Prometheus format. -func parseMetrics(metrics []byte) (map[string]*prom.MetricFamily, error) { - var ( - reader = bytes.NewReader(metrics) - parser expfmt.TextParser - ) - - mf, err := parser.TextToMetricFamilies(reader) - if err != nil { - return nil, err - } - - return mf, nil -} - -// processMetricValue process one metric value according to the given header and metric families. -func processMetricValue(metricFamilies map[string]*prom.MetricFamily, header ReportTableHeader) string { - if mf, ok := metricFamilies[header.Metric.Name]; ok { - var value float64 - - switch header.Metric.Type { - case metricTypeGauge: - value = *mf.Metric[0].Gauge.Value - case metricTypeCounter: - value = *mf.Metric[0].Counter.Value - default: - return omitEmptyValue - } - - if header.Metric.ConvertUnit != nil { - value = header.Metric.ConvertUnit(value) - } - - return strconv.FormatFloat(value, 'f', -1, 64) - } - - return omitEmptyValue -} diff --git a/test/benchmark/suite/report.go b/test/benchmark/suite/report.go index c37539f6231..d0176331ee4 100644 --- a/test/benchmark/suite/report.go +++ b/test/benchmark/suite/report.go @@ -13,73 +13,47 @@ import ( "context" "fmt" "io" - "net/http" - "testing" + "strconv" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - "github.com/envoyproxy/gateway/internal/cmd/options" kube "github.com/envoyproxy/gateway/internal/kubernetes" -) - -const ( - localMetricsPort = 0 - controlPlaneMetricsPort = 19001 + prom "github.com/envoyproxy/gateway/test/utils/prometheus" ) type BenchmarkReport struct { - Name string - RawResult []byte - RawCPMetrics []byte - RawDPMetrics map[string][]byte + Name string + Result []byte + Metrics map[string]float64 // metricTableHeaderName:metricValue kubeClient kube.CLIClient + promClient *prom.Client } -func NewBenchmarkReport(name string) (*BenchmarkReport, error) { - kubeClient, err := kube.NewCLIClient(options.DefaultConfigFlags.ToRawKubeConfigLoader()) - if err != nil { - return nil, err - } - +func NewBenchmarkReport(name string, kubeClient kube.CLIClient, promClient *prom.Client) *BenchmarkReport { return &BenchmarkReport{ - Name: name, - RawDPMetrics: make(map[string][]byte), - kubeClient: kubeClient, - }, nil -} - -// Print prints the raw report of one benchmark test. -func (r *BenchmarkReport) Print(t *testing.T, name string) { - t.Logf("The raw report of benchmark test: %s", name) - - t.Logf("=== Benchmark Result: \n\n %s \n\n", r.RawResult) - t.Logf("=== Control-Plane Metrics: \n\n %s \n\n", r.RawCPMetrics) - - for dpName, dpMetrics := range r.RawDPMetrics { - t.Logf("=== Data-Plane Metrics for %s: \n\n %s \n\n", dpName, dpMetrics) + Name: name, + Metrics: make(map[string]float64), + kubeClient: kubeClient, + promClient: promClient, } } -func (r *BenchmarkReport) Collect(t *testing.T, ctx context.Context, job *types.NamespacedName) error { - if err := r.GetBenchmarkResult(t, ctx, job); err != nil { - return err - } - - if err := r.GetControlPlaneMetrics(t, ctx); err != nil { +func (r *BenchmarkReport) Collect(ctx context.Context, job *types.NamespacedName) error { + if err := r.GetMetrics(ctx); err != nil { return err } - if err := r.GetDataPlaneMetrics(t, ctx); err != nil { + if err := r.GetResult(ctx, job); err != nil { return err } return nil } -func (r *BenchmarkReport) GetBenchmarkResult(t *testing.T, ctx context.Context, job *types.NamespacedName) error { +func (r *BenchmarkReport) GetResult(ctx context.Context, job *types.NamespacedName) error { pods, err := r.kubeClient.Kube().CoreV1().Pods(job.Namespace).List(ctx, metav1.ListOptions{LabelSelector: "job-name=" + job.Name}) if err != nil { return err @@ -89,73 +63,47 @@ func (r *BenchmarkReport) GetBenchmarkResult(t *testing.T, ctx context.Context, return fmt.Errorf("failed to get any pods for job %s", job.String()) } - if len(pods.Items) > 1 { - t.Logf("Got %d pod(s) associated job %s, should be 1 pod, could be pod err and job backoff then restart, please check your pod(s) status", - len(pods.Items), job.Name) - } - - pod := &pods.Items[0] - logs, err := r.getLogsFromPod( - ctx, &types.NamespacedName{Name: pod.Name, Namespace: pod.Namespace}, - ) - if err != nil { - return err - } - - r.RawResult = logs - - return nil -} - -func (r *BenchmarkReport) GetControlPlaneMetrics(t *testing.T, ctx context.Context) error { - egPods, err := r.kubeClient.Kube().CoreV1().Pods("envoy-gateway-system"). - List(ctx, metav1.ListOptions{LabelSelector: "control-plane=envoy-gateway"}) - if err != nil { - return err - } - - if len(egPods.Items) < 1 { - return fmt.Errorf("failed to get any pods for envoy-gateway") - } - - if len(egPods.Items) > 1 { - t.Logf("Got %d pod(s), using the first one as default envoy-gateway pod", len(egPods.Items)) + // Find the pod that complete successfully. + var pod corev1.Pod + for _, p := range pods.Items { + if p.Status.Phase == corev1.PodSucceeded { + pod = p + break + } } - egPod := &egPods.Items[0] - metrics, err := r.getMetricsFromPortForwarder( - t, &types.NamespacedName{Name: egPod.Name, Namespace: egPod.Namespace}, "/metrics", - ) + logs, err := r.getLogsFromPod(ctx, &types.NamespacedName{Name: pod.Name, Namespace: pod.Namespace}) if err != nil { return err } - r.RawCPMetrics = metrics + r.Result = logs return nil } -func (r *BenchmarkReport) GetDataPlaneMetrics(t *testing.T, ctx context.Context) error { - epPods, err := r.kubeClient.Kube().CoreV1().Pods("envoy-gateway-system"). - List(ctx, metav1.ListOptions{LabelSelector: "gateway.envoyproxy.io/owning-gateway-namespace=benchmark-test,gateway.envoyproxy.io/owning-gateway-name=benchmark"}) - if err != nil { - return err - } - - if len(epPods.Items) < 1 { - return fmt.Errorf("failed to get any pods for envoy-proxies") - } - - t.Logf("Got %d pod(s) from data-plane", len(epPods.Items)) +func (r *BenchmarkReport) GetMetrics(ctx context.Context) error { + for _, h := range metricsTableHeader { + if len(h.promQL) == 0 { + continue + } - for _, epPod := range epPods.Items { - podNN := &types.NamespacedName{Name: epPod.Name, Namespace: epPod.Namespace} - metrics, err := r.getMetricsFromPortForwarder(t, podNN, "/stats/prometheus") - if err != nil { - return err + var ( + v float64 + err error + ) + switch h.queryType { + case querySum: + v, err = r.promClient.QuerySum(ctx, h.promQL) + case queryAvg: + v, err = r.promClient.QueryAvg(ctx, h.promQL) + default: + return fmt.Errorf("unsupported query type: %s", h.queryType) } - r.RawDPMetrics[podNN.String()] = metrics + if err == nil { + r.Metrics[h.name], _ = strconv.ParseFloat(fmt.Sprintf("%.2f", v), 64) + } } return nil @@ -181,44 +129,3 @@ func (r *BenchmarkReport) getLogsFromPod(ctx context.Context, pod *types.Namespa return buf.Bytes(), nil } - -// getMetricsFromPortForwarder retrieves metrics from pod by request url, like `/metrics`. -func (r *BenchmarkReport) getMetricsFromPortForwarder(t *testing.T, pod *types.NamespacedName, url string) ([]byte, error) { - fw, err := kube.NewLocalPortForwarder(r.kubeClient, *pod, localMetricsPort, controlPlaneMetricsPort) - if err != nil { - return nil, fmt.Errorf("failed to build port forwarder for pod %s: %w", pod.String(), err) - } - - if err = fw.Start(); err != nil { - fw.Stop() - - return nil, fmt.Errorf("failed to start port forwarder for pod %s: %w", pod.String(), err) - } - requestURL := fmt.Sprintf("http://%s%s", fw.Address(), url) - - var out []byte - // Retrieving metrics from Pod. - go func(requestURL string) { - defer fw.Stop() - - //nolint: gosec - resp, err := http.Get(requestURL) - if err != nil { - t.Errorf("failed to request %s: %v", requestURL, err) - return - } - defer resp.Body.Close() - - metrics, err := io.ReadAll(resp.Body) - if err != nil { - t.Errorf("failed to read metrics: %v", err) - return - } - - out = metrics - }(requestURL) - - fw.WaitForStop() - - return out, nil -} diff --git a/test/benchmark/suite/suite.go b/test/benchmark/suite/suite.go index 376766ae1b1..035ac68d5a8 100644 --- a/test/benchmark/suite/suite.go +++ b/test/benchmark/suite/suite.go @@ -13,6 +13,7 @@ import ( "context" "fmt" "os" + "path" "strconv" "testing" "time" @@ -25,6 +26,10 @@ import ( gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" "sigs.k8s.io/gateway-api/conformance/utils/config" "sigs.k8s.io/yaml" + + opt "github.com/envoyproxy/gateway/internal/cmd/options" + kube "github.com/envoyproxy/gateway/internal/kubernetes" + prom "github.com/envoyproxy/gateway/test/utils/prometheus" ) const ( @@ -33,24 +38,34 @@ const ( DefaultControllerName = "gateway.envoyproxy.io/gatewayclass-controller" ) +type BenchmarkTest struct { + ShortName string + Description string + Test func(*testing.T, *BenchmarkTestSuite) []*BenchmarkReport +} + type BenchmarkTestSuite struct { Client client.Client TimeoutConfig config.TimeoutConfig ControllerName string Options BenchmarkOptions - ReportSavePath string + ReportSaveDir string // Resources template for supported benchmark targets. GatewayTemplate *gwapiv1.Gateway HTTPRouteTemplate *gwapiv1.HTTPRoute BenchmarkClientJob *batchv1.Job - // Indicates which resources are scaled. - scaledLabel map[string]string + // Labels + scaledLabels map[string]string // indicate which resources are scaled + + // Clients that for internal usage. + kubeClient kube.CLIClient // required for getting logs from pod + promClient *prom.Client } func NewBenchmarkTestSuite(client client.Client, options BenchmarkOptions, - gatewayManifest, httpRouteManifest, benchmarkClientManifest, reportPath string, + gatewayManifest, httpRouteManifest, benchmarkClientManifest, reportDir string, ) (*BenchmarkTestSuite, error) { var ( gateway = new(gwapiv1.Gateway) @@ -87,23 +102,48 @@ func NewBenchmarkTestSuite(client client.Client, options BenchmarkOptions, config.SetupTimeoutConfig(&timeoutConfig) timeoutConfig.RouteMustHaveParents = 180 * time.Second + // Ensure the report directory exist. + if len(reportDir) > 0 { + if _, err = os.Stat(reportDir); err != nil { + if os.IsNotExist(err) { + if err = os.MkdirAll(reportDir, os.ModePerm); err != nil { + return nil, err + } + } else { + return nil, err + } + } + } + // Prepare static options for benchmark client. staticArgs := prepareBenchmarkClientStaticArgs(options) container := &benchmarkClient.Spec.Template.Spec.Containers[0] container.Args = append(container.Args, staticArgs...) + // Initial various client. + kubeClient, err := kube.NewCLIClient(opt.DefaultConfigFlags.ToRawKubeConfigLoader()) + if err != nil { + return nil, err + } + promClient, err := prom.NewClient(client, types.NamespacedName{Name: "prometheus", Namespace: "monitoring"}) + if err != nil { + return nil, err + } + return &BenchmarkTestSuite{ Client: client, Options: options, TimeoutConfig: timeoutConfig, ControllerName: DefaultControllerName, - ReportSavePath: reportPath, + ReportSaveDir: reportDir, GatewayTemplate: gateway, HTTPRouteTemplate: httproute, BenchmarkClientJob: benchmarkClient, - scaledLabel: map[string]string{ + scaledLabels: map[string]string{ BenchmarkTestScaledKey: "true", }, + kubeClient: kubeClient, + promClient: promClient, }, nil } @@ -113,7 +153,7 @@ func (b *BenchmarkTestSuite) Run(t *testing.T, tests []BenchmarkTest) { buf := make([]byte, 0) writer := bytes.NewBuffer(buf) - writeSection(writer, "Benchmark Report", 1, "") + writeSection(writer, "Benchmark Report", 1, "Benchmark test settings:") renderEnvSettingsTable(writer) for _, test := range tests { @@ -127,16 +167,17 @@ func (b *BenchmarkTestSuite) Run(t *testing.T, tests []BenchmarkTest) { // Generate a human-readable benchmark report for each test. t.Logf("Got %d reports for test: %s", len(reports), test.ShortName) - if err := RenderReport(writer, "Test: "+test.ShortName, test.Description, reports, 2); err != nil { + if err := RenderReport(writer, "Test: "+test.ShortName, test.Description, 2, reports); err != nil { t.Errorf("Error generating report for %s: %v", test.ShortName, err) } } - if len(b.ReportSavePath) > 0 { - if err := os.WriteFile(b.ReportSavePath, writer.Bytes(), 0o600); err != nil { - t.Errorf("Error writing report to path '%s': %v", b.ReportSavePath, err) + if len(b.ReportSaveDir) > 0 { + reportPath := path.Join(b.ReportSaveDir, "benchmark_report.md") + if err := os.WriteFile(reportPath, writer.Bytes(), 0o600); err != nil { + t.Errorf("Error writing report to path '%s': %v", reportPath, err) } else { - t.Logf("Writing report to path '%s' successfully", b.ReportSavePath) + t.Logf("Writing report to path '%s' successfully", reportPath) } } else { t.Logf("%s", writer.Bytes()) @@ -162,7 +203,7 @@ func (b *BenchmarkTestSuite) Benchmark(t *testing.T, ctx context.Context, name, } // Wait from benchmark test job to complete. - if err = wait.PollUntilContextTimeout(ctx, 10*time.Second, time.Duration(duration*10)*time.Second, true, func(ctx context.Context) (bool, error) { + if err = wait.PollUntilContextTimeout(ctx, 6*time.Second, time.Duration(duration*10)*time.Second, true, func(ctx context.Context) (bool, error) { job := new(batchv1.Job) if err = b.Client.Get(ctx, *jobNN, job); err != nil { return false, err @@ -191,24 +232,21 @@ func (b *BenchmarkTestSuite) Benchmark(t *testing.T, ctx context.Context, name, t.Logf("Running benchmark test: %s successfully", name) - report, err := NewBenchmarkReport(name) - if err != nil { - return nil, err - } - + report := NewBenchmarkReport(name, b.kubeClient, b.promClient) // Get all the reports from this benchmark test run. - if err = report.Collect(t, ctx, jobNN); err != nil { + if err = report.Collect(ctx, jobNN); err != nil { return nil, err } - report.Print(t, name) - return report, nil } func (b *BenchmarkTestSuite) createBenchmarkClientJob(ctx context.Context, name, gatewayHostPort string, requestHeaders ...string) (*types.NamespacedName, error) { job := b.BenchmarkClientJob.DeepCopy() job.SetName(name) + job.SetLabels(map[string]string{ + BenchmarkTestClientKey: "true", + }) runtimeArgs := prepareBenchmarkClientRuntimeArgs(gatewayHostPort, requestHeaders...) container := &job.Spec.Template.Spec.Containers[0] @@ -261,7 +299,7 @@ func (b *BenchmarkTestSuite) ScaleUpHTTPRoutes(ctx context.Context, scaleRange [ routeName := fmt.Sprintf(routeNameFormat, i) newRoute := b.HTTPRouteTemplate.DeepCopy() newRoute.SetName(routeName) - newRoute.SetLabels(b.scaledLabel) + newRoute.SetLabels(b.scaledLabels) newRoute.Spec.ParentRefs[0].Name = gwapiv1.ObjectName(refGateway) if err := b.CreateResource(ctx, newRoute); err != nil { @@ -297,7 +335,7 @@ func (b *BenchmarkTestSuite) ScaleDownHTTPRoutes(ctx context.Context, scaleRange routeName := fmt.Sprintf(routeNameFormat, i) oldRoute := b.HTTPRouteTemplate.DeepCopy() oldRoute.SetName(routeName) - oldRoute.SetLabels(b.scaledLabel) + oldRoute.SetLabels(b.scaledLabels) oldRoute.Spec.ParentRefs[0].Name = gwapiv1.ObjectName(refGateway) if err := b.DeleteResource(ctx, oldRoute); err != nil { diff --git a/test/cel-validation/backendtrafficpolicy_test.go b/test/cel-validation/backendtrafficpolicy_test.go index e8418790d31..903d3fa19f1 100644 --- a/test/cel-validation/backendtrafficpolicy_test.go +++ b/test/cel-validation/backendtrafficpolicy_test.go @@ -1134,6 +1134,25 @@ func TestBackendTrafficPolicyTarget(t *testing.T) { " Invalid value: \"object\": either targetRef or targetRefs must be used", }, }, + { + desc: "target selectors without targetRefs or targetRef", + mutate: func(sp *egv1a1.BackendTrafficPolicy) { + sp.Spec = egv1a1.BackendTrafficPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetSelectors: []egv1a1.TargetSelector{ + { + Group: ptr.To(gwapiv1a2.Group("gateway.networking.k8s.io")), + Kind: "HTTPRoute", + MatchLabels: map[string]string{ + "eg/namespace": "reference-apps", + }, + }, + }, + }, + } + }, + wantErrors: []string{}, + }, } for _, tc := range cases { diff --git a/test/cel-validation/clienttrafficpolicy_test.go b/test/cel-validation/clienttrafficpolicy_test.go index e28ad6c83bf..4d98efd0aa2 100644 --- a/test/cel-validation/clienttrafficpolicy_test.go +++ b/test/cel-validation/clienttrafficpolicy_test.go @@ -445,6 +445,25 @@ func TestClientTrafficPolicyTarget(t *testing.T) { }, wantErrors: []string{}, }, + { + desc: "target selectors without targetRefs or targetRef", + mutate: func(sp *egv1a1.ClientTrafficPolicy) { + sp.Spec = egv1a1.ClientTrafficPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetSelectors: []egv1a1.TargetSelector{ + { + Group: ptr.To(gwapiv1a2.Group("gateway.networking.k8s.io")), + Kind: "HTTPRoute", + MatchLabels: map[string]string{ + "eg/namespace": "reference-apps", + }, + }, + }, + }, + } + }, + wantErrors: []string{}, + }, } for _, tc := range cases { diff --git a/test/cel-validation/envoyextensionpolicy_test.go b/test/cel-validation/envoyextensionpolicy_test.go index c16d7169dbc..b199ed49d72 100644 --- a/test/cel-validation/envoyextensionpolicy_test.go +++ b/test/cel-validation/envoyextensionpolicy_test.go @@ -407,6 +407,25 @@ func TestEnvoyExtensionPolicyTarget(t *testing.T) { "spec.extProc[0].processingMode.request.body: Unsupported value: \"not-a-body-mode\": supported values: \"Streamed\", \"Buffered\", \"BufferedPartial\"", }, }, + { + desc: "target selectors without targetRefs or targetRef", + mutate: func(sp *egv1a1.EnvoyExtensionPolicy) { + sp.Spec = egv1a1.EnvoyExtensionPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetSelectors: []egv1a1.TargetSelector{ + { + Group: ptr.To(gwapiv1a2.Group("gateway.networking.k8s.io")), + Kind: "HTTPRoute", + MatchLabels: map[string]string{ + "eg/namespace": "reference-apps", + }, + }, + }, + }, + } + }, + wantErrors: []string{}, + }, } for _, tc := range cases { diff --git a/test/cel-validation/securitypolicy_test.go b/test/cel-validation/securitypolicy_test.go index 4c24c8ed712..24f229dd6f1 100644 --- a/test/cel-validation/securitypolicy_test.go +++ b/test/cel-validation/securitypolicy_test.go @@ -899,6 +899,25 @@ func TestSecurityPolicyTarget(t *testing.T) { }, wantErrors: []string{}, }, + { + desc: "target selectors without targetRefs or targetRef", + mutate: func(sp *egv1a1.SecurityPolicy) { + sp.Spec = egv1a1.SecurityPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetSelectors: []egv1a1.TargetSelector{ + { + Group: ptr.To(gwapiv1a2.Group("gateway.networking.k8s.io")), + Kind: "HTTPRoute", + MatchLabels: map[string]string{ + "eg/namespace": "reference-apps", + }, + }, + }, + }, + } + }, + wantErrors: []string{}, + }, } for _, tc := range cases { diff --git a/test/e2e/base/manifests.yaml b/test/e2e/base/manifests.yaml index 076240d60ef..3bba21844a0 100644 --- a/test/e2e/base/manifests.yaml +++ b/test/e2e/base/manifests.yaml @@ -6,7 +6,6 @@ # namespace): # - same-namespace (only supports route in same ns) # - all-namespaces (supports routes in all ns) -# - backend-namespaces (supports routes in ns with backend label) apiVersion: v1 kind: Namespace metadata: @@ -43,60 +42,6 @@ spec: allowedRoutes: namespaces: from: All - infrastructure: - parametersRef: - group: gateway.envoyproxy.io - kind: EnvoyProxy - name: zipkin-tracing - namespace: envoy-gateway-system ---- -apiVersion: gateway.networking.k8s.io/v1 -kind: Gateway -metadata: - name: backend-namespaces - namespace: gateway-conformance-infra -spec: - gatewayClassName: "{GATEWAY_CLASS_NAME}" - listeners: - - name: https - port: 443 - protocol: HTTPS - tls: - certificateRefs: - - group: "" - kind: Secret - name: backend-tls-certificate - mode: Terminate - - name: http - port: 80 - protocol: HTTP - allowedRoutes: - namespaces: - from: Selector - selector: - matchLabels: - gateway-conformance: backend ---- -apiVersion: gateway.networking.k8s.io/v1beta1 -kind: Gateway -metadata: - name: my-tcp-gateway - namespace: gateway-conformance-infra -spec: - gatewayClassName: "{GATEWAY_CLASS_NAME}" - listeners: - - name: foo - protocol: TCP - port: 8080 - allowedRoutes: - kinds: - - kind: TCPRoute - - name: bar - protocol: TCP - port: 8090 - allowedRoutes: - kinds: - - kind: TCPRoute --- apiVersion: v1 kind: Service @@ -472,92 +417,6 @@ spec: --- apiVersion: v1 kind: Namespace -metadata: - name: gateway-conformance-udp - labels: - gateway-conformance: udp ---- -apiVersion: v1 -kind: Service -metadata: - name: coredns - namespace: gateway-conformance-udp - labels: - app: udp -spec: - ports: - - name: udp-dns - port: 53 - protocol: UDP - targetPort: 53 - selector: - app: udp ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: coredns - namespace: gateway-conformance-udp - labels: - app: udp -spec: - selector: - matchLabels: - app: udp - template: - metadata: - labels: - app: udp - spec: - containers: - - args: - - -conf - - /root/Corefile - image: coredns/coredns - name: coredns - volumeMounts: - - mountPath: /root - name: conf - volumes: - - configMap: - defaultMode: 420 - name: coredns - name: conf ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: coredns - namespace: gateway-conformance-udp -data: - Corefile: | - .:53 { - forward . 8.8.8.8 9.9.9.9 - log - errors - } - - foo.bar.com:53 { - whoami - } ---- -apiVersion: gateway.networking.k8s.io/v1beta1 -kind: Gateway -metadata: - name: udp-gateway - namespace: gateway-conformance-udp -spec: - gatewayClassName: "{GATEWAY_CLASS_NAME}" - listeners: - - name: coredns - protocol: UDP - port: 5300 - allowedRoutes: - kinds: - - kind: UDPRoute ---- -apiVersion: v1 -kind: Namespace metadata: name: gateway-preserve-case-backend --- @@ -664,26 +523,6 @@ spec: targetPort: 8000 --- apiVersion: v1 -kind: Namespace -metadata: - name: gateway-upgrade-infra ---- -apiVersion: gateway.networking.k8s.io/v1 -kind: Gateway -metadata: - name: ha-gateway - namespace: gateway-upgrade-infra -spec: - gatewayClassName: upgrade - listeners: - - allowedRoutes: - namespaces: - from: Same - name: http1 - port: 80 - protocol: HTTP ---- -apiVersion: v1 data: tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURPVENDQWlHZ0F3SUJBZ0lVUWNxbnZtQXlkRUtuOEdqWTdjZzVDb3A2QWp3d0RRWUpLb1pJaHZjTkFRRUwKQlFBd1JURUxNQWtHQTFVRUJoTUNRVlV4RXpBUkJnTlZCQWdNQ2xOdmJXVXRVM1JoZEdVeElUQWZCZ05WQkFvTQpHRWx1ZEdWeWJtVjBJRmRwWkdkcGRITWdVSFI1SUV4MFpEQWVGdzB5TkRBMU1USXhOakF3TlROYUZ3MHlOVEExCk1USXhOakF3TlROYU1FVXhDekFKQmdOVkJBWVRBa0ZWTVJNd0VRWURWUVFJREFwVGIyMWxMVk4wWVhSbE1TRXcKSHdZRFZRUUtEQmhKYm5SbGNtNWxkQ0JYYVdSbmFYUnpJRkIwZVNCTWRHUXdnZ0VpTUEwR0NTcUdTSWIzRFFFQgpBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRQ2kzUis1WGx3SnlYSTNidTRVQ3E0NXgwSkdWQVBTVXRFTFlLUkxpOEo2CnlxOStySE1hVUtubDhsdldLaHlCNDk4WkJBdVVGS0RpcGhkS1A2eU0rRGl1azVIa2UrK0NmeGxkUDFiSGZiNlkKSGFWczh2cFMyUThneUF6NEZqc3NnNThMV1NKWTdEeEhSOWJibUVWelhSUjNWOEtDeDVaYVlkZ3RxU0NZTGJMTwozaGtGRGQramZxSzM3RHdiT253d21OQ2R0QmpRSTF1TmF2dm1QZzB0c3pwd29TQUtPRitPR0pHcTZHcDdNY0NtClFHZ3dYNkV0YzMwd3hJQTd6c3RnTWwzT293a3p4NHNMcFdJamdCSDVlVk9oYnB6NXROLzB2VFZ3Z3hlbTlOVisKQURjSTFBcnY5M1ZsaFB6VEFmZUNDUlljeFFiNlp4dnBuMWlRbVIrZkVpT0JBZ01CQUFHaklUQWZNQjBHQTFVZApEZ1FXQkJTMGRnRHNtQ3AyU0pZVzNPa3pkNDZtbFNndHZ6QU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFab0NCCnE0M2taV1RZT21QR3JYMU5RMllIVTQ2Y0pzRGxsN2JFL0ZIRUo1eEJEcWRGaUdhWkZBcGRkK3Mra2tkUUw5NUUKcU1SVk9nYS83TUFIL042dlRmb2tXcnVKUUFqaStpLzhGSllWb1VZTWMyeUxqYXp3ZS9ZMHlzTDRWRTNGUlZybApmVHRCTC9nVkhjNk9ZOFBpVFh4eitqdy9FN2kxQkRxZkdSK29sYmt4ZkVmWnhHN0tEZUVtQnVva0dxbDlYQXhSCjMzbnhSbFZuODdxSnJrdUlzdWl2ZzczaVVNMVpGUE1CRVp0OEJjU05MaWhxZEx0b29FVy9mcGZ1am9oaC9yTjUKOFA1ajJpWm9KOGpBS0t4YW5SaWhXTklSNzJtYnJ1R2hYOFRIQkxzczFvZlpLdHBXMzlUOTBTM2hnWkFwSmNZYQp2aGVwSnRtbm9jcHNnYUJiL0E9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRQ2kzUis1WGx3SnlYSTMKYnU0VUNxNDV4MEpHVkFQU1V0RUxZS1JMaThKNnlxOStySE1hVUtubDhsdldLaHlCNDk4WkJBdVVGS0RpcGhkSwpQNnlNK0RpdWs1SGtlKytDZnhsZFAxYkhmYjZZSGFWczh2cFMyUThneUF6NEZqc3NnNThMV1NKWTdEeEhSOWJiCm1FVnpYUlIzVjhLQ3g1WmFZZGd0cVNDWUxiTE8zaGtGRGQramZxSzM3RHdiT253d21OQ2R0QmpRSTF1TmF2dm0KUGcwdHN6cHdvU0FLT0YrT0dKR3E2R3A3TWNDbVFHZ3dYNkV0YzMwd3hJQTd6c3RnTWwzT293a3p4NHNMcFdJagpnQkg1ZVZPaGJwejV0Ti8wdlRWd2d4ZW05TlYrQURjSTFBcnY5M1ZsaFB6VEFmZUNDUlljeFFiNlp4dnBuMWlRCm1SK2ZFaU9CQWdNQkFBRUNnZ0VBQkZlOUxUbXNMb3VBWGZTWmdRZStnT0pZbU1pTDZpcG91aWI0Wlk1dFUvM3kKYVZoRXlOVHhuVlRadkoyT2lWc0JzWVNLWUo5TzBaRmFSOUhJSnBHR1BTYzRvOGNYWGpkb2RqUmFrbjdtbDQ2NwprT3JQR0lsQ3ZFb0NVTVdTdkExNXpzMGdmNTZUdmthMjFxL2VHdmtPZ3c3SVBEbVJSNEZpT05nNGt3ODlwRU1FClhNMVROK1NZSGR1eU50RG1wSy90bjFFcUxtOUJUVy9XSk8rMHhLaG1EVStDZnN6Y2hmcS82QitQSVNteEhXUTUKV2JVL3BBNVlvRlM3TWZYbjhMcXVuSG55RGcvNERaQ2NXQkpZZTdZNWtqczVVd2c2MnJBanZxTUZPaWk2ZEgvWgpSQWFWbzlUeEEwZVB4TkZIY2w5M2xuQzRvSGpFV0pvajIzOUdTb3pMWVFLQmdRRGJaNDhNOUpNdkJ2Q2JvSWlXCi9jc3U2ZFRNNmlRTzd3dVJEekhETlpGN0pxWmxOVUhkMU1hZXVYOGhYaE1YeWZpN2FFcGhKMGFZOXNwZ29SdWYKamIyeWVhb3JBbnlOL1VIZ2NjdG5ESWdheEJ0K1JVREZUeE1uTnhrRUVnTnF5WDJCZllOdlNyMmxOVm9PbGhUWQo4VzV6UGJyekNDbUgzSXd6bkhmcW54QVZJUUtCZ1FDK0IwOEVpRERZd1ZKRU9uNyt3eDZyZWlQQklmanBFNUNICjM4ekpYVVBUaWRLZWpORnU4aUlGZmkrdzEvY0VBTTJXcE1xVEtnM2RCSTlzUGZnWXZFSUlvMW5adUFNVnhaY04Kb0k2QXdUckdWMldHSjdQNlNNbjhyWTJhUGRQcDl1ZXE2MFEwZ2p2NVQ1eUliekdPaElmeUpxSXJHYlFvbGdkagp3cXg4K2ZQaVlRS0JnRWJSdklqd0FQb3pBVU1hcER3b200Yi9EeU05aUhvUml1Zzl3VkJEWUR3aUU1K2pleWxCClh3TW8yUEpLVFZ0bVpCVUo2c2hGUnpKa3BwcGVKbTV2OEFWRjVEbVJ5ZWFERXRxQm9LZ1lrVzRpVXNXRlVRemYKSTAyTEtWWDVBb1ZibUZsTnpEa0dKUVRJbmRNTGVwczBBdlRMdmlab1FnK0tqdTZ4Mkxzd3NKNUJBb0dCQUxFcApDUzcxZFd5dkZ1NUxCdGltdWpJdDVhV0o4WkFDVUcyTVpWU1o0Y0VXcmNocENsdi8yMTM1bmFhbVFVRjNLalEyCm9ER0JOSG1JWmRvSkVBS25pSHliSmdwSGRvRFd2SlBVeXVZWXY1M29IdHRxcW0wOWJTcG45eXNFVjB1NWg1UWUKVUhFUHRiQWgyNUtLNzgycG0wQlRhajc2Y0s2aDZIUEdLNTg4UEhZaEFvR0FILzJMS044WnJ3c1R6Nmx6T2c3ZApzdUFuaDVFTUp0TEhTSDJHeFJ5aFcrYVFHdGNxdDZYK1dkNDZnd1BMQjRjd2QzL01nQkFvcFhGazhYV3pTVUlhCnI5SG9SQzZJT2tzQ0lOallCd2h2TjArcm5oN3JzTm5XZVd5Z2tWQ2tERDN5NlNTa2RTZjliOUZzWUJtOHY0VkcKYzFqdmVjWVF6S243QzFRU2FtUnAzRUk9Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K @@ -694,71 +533,6 @@ metadata: type: kubernetes.io/tls --- apiVersion: v1 -kind: Service -metadata: - name: infra-backend - namespace: gateway-upgrade-infra -spec: - selector: - app: infra-backend - ports: - - protocol: TCP - port: 8080 - targetPort: 3000 ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: infra-backend - namespace: gateway-upgrade-infra - labels: - app: infra-backend -spec: - replicas: 2 - selector: - matchLabels: - app: infra-backend - template: - metadata: - labels: - app: infra-backend - spec: - containers: - - name: infra-backend - # From https://github.com/kubernetes-sigs/gateway-api/blob/main/conformance/echo-basic/echo-basic.go - image: gcr.io/k8s-staging-gateway-api/echo-basic:v20231214-v1.0.0-140-gf544a46e - env: - - name: POD_NAME - valueFrom: - fieldRef: - fieldPath: metadata.name - - name: NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - - name: SERVICE_NAME - value: infra-backend - resources: - requests: - cpu: 10m - ---- -apiVersion: gateway.networking.k8s.io/v1beta1 -kind: Gateway -metadata: - name: connection-limit-gateway - namespace: gateway-conformance-infra -spec: - gatewayClassName: "{GATEWAY_CLASS_NAME}" - listeners: - - name: http - port: 80 - protocol: HTTP - allowedRoutes: - namespaces: - from: Same ---- -apiVersion: v1 data: ca.crt: | -----BEGIN CERTIFICATE----- @@ -869,222 +643,9 @@ metadata: name: backend-tls-certificate namespace: gateway-conformance-infra type: kubernetes.io/tls ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: envoy-als - namespace: monitoring -data: - go.mod: | - module envoy-als - go 1.22 - require ( - github.com/envoyproxy/go-control-plane v0.12.0 - github.com/prometheus/client_golang v1.19.1 - google.golang.org/grpc v1.64.0 - ) - - require ( - github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50 // indirect - github.com/envoyproxy/protoc-gen-validate v1.0.4 // indirect - github.com/golang/protobuf v1.5.4 // indirect - github.com/prometheus/client_model v0.5.0 // indirect - github.com/prometheus/common v0.48.0 // indirect - github.com/prometheus/procfs v0.12.0 // indirect - golang.org/x/net v0.22.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect - google.golang.org/protobuf v1.33.0 // indirect - ) - go.sum: | - github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= - github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= - github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= - github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= - github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50 h1:DBmgJDC9dTfkVyGgipamEh2BpGYxScCH1TOF1LL1cXc= - github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50/go.mod h1:5e1+Vvlzido69INQaVO6d87Qn543Xr6nooe9Kz7oBFM= - github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= - github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= - github.com/envoyproxy/go-control-plane v0.12.0 h1:4X+VP1GHd1Mhj6IB5mMeGbLCleqxjletLK6K0rbxyZI= - github.com/envoyproxy/go-control-plane v0.12.0/go.mod h1:ZBTaoJ23lqITozF0M6G4/IragXCQKCnYbmlmtHvwRG0= - github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A= - github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew= - github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= - github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= - github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= - github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= - github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= - github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= - github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= - github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= - github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= - github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= - github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= - github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= - golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= - golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= - golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= - golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= - golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= - golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= - google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc= - google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= - google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= - google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= - google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= - google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= - main.go: | - package main - - import ( - "log" - "net" - "net/http" - - alsv2 "github.com/envoyproxy/go-control-plane/envoy/service/accesslog/v2" - alsv3 "github.com/envoyproxy/go-control-plane/envoy/service/accesslog/v3" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promhttp" - - "google.golang.org/grpc" - ) - - var ( - LogCount = prometheus.NewCounterVec(prometheus.CounterOpts{ - Name: "log_count", - Help: "The total number of logs received.", - }, []string{"api_version"}) - ) - - func init() { - // Register the summary and the histogram with Prometheus's default registry. - prometheus.MustRegister(LogCount) - } - - type ALSServer struct { - } - - func (a *ALSServer) StreamAccessLogs(logStream alsv2.AccessLogService_StreamAccessLogsServer) error { - log.Println("Streaming als v2 logs") - for { - data, err := logStream.Recv() - if err != nil { - return err - } - - httpLogs := data.GetHttpLogs() - if httpLogs != nil { - LogCount.WithLabelValues("v2").Add(float64(len(httpLogs.LogEntry))) - } - - log.Printf("Received v2 log data: %s\n", data.String()) - } - } - - type ALSServerV3 struct { - } - - func (a *ALSServerV3) StreamAccessLogs(logStream alsv3.AccessLogService_StreamAccessLogsServer) error { - log.Println("Streaming als v3 logs") - for { - data, err := logStream.Recv() - if err != nil { - return err - } - - httpLogs := data.GetHttpLogs() - if httpLogs != nil { - LogCount.WithLabelValues("v3").Add(float64(len(httpLogs.LogEntry))) - } - - log.Printf("Received v3 log data: %s\n", data.String()) - } - } - - func NewALSServer() *ALSServer { - return &ALSServer{} - } - - func NewALSServerV3() *ALSServerV3 { - return &ALSServerV3{} - } - - func main() { - mux := http.NewServeMux() - if err := addMonitor(mux); err != nil { - log.Printf("could not establish self-monitoring: %v\n", err) - } - - s := &http.Server{ - Addr: ":19001", - Handler: mux, - } - - go func() { - s.ListenAndServe() - }() - - listener, err := net.Listen("tcp", "0.0.0.0:8080") - if err != nil { - log.Fatalf("Failed to start listener on port 8080: %v", err) - } - - var opts []grpc.ServerOption - grpcServer := grpc.NewServer(opts...) - alsv2.RegisterAccessLogServiceServer(grpcServer, NewALSServer()) - alsv3.RegisterAccessLogServiceServer(grpcServer, NewALSServerV3()) - log.Println("Starting ALS Server") - if err := grpcServer.Serve(listener); err != nil { - log.Fatalf("grpc serve err: %v", err) - } - } - - func addMonitor(mux *http.ServeMux) error { - mux.Handle("/metrics", promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{EnableOpenMetrics: true})) - - return nil - } - ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: envoy-als - namespace: monitoring -spec: - replicas: 1 - selector: - matchLabels: - app: envoy-als - template: - metadata: - annotations: - prometheus.io/scrape: "true" - prometheus.io/port: "19001" - labels: - app: envoy-als - spec: - containers: - - name: envoy-als - command: - - sh - - "-c" - - "cp -a /app /app-live && cd /app-live && go run . " - image: golang:1.22.3-alpine - ports: - - containerPort: 8080 - - containerPort: 19001 - volumeMounts: - - name: envoy-als - mountPath: /app - volumes: - - name: envoy-als - configMap: - name: envoy-als +# This is a trick to pass EnvoyProxy BackendRef check. +# In the future, plan to make OTel-collector support envoy ALS receiver, +# then we can remove this. --- apiVersion: v1 kind: Service @@ -1105,67 +666,3 @@ spec: protocol: TCP port: 19001 targetPort: 19001 ---- -apiVersion: gateway.networking.k8s.io/v1 -kind: Gateway -metadata: - name: eg-special-case - namespace: gateway-conformance-infra -spec: - gatewayClassName: "{GATEWAY_CLASS_NAME}" - listeners: - - name: http - port: 80 - protocol: HTTP - allowedRoutes: - namespaces: - from: All - infrastructure: - parametersRef: - group: gateway.envoyproxy.io - kind: EnvoyProxy - name: zipkin-tracing ---- -# This is a EnvoyProxy used for Zipkin tracing and will -# target to the eg-special-case Gateway -apiVersion: gateway.envoyproxy.io/v1alpha1 -kind: EnvoyProxy -metadata: - name: zipkin-tracing - namespace: gateway-conformance-infra -spec: - logging: - level: - default: debug - telemetry: - tracing: - provider: - type: Zipkin - backendRefs: - - name: otel-collector - namespace: monitoring - port: 9411 - zipkin: - enable128BitTraceId: true - customTags: - "provider": - type: Literal - literal: - value: "zipkin" - "k8s.cluster.name": - type: Literal - literal: - value: "envoy-gateway" - "k8s.pod.name": - type: Environment - environment: - name: ENVOY_POD_NAME - defaultValue: "-" - "k8s.namespace.name": - type: Environment - environment: - name: ENVOY_GATEWAY_NAMESPACE - defaultValue: "envoy-gateway-system" - shutdown: - drainTimeout: 5s - minDrainDuration: 1s diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 35d52a8621c..e0e71f95173 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -13,39 +13,32 @@ import ( "io/fs" "testing" - "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/util/sets" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/config" "sigs.k8s.io/gateway-api/conformance/utils/flags" "sigs.k8s.io/gateway-api/conformance/utils/suite" + "sigs.k8s.io/gateway-api/conformance/utils/tlog" "sigs.k8s.io/gateway-api/pkg/features" "github.com/envoyproxy/gateway/test/e2e/tests" + kubetest "github.com/envoyproxy/gateway/test/utils/kubernetes" ) func TestE2E(t *testing.T) { flag.Parse() - cfg, err := config.GetConfig() - require.NoError(t, err) - - c, err := client.New(cfg, client.Options{}) - require.NoError(t, err) - - // Install all the scheme for kubernetes client. - CheckInstallScheme(t, c) + c, cfg := kubetest.NewClient(t) if flags.RunTest != nil && *flags.RunTest != "" { - t.Logf("Running E2E test %s with %s GatewayClass\n cleanup: %t\n debug: %t", + tlog.Logf(t, "Running E2E test %s with %s GatewayClass\n cleanup: %t\n debug: %t", *flags.RunTest, *flags.GatewayClassName, *flags.CleanupBaseResources, *flags.ShowDebug) } else { - t.Logf("Running E2E tests with %s GatewayClass\n cleanup: %t\n debug: %t", + tlog.Logf(t, "Running E2E tests with %s GatewayClass\n cleanup: %t\n debug: %t", *flags.GatewayClassName, *flags.CleanupBaseResources, *flags.ShowDebug) } cSuite, err := suite.NewConformanceTestSuite(suite.ConformanceOptions{ Client: c, + RestConfig: cfg, GatewayClassName: *flags.GatewayClassName, Debug: *flags.ShowDebug, CleanupBaseResources: *flags.CleanupBaseResources, @@ -55,9 +48,7 @@ func TestE2E(t *testing.T) { // All e2e tests should leave Features empty. SupportedFeatures: sets.New[features.SupportedFeature](features.SupportGateway), SkipTests: []string{ - tests.ClientTimeoutTest.ShortName, // https://github.com/envoyproxy/gateway/issues/2720 tests.GatewayInfraResourceTest.ShortName, // https://github.com/envoyproxy/gateway/issues/3191 - tests.UseClientProtocolTest.ShortName, // https://github.com/envoyproxy/gateway/issues/3473 }, }) if err != nil { @@ -65,9 +56,13 @@ func TestE2E(t *testing.T) { } cSuite.Setup(t, tests.ConformanceTests) - t.Logf("Running %d E2E tests", len(tests.ConformanceTests)) + if cSuite.RunTest != "" { + tlog.Logf(t, "Running E2E test %s", cSuite.RunTest) + } else { + tlog.Logf(t, "Running %d E2E tests", len(tests.ConformanceTests)) + } err = cSuite.Run(t, tests.ConformanceTests) if err != nil { - t.Fatalf("Failed to run E2E tests: %v", err) + tlog.Fatalf(t, "Failed to run E2E tests: %v", err) } } diff --git a/test/e2e/embed.go b/test/e2e/embed.go index 6b5a891a43f..7bf3080fac2 100644 --- a/test/e2e/embed.go +++ b/test/e2e/embed.go @@ -8,27 +8,10 @@ package e2e -import ( - "embed" - "testing" - - "github.com/stretchr/testify/require" - "sigs.k8s.io/controller-runtime/pkg/client" - gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" - gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" - gwapiv1a3 "sigs.k8s.io/gateway-api/apis/v1alpha3" - gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" - - egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" -) +import "embed" //go:embed testdata/*.yaml base/* var Manifests embed.FS -func CheckInstallScheme(t *testing.T, c client.Client) { - require.NoError(t, gwapiv1a3.Install(c.Scheme())) - require.NoError(t, gwapiv1a2.Install(c.Scheme())) - require.NoError(t, gwapiv1b1.Install(c.Scheme())) - require.NoError(t, gwapiv1.Install(c.Scheme())) - require.NoError(t, egv1a1.AddToScheme(c.Scheme())) -} +//go:embed testdata/*.yaml upgrade/* +var UpgradeManifests embed.FS diff --git a/test/e2e/merge_gateways/merge_gateways_test.go b/test/e2e/merge_gateways/merge_gateways_test.go index ca62bf19b60..fe8c616d2ed 100644 --- a/test/e2e/merge_gateways/merge_gateways_test.go +++ b/test/e2e/merge_gateways/merge_gateways_test.go @@ -13,41 +13,34 @@ import ( "io/fs" "testing" - "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/util/sets" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/config" "sigs.k8s.io/gateway-api/conformance/utils/flags" "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" "sigs.k8s.io/gateway-api/conformance/utils/suite" + "sigs.k8s.io/gateway-api/conformance/utils/tlog" "sigs.k8s.io/gateway-api/pkg/features" "github.com/envoyproxy/gateway/test/e2e" "github.com/envoyproxy/gateway/test/e2e/tests" + kubetest "github.com/envoyproxy/gateway/test/utils/kubernetes" ) func TestMergeGateways(t *testing.T) { flag.Parse() - cfg, err := config.GetConfig() - require.NoError(t, err) - - c, err := client.New(cfg, client.Options{}) - require.NoError(t, err) - - // Install all the scheme to kubernetes client. - e2e.CheckInstallScheme(t, c) + c, cfg := kubetest.NewClient(t) if flags.RunTest != nil && *flags.RunTest != "" { - t.Logf("Running E2E test %s with %s GatewayClass\n cleanup: %t\n debug: %t", + tlog.Logf(t, "Running E2E test %s with %s GatewayClass\n cleanup: %t\n debug: %t", *flags.RunTest, *flags.GatewayClassName, *flags.CleanupBaseResources, *flags.ShowDebug) } else { - t.Logf("Running E2E tests with %s GatewayClass\n cleanup: %t\n debug: %t", + tlog.Logf(t, "Running E2E tests with %s GatewayClass\n cleanup: %t\n debug: %t", *flags.GatewayClassName, *flags.CleanupBaseResources, *flags.ShowDebug) } cSuite, err := suite.NewConformanceTestSuite(suite.ConformanceOptions{ Client: c, + RestConfig: cfg, GatewayClassName: *flags.GatewayClassName, Debug: *flags.ShowDebug, CleanupBaseResources: *flags.CleanupBaseResources, @@ -67,7 +60,7 @@ func TestMergeGateways(t *testing.T) { cSuite.Applier.GatewayClass = *flags.GatewayClassName cSuite.ControllerName = kubernetes.GWCMustHaveAcceptedConditionTrue(t, cSuite.Client, cSuite.TimeoutConfig, cSuite.GatewayClassName) - t.Logf("Running %d MergeGateways tests", len(tests.MergeGatewaysTests)) + tlog.Logf(t, "Running %d MergeGateways tests", len(tests.MergeGatewaysTests)) err = cSuite.Run(t, tests.MergeGatewaysTests) if err != nil { t.Fatalf("Failed to run MergeGateways tests: %v", err) diff --git a/test/e2e/multiple_gc/multiple_gc_test.go b/test/e2e/multiple_gc/multiple_gc_test.go index 75b642bd16c..571d1afd33d 100644 --- a/test/e2e/multiple_gc/multiple_gc_test.go +++ b/test/e2e/multiple_gc/multiple_gc_test.go @@ -13,43 +13,37 @@ import ( "io/fs" "testing" - "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/util/sets" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/config" "sigs.k8s.io/gateway-api/conformance/utils/flags" "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" "sigs.k8s.io/gateway-api/conformance/utils/suite" + "sigs.k8s.io/gateway-api/conformance/utils/tlog" "sigs.k8s.io/gateway-api/pkg/features" "github.com/envoyproxy/gateway/test/e2e" "github.com/envoyproxy/gateway/test/e2e/tests" + kubetest "github.com/envoyproxy/gateway/test/utils/kubernetes" ) func TestMultipleGC(t *testing.T) { flag.Parse() - cfg, err := config.GetConfig() - require.NoError(t, err) - - c, err := client.New(cfg, client.Options{}) - require.NoError(t, err) - - // Install all the scheme to kubernetes client. - e2e.CheckInstallScheme(t, c) + c, cfg := kubetest.NewClient(t) if flags.RunTest != nil && *flags.RunTest != "" { - t.Logf("Running E2E test %s with %s GatewayClass\n cleanup: %t\n debug: %t", + tlog.Logf(t, "Running E2E test %s with %s GatewayClass\n cleanup: %t\n debug: %t", *flags.RunTest, *flags.GatewayClassName, *flags.CleanupBaseResources, *flags.ShowDebug) } else { - t.Logf("Running E2E tests with %s GatewayClass\n cleanup: %t\n debug: %t", + tlog.Logf(t, "Running E2E tests with %s GatewayClass\n cleanup: %t\n debug: %t", *flags.GatewayClassName, *flags.CleanupBaseResources, *flags.ShowDebug) } + t.Run("Internet GC Test", func(t *testing.T) { t.Parallel() internetGatewaySuiteGatewayClassName := "internet" internetGatewaySuite, err := suite.NewConformanceTestSuite(suite.ConformanceOptions{ Client: c, + RestConfig: cfg, GatewayClassName: internetGatewaySuiteGatewayClassName, Debug: *flags.ShowDebug, CleanupBaseResources: *flags.CleanupBaseResources, @@ -69,7 +63,7 @@ func TestMultipleGC(t *testing.T) { internetGatewaySuite.Applier.GatewayClass = internetGatewaySuiteGatewayClassName internetGatewaySuite.ControllerName = kubernetes.GWCMustHaveAcceptedConditionTrue(t, internetGatewaySuite.Client, internetGatewaySuite.TimeoutConfig, internetGatewaySuite.GatewayClassName) - t.Logf("Running %d MultipleGC tests", len(tests.MultipleGCTests[internetGatewaySuiteGatewayClassName])) + tlog.Logf(t, "Running %d MultipleGC tests", len(tests.MultipleGCTests[internetGatewaySuiteGatewayClassName])) err = internetGatewaySuite.Run(t, tests.MultipleGCTests[internetGatewaySuiteGatewayClassName]) if err != nil { @@ -101,7 +95,7 @@ func TestMultipleGC(t *testing.T) { privateGatewaySuite.Applier.GatewayClass = privateGatewaySuiteGatewayClassName privateGatewaySuite.ControllerName = kubernetes.GWCMustHaveAcceptedConditionTrue(t, privateGatewaySuite.Client, privateGatewaySuite.TimeoutConfig, privateGatewaySuite.GatewayClassName) - t.Logf("Running %d MultipleGC tests", len(tests.MultipleGCTests[privateGatewaySuiteGatewayClassName])) + tlog.Logf(t, "Running %d MultipleGC tests", len(tests.MultipleGCTests[privateGatewaySuiteGatewayClassName])) err = privateGatewaySuite.Run(t, tests.MultipleGCTests[privateGatewaySuiteGatewayClassName]) if err != nil { t.Fatalf("Failed to run PrivateGC tests: %v", err) diff --git a/test/e2e/testdata/accesslog-als.yaml b/test/e2e/testdata/accesslog-als.yaml index 86606c3c053..cd998df4655 100644 --- a/test/e2e/testdata/accesslog-als.yaml +++ b/test/e2e/testdata/accesslog-als.yaml @@ -14,3 +14,219 @@ spec: backendRefs: - name: infra-backend-v1 port: 8080 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: envoy-als + namespace: monitoring +data: + go.mod: | + module envoy-als + go 1.22 + require ( + github.com/envoyproxy/go-control-plane v0.12.0 + github.com/prometheus/client_golang v1.19.1 + google.golang.org/grpc v1.64.0 + ) + + require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50 // indirect + github.com/envoyproxy/protoc-gen-validate v1.0.4 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.48.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + golang.org/x/net v0.22.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + google.golang.org/protobuf v1.33.0 // indirect + ) + go.sum: | + github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= + github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= + github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= + github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= + github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50 h1:DBmgJDC9dTfkVyGgipamEh2BpGYxScCH1TOF1LL1cXc= + github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50/go.mod h1:5e1+Vvlzido69INQaVO6d87Qn543Xr6nooe9Kz7oBFM= + github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= + github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= + github.com/envoyproxy/go-control-plane v0.12.0 h1:4X+VP1GHd1Mhj6IB5mMeGbLCleqxjletLK6K0rbxyZI= + github.com/envoyproxy/go-control-plane v0.12.0/go.mod h1:ZBTaoJ23lqITozF0M6G4/IragXCQKCnYbmlmtHvwRG0= + github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A= + github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew= + github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= + github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= + github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= + github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= + github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= + github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= + github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= + github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= + github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= + github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= + github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= + github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= + golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= + golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= + golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= + golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= + golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= + golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= + google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc= + google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= + google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= + google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= + google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= + google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= + main.go: | + package main + + import ( + "log" + "net" + "net/http" + + alsv2 "github.com/envoyproxy/go-control-plane/envoy/service/accesslog/v2" + alsv3 "github.com/envoyproxy/go-control-plane/envoy/service/accesslog/v3" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + + "google.golang.org/grpc" + ) + + var ( + LogCount = prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "log_count", + Help: "The total number of logs received.", + }, []string{"api_version"}) + ) + + func init() { + // Register the summary and the histogram with Prometheus's default registry. + prometheus.MustRegister(LogCount) + } + + type ALSServer struct { + } + + func (a *ALSServer) StreamAccessLogs(logStream alsv2.AccessLogService_StreamAccessLogsServer) error { + log.Println("Streaming als v2 logs") + for { + data, err := logStream.Recv() + if err != nil { + return err + } + + httpLogs := data.GetHttpLogs() + if httpLogs != nil { + LogCount.WithLabelValues("v2").Add(float64(len(httpLogs.LogEntry))) + } + + log.Printf("Received v2 log data: %s\n", data.String()) + } + } + + type ALSServerV3 struct { + } + + func (a *ALSServerV3) StreamAccessLogs(logStream alsv3.AccessLogService_StreamAccessLogsServer) error { + log.Println("Streaming als v3 logs") + for { + data, err := logStream.Recv() + if err != nil { + return err + } + + httpLogs := data.GetHttpLogs() + if httpLogs != nil { + LogCount.WithLabelValues("v3").Add(float64(len(httpLogs.LogEntry))) + } + + log.Printf("Received v3 log data: %s\n", data.String()) + } + } + + func NewALSServer() *ALSServer { + return &ALSServer{} + } + + func NewALSServerV3() *ALSServerV3 { + return &ALSServerV3{} + } + + func main() { + mux := http.NewServeMux() + if err := addMonitor(mux); err != nil { + log.Printf("could not establish self-monitoring: %v\n", err) + } + + s := &http.Server{ + Addr: ":19001", + Handler: mux, + } + + go func() { + s.ListenAndServe() + }() + + listener, err := net.Listen("tcp", "0.0.0.0:8080") + if err != nil { + log.Fatalf("Failed to start listener on port 8080: %v", err) + } + + var opts []grpc.ServerOption + grpcServer := grpc.NewServer(opts...) + alsv2.RegisterAccessLogServiceServer(grpcServer, NewALSServer()) + alsv3.RegisterAccessLogServiceServer(grpcServer, NewALSServerV3()) + log.Println("Starting ALS Server") + if err := grpcServer.Serve(listener); err != nil { + log.Fatalf("grpc serve err: %v", err) + } + } + + func addMonitor(mux *http.ServeMux) error { + mux.Handle("/metrics", promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{EnableOpenMetrics: true})) + + return nil + } + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: envoy-als + namespace: monitoring +spec: + replicas: 1 + selector: + matchLabels: + app: envoy-als + template: + metadata: + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "19001" + labels: + app: envoy-als + spec: + containers: + - name: envoy-als + command: + - sh + - "-c" + - "cp -a /app /app-live && cd /app-live && go run . " + image: golang:1.22.3-alpine + ports: + - containerPort: 8080 + - containerPort: 19001 + volumeMounts: + - name: envoy-als + mountPath: /app + volumes: + - name: envoy-als + configMap: + name: envoy-als diff --git a/test/e2e/testdata/authorization-client-ip.yaml b/test/e2e/testdata/authorization-client-ip.yaml index 08ead703cca..cf3ad4b88f7 100644 --- a/test/e2e/testdata/authorization-client-ip.yaml +++ b/test/e2e/testdata/authorization-client-ip.yaml @@ -38,8 +38,8 @@ metadata: name: authorization-client-ip-1 namespace: gateway-conformance-infra spec: - targetRef: - group: gateway.networking.k8s.io + targetRefs: + - group: gateway.networking.k8s.io kind: HTTPRoute name: http-with-authorization-client-ip-1 authorization: @@ -68,8 +68,8 @@ metadata: name: authorization-client-ip-2 namespace: gateway-conformance-infra spec: - targetRef: - group: gateway.networking.k8s.io + targetRefs: + - group: gateway.networking.k8s.io kind: HTTPRoute name: http-with-authorization-client-ip-2 authorization: @@ -92,7 +92,7 @@ spec: clientIPDetection: xForwardedFor: numTrustedHops: 1 - targetRef: - group: gateway.networking.k8s.io + targetRefs: + - group: gateway.networking.k8s.io kind: Gateway name: same-namespace diff --git a/test/e2e/testdata/authorization-default-action.yaml b/test/e2e/testdata/authorization-default-action.yaml index bc35b2ba80b..68b051d2763 100644 --- a/test/e2e/testdata/authorization-default-action.yaml +++ b/test/e2e/testdata/authorization-default-action.yaml @@ -38,8 +38,8 @@ metadata: name: authorization-empty namespace: gateway-conformance-infra spec: - targetRef: - group: gateway.networking.k8s.io + targetRefs: + - group: gateway.networking.k8s.io kind: HTTPRoute name: http-with-authorization-empty authorization: {} # An empty authorization policy means deny all since default action is deny @@ -50,8 +50,8 @@ metadata: name: authorization-allow-all namespace: gateway-conformance-infra spec: - targetRef: - group: gateway.networking.k8s.io + targetRefs: + - group: gateway.networking.k8s.io kind: HTTPRoute name: http-with-authorization-allow-all authorization: # Allow all since default action is allow and no rules are defined diff --git a/test/e2e/testdata/backend-health-check-active-http.yaml b/test/e2e/testdata/backend-health-check-active-http.yaml index 999029c9d8a..1de49c63972 100644 --- a/test/e2e/testdata/backend-health-check-active-http.yaml +++ b/test/e2e/testdata/backend-health-check-active-http.yaml @@ -8,7 +8,6 @@ spec: - group: gateway.networking.k8s.io kind: HTTPRoute name: http-with-health-check-active-http-pass - namespace: gateway-conformance-infra healthCheck: active: timeout: 3s @@ -31,7 +30,6 @@ spec: - group: gateway.networking.k8s.io kind: HTTPRoute name: http-with-health-check-active-http-fail - namespace: gateway-conformance-infra healthCheck: active: timeout: 3s @@ -89,7 +87,6 @@ spec: group: gateway.networking.k8s.io kind: Gateway name: same-namespace - namespace: gateway-conformance-infra type: JSONPatch jsonPatches: - type: "type.googleapis.com/envoy.config.cluster.v3.Cluster" diff --git a/test/e2e/testdata/backend-tls-settings.yaml b/test/e2e/testdata/backend-tls-settings.yaml index ffd8abcdd27..749255f82e5 100644 --- a/test/e2e/testdata/backend-tls-settings.yaml +++ b/test/e2e/testdata/backend-tls-settings.yaml @@ -1,3 +1,30 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: backend-namespaces + namespace: gateway-conformance-infra +spec: + gatewayClassName: "{GATEWAY_CLASS_NAME}" + listeners: + - name: https + port: 443 + protocol: HTTPS + tls: + certificateRefs: + - group: "" + kind: Secret + name: backend-tls-certificate + mode: Terminate + - name: http + port: 80 + protocol: HTTP + allowedRoutes: + namespaces: + from: Selector + selector: + matchLabels: + gateway-conformance: backend +--- apiVersion: v1 data: ca.crt: | diff --git a/test/e2e/testdata/backend-upgrade.yaml b/test/e2e/testdata/backend-upgrade.yaml index e708342406d..7915d94fa5e 100644 --- a/test/e2e/testdata/backend-upgrade.yaml +++ b/test/e2e/testdata/backend-upgrade.yaml @@ -4,11 +4,10 @@ metadata: name: backend-upgrade-example namespace: gateway-conformance-infra spec: - targetRef: - group: gateway.networking.k8s.io + targetRefs: + - group: gateway.networking.k8s.io kind: HTTPRoute name: http-backend-upgrade - namespace: gateway-conformance-infra circuitBreaker: maxParallelRequests: 10000 maxConnections: 10000 diff --git a/test/e2e/testdata/basic-auth.yaml b/test/e2e/testdata/basic-auth.yaml index db99cf2cd85..4caef4b7c08 100644 --- a/test/e2e/testdata/basic-auth.yaml +++ b/test/e2e/testdata/basic-auth.yaml @@ -55,11 +55,10 @@ metadata: name: basic-auth-1 namespace: gateway-conformance-infra spec: - targetRef: - group: gateway.networking.k8s.io + targetRefs: + - group: gateway.networking.k8s.io kind: HTTPRoute name: http-with-basic-auth-1 - namespace: gateway-conformance-infra basicAuth: users: name: "basic-auth-users-secret-1" @@ -70,8 +69,8 @@ metadata: name: basic-auth-2 namespace: gateway-conformance-infra spec: - targetRef: - group: gateway.networking.k8s.io + targetRefs: + - group: gateway.networking.k8s.io kind: HTTPRoute name: http-with-basic-auth-2 basicAuth: diff --git a/test/e2e/testdata/circuitbreaker.yaml b/test/e2e/testdata/circuitbreaker.yaml index 912c041cf80..fddeca49f34 100644 --- a/test/e2e/testdata/circuitbreaker.yaml +++ b/test/e2e/testdata/circuitbreaker.yaml @@ -4,11 +4,10 @@ metadata: name: circuitbreaker-example namespace: gateway-conformance-infra spec: - targetRef: - group: gateway.networking.k8s.io + targetRefs: + - group: gateway.networking.k8s.io kind: HTTPRoute name: http-with-circuitbreaker - namespace: gateway-conformance-infra circuitBreaker: maxConnections: 0 maxParallelRequests: 0 diff --git a/test/e2e/testdata/client-mtls.yaml b/test/e2e/testdata/client-mtls.yaml new file mode 100644 index 00000000000..fc1a62f1ca4 --- /dev/null +++ b/test/e2e/testdata/client-mtls.yaml @@ -0,0 +1,71 @@ +apiVersion: v1 +data: + tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRQ3N0SDg4dVdtVmJUSTkKTlZHemc1SlVrZFp0dWRpclFOci81SHpqYU9lbmVBVDVZSTFNRmtGVEVVcTFOWU84VHBGYittL3h0N1o1Z0Y2VwpIK2xVZGZOdG96czZmdFFodkhvaFJ5WERuNmxvbU52THVxa1ZuaVQxWHMxN2dKZG9NYW00anVpcmVxWjYrSXFvClU0Ui9yNDFyRmphUWlGNUpkYTdtRjU4VmYrQWZBcW5FU011aG9TYzUyUmVNT0t3T2czaFVYM2NyZFlMcHBIZTUKemxGMS91R045cE1DeFJEdHhySUVQTG9tUXdYTGp5dlJlQmcrZHJlbHpkRHBIdWVlTVNMMVZxSUV1VnZjVDBmNgpqWFFXUndtVzhEWElPRURHSHBuNkpDWFVNL00xQ0s3ODFDSnQ3NlBTdTZ2Tm0rY3VSQ1o5bHhQZlV0cWVrMzYzCmg1cFQ4eWVMQWdNQkFBRUNnZ0VBUGZHWFdWNWZRbHNpRTlZVjhiOFZNd1FEMXVNN2xXRHpxNmgyS3RLcmsrTjIKVlVPT0ttWXBFL1ExeVh6aVBSNHBBTVo0ZEVnaVNMNEVMdkJQTGg0SUs2REtVNWE5MjBQUXdqTFVscStqZ1lLaQpBeFoySXV6UDhMb0tGV3Z2NGswOXhKWnRXV1BxWmx6U3pzRDEyeDVLS01pVTFZNHowMEZFYy82dzduTXBIYld0CndobjdJZmhFV0s2UWVIRUlWZklaZThhZEduNTE1elVLWndjWWZxZFZrdEtNNmJjeUIrWjJqWGRBLzA4Wmd6U3YKNmxKY3pPaXA4MDFaK0pYZnlnbDJZNDRPdURQV3Q1cytVaTgwODFrTndxUmkweFBlN3JUTG9RU2dRM2piYnY1RQppUkkwQzM2ekx0dmR0R21GZGNIbXdDVDJVZlBNUDZxRW9ReTd1eXB3Z1FLQmdRRGVLLzhIUndxdXdTdjBxRG5FCkd4RzBuYVVIbEFFcE5ycFdUVjg5T1UxSklHUC8yMzNBYmJIbkZqWlhqUzRadC83L1NmVkRGOFlMdGpIKzRCQXYKOWNhNXdhZ1pkblh4SkcxWmpXY0VvMXlpTERsT0lDTitpbkc2SDFSZTM5T0hKSzVUZk1YcEhNZVpHSXREZU56Twp1N0NCOEp0RkdTRE14YXNNNWlPK2tza3Vxd0tCZ1FESEFGZVFLbDZrQmo5TThLZ0NzQzJIVzY0SVZaejluQTRwClpSbFViaTc5Y3lBakJMdE4rOFlpVnlVNUUzTTZWYXNiREtqT3dWaUltMEYvOE9Hck9MU2paRFRicG1Rd01tMDUKMXp0bUU2UittSXFnNm1CK1ZWWko0OU9UTTBVWkdWK3k5ZmsyMWxKS2ZMQWhEdkhUajF0MldlNDEvTktheDlLagpUNE5OZWs1cW9RS0JnRmR2ZVM2a0J0QS90Y3pUWitpRnZySVdXSnhpNHhJVk44QS9NdndsVVFVMmVpUjNmVVg5CjVjWE1PQmE4dVNmbUFiVDJwaytCUzYvUVJGektVWlNPWldjMWo3SjFySFQ2b1E1ZFYrMjdYUGwxN2hlUkRtYisKbFVOcWtRbkZqTG5pOWJobG9uM2JsYkhxdHVRVzdNOXZqa2VWS1laSUhXQjVhcGZvK3FoRm5HZUJBb0dCQUoraQpLcHUvSVVRNnJyU3RxRmpmc3VZT0tVRU9URzAwcmplT25nOFloQ0ZVSk1keW1ENHFaZnBPSmRIdlJGUGJQUExwCjZOQlNVUTdrWEZxaGhLV3hTMVoybitCRFRjTkw4RXFoMlVnOUlZdGNHbmlUQ3V6TXovZGVCdTdpQmQvb2R0ZzgKY0xvSW11S2R1endKblB1MDJBM01ma1pZbFNrTWVtLys3TGxPRDNHQkFvR0FKTnFZSmtuN1A3WFRtVnp0dGNDMgpPR3d4NzQ0bmdBV3JZcGg3Nm94cmoxWHBRQ3NvMlo4RFV0NzJ2anlJQnNHMytFNHNRL3c4WWFaNUE3R0ErOGpCCkJ2UVBFS0l2QzZ6WEZIQ3lOeTM1MFdjaFZFa1dzK2k2YVl1elZTRVVOWjV4RlFHcExXa0hJMFo5cXN5eTlsUmMKT2tFVmFqTHd3cXBTK2ZFTGljcVRjUTQ9Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0= + tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUR6akNDQXJhZ0F3SUJBZ0lVT0dKOUx1VGtKWkU0NmNVaUpGYmJ2bm10elFvd0RRWUpLb1pJaHZjTkFRRUwKQlFBd2J6RUxNQWtHQTFVRUJoTUNWVk14Q3pBSkJnTlZCQWdNQWxaQk1SRXdEd1lEVlFRSERBaFRiMjFsUTJsMAplVEVUTUJFR0ExVUVDZ3dLUlc1MmIzbFFjbTk0ZVRFUU1BNEdBMVVFQ3d3SFIyRjBaWGRoZVRFWk1CY0dBMVVFCkF3d1FiWFJzY3k1bGVHRnRjR3hsTG1OdmJUQWdGdzB5TkRBM01UWXlNalV4TWpOYUdBOHlNVEkwTURZeU1qSXkKTlRFeU0xb3diekVMTUFrR0ExVUVCaE1DVlZNeEN6QUpCZ05WQkFnTUFsWkJNUkV3RHdZRFZRUUhEQWhUYjIxbApRMmwwZVRFVE1CRUdBMVVFQ2d3S1JXNTJiM2xRY205NGVURVFNQTRHQTFVRUN3d0hSMkYwWlhkaGVURVpNQmNHCkExVUVBd3dRYlhSc2N5NWxlR0Z0Y0d4bExtTnZiVENDQVNJd0RRWUpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFEQ0MKQVFvQ2dnRUJBS3kwZnp5NWFaVnRNajAxVWJPRGtsU1IxbTI1Mkt0QTJ2L2tmT05vNTZkNEJQbGdqVXdXUVZNUgpTclUxZzd4T2tWdjZiL0czdG5tQVhwWWY2VlIxODIyak96cCsxQ0c4ZWlGSEpjT2ZxV2lZMjh1NnFSV2VKUFZlCnpYdUFsMmd4cWJpTzZLdDZwbnI0aXFoVGhIK3ZqV3NXTnBDSVhrbDFydVlYbnhWLzRCOENxY1JJeTZHaEp6bloKRjR3NHJBNkRlRlJmZHl0MWd1bWtkN25PVVhYKzRZMzJrd0xGRU8zR3NnUTh1aVpEQmN1UEs5RjRHRDUydDZYTgowT2tlNTU0eEl2VldvZ1M1Vzl4UFIvcU5kQlpIQ1pid05jZzRRTVllbWZva0pkUXo4elVJcnZ6VUltM3ZvOUs3CnE4MmI1eTVFSm4yWEU5OVMycDZUZnJlSG1sUHpKNHNDQXdFQUFhTmdNRjR3Q3dZRFZSMFBCQVFEQWdTd01CTUcKQTFVZEpRUU1NQW9HQ0NzR0FRVUZCd01CTUJzR0ExVWRFUVFVTUJLQ0VHMTBiSE11WlhoaGJYQnNaUzVqYjIwdwpIUVlEVlIwT0JCWUVGRm1FTjBqRVFpckpYeGlLRHFlK2tTMVV3Q2gyTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCCkFRQ0NTVlluRVJPbHJpWDM2M0VtRzd1b091Nm54ajU1eWVmOXRKbnRubFVMVFZsMjlqc205Z3d5VnFUVCtUcXMKdzFPYW01TExmMEpjSWNRdmFUM203b0RpMElDUUo5eTlRQkNwMTh1TDBUeElDaFdVRTVnRUIxM3MyTzEwWWNFMQp1K2ozSzM0MStQNStoaEJpQnJ1d0dncStkVVRGRTZTYVVMY0xMVlB1RjdTeG1KbTRHK0Q0NVlqM1NDVy9aMzU2CkFXZzB4L0prZGFKSVVLVFJaUDVJTEZKQ1lJTUM3QWI1RmdWeGRCVW5kNWxheUZGb2NVMzRQaDlwZUxiYW00alYKdGt0SGNTSFJ6OERNTm1PNHpHTEZYNzlQR0lsaTZzTDU3V0N6bkw5RFFtajRyajFIS2tyeEdnMVExbUcwbDhOTQo5cXQyWEZNNUttWkVOb2E1MmFWSklHYWoKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= + ca.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUR6akNDQXJhZ0F3SUJBZ0lVT0dKOUx1VGtKWkU0NmNVaUpGYmJ2bm10elFvd0RRWUpLb1pJaHZjTkFRRUwKQlFBd2J6RUxNQWtHQTFVRUJoTUNWVk14Q3pBSkJnTlZCQWdNQWxaQk1SRXdEd1lEVlFRSERBaFRiMjFsUTJsMAplVEVUTUJFR0ExVUVDZ3dLUlc1MmIzbFFjbTk0ZVRFUU1BNEdBMVVFQ3d3SFIyRjBaWGRoZVRFWk1CY0dBMVVFCkF3d1FiWFJzY3k1bGVHRnRjR3hsTG1OdmJUQWdGdzB5TkRBM01UWXlNalV4TWpOYUdBOHlNVEkwTURZeU1qSXkKTlRFeU0xb3diekVMTUFrR0ExVUVCaE1DVlZNeEN6QUpCZ05WQkFnTUFsWkJNUkV3RHdZRFZRUUhEQWhUYjIxbApRMmwwZVRFVE1CRUdBMVVFQ2d3S1JXNTJiM2xRY205NGVURVFNQTRHQTFVRUN3d0hSMkYwWlhkaGVURVpNQmNHCkExVUVBd3dRYlhSc2N5NWxlR0Z0Y0d4bExtTnZiVENDQVNJd0RRWUpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFEQ0MKQVFvQ2dnRUJBS3kwZnp5NWFaVnRNajAxVWJPRGtsU1IxbTI1Mkt0QTJ2L2tmT05vNTZkNEJQbGdqVXdXUVZNUgpTclUxZzd4T2tWdjZiL0czdG5tQVhwWWY2VlIxODIyak96cCsxQ0c4ZWlGSEpjT2ZxV2lZMjh1NnFSV2VKUFZlCnpYdUFsMmd4cWJpTzZLdDZwbnI0aXFoVGhIK3ZqV3NXTnBDSVhrbDFydVlYbnhWLzRCOENxY1JJeTZHaEp6bloKRjR3NHJBNkRlRlJmZHl0MWd1bWtkN25PVVhYKzRZMzJrd0xGRU8zR3NnUTh1aVpEQmN1UEs5RjRHRDUydDZYTgowT2tlNTU0eEl2VldvZ1M1Vzl4UFIvcU5kQlpIQ1pid05jZzRRTVllbWZva0pkUXo4elVJcnZ6VUltM3ZvOUs3CnE4MmI1eTVFSm4yWEU5OVMycDZUZnJlSG1sUHpKNHNDQXdFQUFhTmdNRjR3Q3dZRFZSMFBCQVFEQWdTd01CTUcKQTFVZEpRUU1NQW9HQ0NzR0FRVUZCd01CTUJzR0ExVWRFUVFVTUJLQ0VHMTBiSE11WlhoaGJYQnNaUzVqYjIwdwpIUVlEVlIwT0JCWUVGRm1FTjBqRVFpckpYeGlLRHFlK2tTMVV3Q2gyTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCCkFRQ0NTVlluRVJPbHJpWDM2M0VtRzd1b091Nm54ajU1eWVmOXRKbnRubFVMVFZsMjlqc205Z3d5VnFUVCtUcXMKdzFPYW01TExmMEpjSWNRdmFUM203b0RpMElDUUo5eTlRQkNwMTh1TDBUeElDaFdVRTVnRUIxM3MyTzEwWWNFMQp1K2ozSzM0MStQNStoaEJpQnJ1d0dncStkVVRGRTZTYVVMY0xMVlB1RjdTeG1KbTRHK0Q0NVlqM1NDVy9aMzU2CkFXZzB4L0prZGFKSVVLVFJaUDVJTEZKQ1lJTUM3QWI1RmdWeGRCVW5kNWxheUZGb2NVMzRQaDlwZUxiYW00alYKdGt0SGNTSFJ6OERNTm1PNHpHTEZYNzlQR0lsaTZzTDU3V0N6bkw5RFFtajRyajFIS2tyeEdnMVExbUcwbDhOTQo5cXQyWEZNNUttWkVOb2E1MmFWSklHYWoKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= +kind: Secret +metadata: + name: client-mtls-certificate + namespace: gateway-conformance-infra +type: kubernetes.io/tls +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: client-mtls-gateway + namespace: gateway-conformance-infra +spec: + gatewayClassName: "{GATEWAY_CLASS_NAME}" + listeners: + - name: mtls + port: 443 + protocol: HTTPS + hostname: mtls.example.com + tls: + certificateRefs: + - group: "" + kind: Secret + name: client-mtls-certificate + mode: Terminate +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: http-client-mtls + namespace: gateway-conformance-infra +spec: + hostnames: + - mtls.example.com + parentRefs: + - name: client-mtls-gateway + rules: + - matches: + - path: + type: PathPrefix + value: /client-mtls + backendRefs: + - name: infra-backend-v1 + port: 8080 +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: ClientTrafficPolicy +metadata: + name: ctp-client-mtls + namespace: gateway-conformance-infra +spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: client-mtls-gateway + sectionName: mtls + headers: + xForwardedClientCert: + certDetailsToAdd: + - Subject + mode: SanitizeSet + tls: + clientValidation: + caCertificateRefs: + - kind: "Secret" + group: "" + name: "client-mtls-certificate" diff --git a/test/e2e/testdata/client-timeout.yaml b/test/e2e/testdata/client-timeout.yaml index a2776dffbf6..43cf26ec3cf 100644 --- a/test/e2e/testdata/client-timeout.yaml +++ b/test/e2e/testdata/client-timeout.yaml @@ -1,17 +1,30 @@ apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: BackendTrafficPolicy +metadata: + name: client-timeout + namespace: gateway-conformance-infra +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: http-client-timeout + faultInjection: + delay: + fixedDelay: 100ms +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 kind: ClientTrafficPolicy metadata: name: client-timeout namespace: gateway-conformance-infra spec: - targetRef: - group: gateway.networking.k8s.io - kind: Gateway - name: same-namespace - namespace: gateway-conformance-infra + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: same-namespace timeout: http: - requestReceivedTimeout: 1ms + requestReceivedTimeout: 50ms --- apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute diff --git a/test/e2e/testdata/connection-limit.yaml b/test/e2e/testdata/connection-limit.yaml index 3edb268aad8..61f18642a97 100644 --- a/test/e2e/testdata/connection-limit.yaml +++ b/test/e2e/testdata/connection-limit.yaml @@ -1,14 +1,28 @@ +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: Gateway +metadata: + name: connection-limit-gateway + namespace: gateway-conformance-infra +spec: + gatewayClassName: "{GATEWAY_CLASS_NAME}" + listeners: + - name: http + port: 80 + protocol: HTTP + allowedRoutes: + namespaces: + from: Same +--- apiVersion: gateway.envoyproxy.io/v1alpha1 kind: ClientTrafficPolicy metadata: name: connection-limit-ctp namespace: gateway-conformance-infra spec: - targetRef: - group: gateway.networking.k8s.io - kind: Gateway - name: connection-limit-gateway - namespace: gateway-conformance-infra + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: connection-limit-gateway connection: connectionLimit: value: 3 diff --git a/test/e2e/testdata/cors.yaml b/test/e2e/testdata/cors.yaml index c823b97f646..0c20bfa1998 100644 --- a/test/e2e/testdata/cors.yaml +++ b/test/e2e/testdata/cors.yaml @@ -4,11 +4,10 @@ metadata: name: cors-example namespace: gateway-conformance-infra spec: - targetRef: - group: gateway.networking.k8s.io + targetRefs: + - group: gateway.networking.k8s.io kind: HTTPRoute name: http-with-cors - namespace: gateway-conformance-infra cors: allowOrigins: - "https://www.foo.com" diff --git a/test/e2e/testdata/envoy-patch-policy.yaml b/test/e2e/testdata/envoy-patch-policy.yaml index e8f56215278..ede3800d451 100644 --- a/test/e2e/testdata/envoy-patch-policy.yaml +++ b/test/e2e/testdata/envoy-patch-policy.yaml @@ -26,7 +26,6 @@ spec: group: gateway.networking.k8s.io kind: Gateway name: same-namespace - namespace: gateway-conformance-infra type: JSONPatch jsonPatches: - type: "type.googleapis.com/envoy.config.listener.v3.Listener" diff --git a/test/e2e/testdata/ext-auth-grpc-securitypolicy.yaml b/test/e2e/testdata/ext-auth-grpc-securitypolicy.yaml index 3f40fc5830a..c75ee250f09 100644 --- a/test/e2e/testdata/ext-auth-grpc-securitypolicy.yaml +++ b/test/e2e/testdata/ext-auth-grpc-securitypolicy.yaml @@ -41,8 +41,8 @@ metadata: name: ext-auth-test namespace: gateway-conformance-infra spec: - targetRef: - group: gateway.networking.k8s.io + targetRefs: + - group: gateway.networking.k8s.io kind: HTTPRoute name: http-with-ext-auth extAuth: diff --git a/test/e2e/testdata/ext-auth-http-securitypolicy.yaml b/test/e2e/testdata/ext-auth-http-securitypolicy.yaml index e77dbde80e5..c6a1e73c6a6 100644 --- a/test/e2e/testdata/ext-auth-http-securitypolicy.yaml +++ b/test/e2e/testdata/ext-auth-http-securitypolicy.yaml @@ -41,8 +41,8 @@ metadata: name: ext-auth-test namespace: gateway-conformance-infra spec: - targetRef: - group: gateway.networking.k8s.io + targetRefs: + - group: gateway.networking.k8s.io kind: HTTPRoute name: http-with-ext-auth extAuth: diff --git a/test/e2e/testdata/ext-proc-envoyextensionpolicy.yaml b/test/e2e/testdata/ext-proc-envoyextensionpolicy.yaml index b495a7b9f7e..3663e19b610 100644 --- a/test/e2e/testdata/ext-proc-envoyextensionpolicy.yaml +++ b/test/e2e/testdata/ext-proc-envoyextensionpolicy.yaml @@ -41,8 +41,8 @@ metadata: name: ext-proc-test namespace: gateway-conformance-infra spec: - targetRef: - group: gateway.networking.k8s.io + targetRefs: + - group: gateway.networking.k8s.io kind: HTTPRoute name: http-with-ext-proc extProc: @@ -60,8 +60,8 @@ metadata: name: ext-proc-no-procmode-test namespace: gateway-conformance-infra spec: - targetRef: - group: gateway.networking.k8s.io + targetRefs: + - group: gateway.networking.k8s.io kind: HTTPRoute name: http-without-procmode extProc: @@ -122,8 +122,8 @@ metadata: name: ext-proc-uds-test namespace: gateway-conformance-infra spec: - targetRef: - group: gateway.networking.k8s.io + targetRefs: + - group: gateway.networking.k8s.io kind: HTTPRoute name: http-with-extproc-uds-tls extProc: diff --git a/test/e2e/testdata/fault-injection.yaml b/test/e2e/testdata/fault-injection.yaml index 13ec352d7aa..b52112ccd09 100644 --- a/test/e2e/testdata/fault-injection.yaml +++ b/test/e2e/testdata/fault-injection.yaml @@ -4,11 +4,10 @@ metadata: name: fault-injection-abort namespace: gateway-conformance-infra spec: - targetRef: - group: gateway.networking.k8s.io - kind: HTTPRoute - name: http-fault-abort - namespace: gateway-conformance-infra + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: http-fault-abort faultInjection: abort: httpStatus: 501 @@ -19,11 +18,10 @@ metadata: name: fault-injection-abort-and-delay namespace: gateway-conformance-infra spec: - targetRef: - group: gateway.networking.k8s.io - kind: HTTPRoute - name: http-route-delayandabort - namespace: gateway-conformance-infra + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: http-route-delayandabort faultInjection: delay: fixedDelay: 2s diff --git a/test/e2e/testdata/jwt-optional.yaml b/test/e2e/testdata/jwt-optional.yaml index 398040fd1dd..d5ca319fa03 100644 --- a/test/e2e/testdata/jwt-optional.yaml +++ b/test/e2e/testdata/jwt-optional.yaml @@ -4,10 +4,10 @@ metadata: name: jwt-optional namespace: gateway-conformance-infra spec: - targetRef: - group: gateway.networking.k8s.io - kind: HTTPRoute - name: jwt-optional + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: jwt-optional jwt: providers: - name: example diff --git a/test/e2e/testdata/listener-health-check.yaml b/test/e2e/testdata/listener-health-check.yaml index eb8066334e3..08f2f6b5d7f 100644 --- a/test/e2e/testdata/listener-health-check.yaml +++ b/test/e2e/testdata/listener-health-check.yaml @@ -4,11 +4,10 @@ metadata: name: health-check-ctp namespace: gateway-conformance-infra spec: - targetRef: - group: gateway.networking.k8s.io - kind: Gateway - name: same-namespace - namespace: gateway-conformance-infra + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: same-namespace healthCheck: path: /ready --- diff --git a/test/e2e/testdata/load_balancing_consistent_hash_cookie.yaml b/test/e2e/testdata/load_balancing_consistent_hash_cookie.yaml new file mode 100644 index 00000000000..45d927bf2d5 --- /dev/null +++ b/test/e2e/testdata/load_balancing_consistent_hash_cookie.yaml @@ -0,0 +1,85 @@ +apiVersion: v1 +kind: Service +metadata: + name: lb-backend-cookie + namespace: gateway-conformance-infra +spec: + selector: + app: lb-backend-cookie + ports: + - protocol: TCP + port: 8080 + targetPort: 3000 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: lb-backend-cookie + namespace: gateway-conformance-infra + labels: + app: lb-backend-cookie +spec: + replicas: 4 + selector: + matchLabels: + app: lb-backend-cookie + template: + metadata: + labels: + app: lb-backend-cookie + spec: + containers: + - name: backend + image: gcr.io/k8s-staging-gateway-api/echo-basic:v20231214-v1.0.0-140-gf544a46e + imagePullPolicy: IfNotPresent + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: SERVICE_NAME + value: lb-backend-cookie + resources: + requests: + cpu: 10m +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: BackendTrafficPolicy +metadata: + name: cookie-lb-policy + namespace: gateway-conformance-infra +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: cookie-lb-route + loadBalancer: + type: ConsistentHash + consistentHash: + type: Cookie + cookie: + name: "Lb-Test-Cookie" + ttl: 60s + attributes: + SameSite: Strict +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: cookie-lb-route + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + rules: + - matches: + - path: + type: PathPrefix + value: /cookie + backendRefs: + - name: lb-backend-cookie + port: 8080 diff --git a/test/e2e/testdata/load_balancing_consistent_hash_header.yaml b/test/e2e/testdata/load_balancing_consistent_hash_header.yaml new file mode 100644 index 00000000000..2c675a602b7 --- /dev/null +++ b/test/e2e/testdata/load_balancing_consistent_hash_header.yaml @@ -0,0 +1,82 @@ +apiVersion: v1 +kind: Service +metadata: + name: lb-backend-header + namespace: gateway-conformance-infra +spec: + selector: + app: lb-backend-header + ports: + - protocol: TCP + port: 8080 + targetPort: 3000 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: lb-backend-header + namespace: gateway-conformance-infra + labels: + app: lb-backend-header +spec: + replicas: 4 + selector: + matchLabels: + app: lb-backend-header + template: + metadata: + labels: + app: lb-backend-header + spec: + containers: + - name: backend + image: gcr.io/k8s-staging-gateway-api/echo-basic:v20231214-v1.0.0-140-gf544a46e + imagePullPolicy: IfNotPresent + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: SERVICE_NAME + value: lb-backend-header + resources: + requests: + cpu: 10m +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: BackendTrafficPolicy +metadata: + name: header-lb-policy + namespace: gateway-conformance-infra +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: header-lb-route + loadBalancer: + type: ConsistentHash + consistentHash: + type: Header + header: + name: "Lb-Test-Header" +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: header-lb-route + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + rules: + - matches: + - path: + type: PathPrefix + value: /header + backendRefs: + - name: lb-backend-header + port: 8080 diff --git a/test/e2e/testdata/load_balancing_consistent_hash_source_ip.yaml b/test/e2e/testdata/load_balancing_consistent_hash_source_ip.yaml new file mode 100644 index 00000000000..c83c5e78160 --- /dev/null +++ b/test/e2e/testdata/load_balancing_consistent_hash_source_ip.yaml @@ -0,0 +1,80 @@ +apiVersion: v1 +kind: Service +metadata: + name: lb-backend-sourceip + namespace: gateway-conformance-infra +spec: + selector: + app: lb-backend-sourceip + ports: + - protocol: TCP + port: 8080 + targetPort: 3000 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: lb-backend-sourceip + namespace: gateway-conformance-infra + labels: + app: lb-backend-sourceip +spec: + replicas: 4 + selector: + matchLabels: + app: lb-backend-sourceip + template: + metadata: + labels: + app: lb-backend-sourceip + spec: + containers: + - name: backend + image: gcr.io/k8s-staging-gateway-api/echo-basic:v20231214-v1.0.0-140-gf544a46e + imagePullPolicy: IfNotPresent + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: SERVICE_NAME + value: lb-backend-sourceip + resources: + requests: + cpu: 10m +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: BackendTrafficPolicy +metadata: + name: source-ip-lb-policy + namespace: gateway-conformance-infra +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: source-ip-lb-route + loadBalancer: + type: ConsistentHash + consistentHash: + type: SourceIP +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: source-ip-lb-route + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + rules: + - matches: + - path: + type: PathPrefix + value: /source + backendRefs: + - name: lb-backend-sourceip + port: 8080 diff --git a/test/e2e/testdata/load_balancing_round_robin.yaml b/test/e2e/testdata/load_balancing_round_robin.yaml new file mode 100644 index 00000000000..c50d170925a --- /dev/null +++ b/test/e2e/testdata/load_balancing_round_robin.yaml @@ -0,0 +1,78 @@ +apiVersion: v1 +kind: Service +metadata: + name: lb-backend-roundrobin + namespace: gateway-conformance-infra +spec: + selector: + app: lb-backend-roundrobin + ports: + - protocol: TCP + port: 8080 + targetPort: 3000 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: lb-backend-roundrobin + namespace: gateway-conformance-infra + labels: + app: lb-backend-roundrobin +spec: + replicas: 3 + selector: + matchLabels: + app: lb-backend-roundrobin + template: + metadata: + labels: + app: lb-backend-roundrobin + spec: + containers: + - name: backend + image: gcr.io/k8s-staging-gateway-api/echo-basic:v20231214-v1.0.0-140-gf544a46e + imagePullPolicy: IfNotPresent + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: SERVICE_NAME + value: lb-backend-roundrobin + resources: + requests: + cpu: 10m +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: BackendTrafficPolicy +metadata: + name: round-robin-lb-policy + namespace: gateway-conformance-infra +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: round-robin-lb-route + loadBalancer: + type: RoundRobin +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: round-robin-lb-route + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + rules: + - matches: + - path: + type: PathPrefix + value: /round + backendRefs: + - name: lb-backend-roundrobin + port: 8080 diff --git a/test/e2e/testdata/local-ratelimit.yaml b/test/e2e/testdata/local-ratelimit.yaml index e0e5513a03a..cf3ae5da061 100644 --- a/test/e2e/testdata/local-ratelimit.yaml +++ b/test/e2e/testdata/local-ratelimit.yaml @@ -4,11 +4,10 @@ metadata: name: ratelimit-specific-user namespace: gateway-conformance-infra spec: - targetRef: - group: gateway.networking.k8s.io + targetRefs: + - group: gateway.networking.k8s.io kind: HTTPRoute name: http-ratelimit-specific-user - namespace: gateway-conformance-infra rateLimit: type: Local local: @@ -30,11 +29,10 @@ metadata: name: ratelimit-all-traffic namespace: gateway-conformance-infra spec: - targetRef: - group: gateway.networking.k8s.io + targetRefs: + - group: gateway.networking.k8s.io kind: HTTPRoute name: http-ratelimit-all-traffic - namespace: gateway-conformance-infra rateLimit: type: Local local: diff --git a/test/e2e/testdata/oidc-securitypolicy.yaml b/test/e2e/testdata/oidc-securitypolicy.yaml index 0350979aa18..0c34ab0e92b 100644 --- a/test/e2e/testdata/oidc-securitypolicy.yaml +++ b/test/e2e/testdata/oidc-securitypolicy.yaml @@ -63,8 +63,8 @@ metadata: name: oidc-test namespace: gateway-conformance-infra spec: - targetRef: - group: gateway.networking.k8s.io + targetRefs: + - group: gateway.networking.k8s.io kind: HTTPRoute name: http-with-oidc oidc: diff --git a/test/e2e/testdata/preserve-case.yaml b/test/e2e/testdata/preserve-case.yaml index 53d521d7ea8..c815a19e332 100644 --- a/test/e2e/testdata/preserve-case.yaml +++ b/test/e2e/testdata/preserve-case.yaml @@ -18,11 +18,10 @@ metadata: name: preserve-case namespace: gateway-conformance-infra spec: - targetRef: - group: gateway.networking.k8s.io + targetRefs: + - group: gateway.networking.k8s.io kind: Gateway name: same-namespace - namespace: gateway-conformance-infra http1: preserveHeaderCase: true --- diff --git a/test/e2e/testdata/ratelimit-based-jwt-claims.yaml b/test/e2e/testdata/ratelimit-based-jwt-claims.yaml index d16e9c1ebdb..2d01996c981 100644 --- a/test/e2e/testdata/ratelimit-based-jwt-claims.yaml +++ b/test/e2e/testdata/ratelimit-based-jwt-claims.yaml @@ -4,11 +4,10 @@ metadata: name: jwt-example namespace: gateway-conformance-infra spec: - targetRef: - group: gateway.networking.k8s.io + targetRefs: + - group: gateway.networking.k8s.io kind: HTTPRoute name: http-ratelimit-based-jwt-claims - namespace: gateway-conformance-infra jwt: providers: - name: example @@ -24,11 +23,10 @@ metadata: name: ratelimit-specific-user namespace: gateway-conformance-infra spec: - targetRef: - group: gateway.networking.k8s.io + targetRefs: + - group: gateway.networking.k8s.io kind: HTTPRoute name: http-ratelimit-based-jwt-claims - namespace: gateway-conformance-infra rateLimit: type: Global global: diff --git a/test/e2e/testdata/ratelimit-cidr-match.yaml b/test/e2e/testdata/ratelimit-cidr-match.yaml index fdc112e6663..52054976270 100644 --- a/test/e2e/testdata/ratelimit-cidr-match.yaml +++ b/test/e2e/testdata/ratelimit-cidr-match.yaml @@ -4,11 +4,10 @@ metadata: name: ratelimit-all-ips namespace: gateway-conformance-infra spec: - targetRef: - group: gateway.networking.k8s.io - kind: HTTPRoute - name: cidr-ratelimit - namespace: gateway-conformance-infra + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: cidr-ratelimit rateLimit: type: Global global: diff --git a/test/e2e/testdata/ratelimit-header-match.yaml b/test/e2e/testdata/ratelimit-header-match.yaml index d38d66a5a79..d538471a86e 100644 --- a/test/e2e/testdata/ratelimit-header-match.yaml +++ b/test/e2e/testdata/ratelimit-header-match.yaml @@ -4,11 +4,10 @@ metadata: name: ratelimit-anded-headers namespace: gateway-conformance-infra spec: - targetRef: - group: gateway.networking.k8s.io - kind: HTTPRoute - name: header-ratelimit - namespace: gateway-conformance-infra + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: header-ratelimit rateLimit: type: Global global: diff --git a/test/e2e/testdata/ratelimit-headers-disabled.yaml b/test/e2e/testdata/ratelimit-headers-disabled.yaml index 6f1e4e44774..4f8c71385f3 100644 --- a/test/e2e/testdata/ratelimit-headers-disabled.yaml +++ b/test/e2e/testdata/ratelimit-headers-disabled.yaml @@ -6,10 +6,10 @@ metadata: spec: headers: disableRateLimitHeaders: true - targetRef: - group: gateway.networking.k8s.io - kind: Gateway - name: same-namespace + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: same-namespace --- apiVersion: gateway.envoyproxy.io/v1alpha1 kind: BackendTrafficPolicy @@ -17,11 +17,10 @@ metadata: name: ratelimit-headers-disabled-btp namespace: gateway-conformance-infra spec: - targetRef: - group: gateway.networking.k8s.io - kind: HTTPRoute - name: ratelimit-headers-disabled - namespace: gateway-conformance-infra + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: ratelimit-headers-disabled rateLimit: type: Global global: diff --git a/test/e2e/testdata/ratelimit-multiple-listeners.yaml b/test/e2e/testdata/ratelimit-multiple-listeners.yaml index 47aa3e46450..4ff8255dad2 100644 --- a/test/e2e/testdata/ratelimit-multiple-listeners.yaml +++ b/test/e2e/testdata/ratelimit-multiple-listeners.yaml @@ -36,11 +36,10 @@ metadata: name: ratelimit-all-ips namespace: gateway-conformance-infra spec: - targetRef: - group: gateway.networking.k8s.io - kind: HTTPRoute - name: cidr-ratelimit - namespace: gateway-conformance-infra + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: cidr-ratelimit rateLimit: type: Global global: diff --git a/test/e2e/testdata/retry.yaml b/test/e2e/testdata/retry.yaml index bacb78b1d60..5693c665e3b 100644 --- a/test/e2e/testdata/retry.yaml +++ b/test/e2e/testdata/retry.yaml @@ -4,11 +4,10 @@ metadata: name: retry-policy namespace: gateway-conformance-infra spec: - targetRef: - group: gateway.networking.k8s.io - kind: HTTPRoute - name: retry-route - namespace: gateway-conformance-infra + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: retry-route retry: numRetries: 5 perRetry: diff --git a/test/e2e/testdata/securitypolicy-translation-failed.yaml b/test/e2e/testdata/securitypolicy-translation-failed.yaml new file mode 100644 index 00000000000..01cdec37b17 --- /dev/null +++ b/test/e2e/testdata/securitypolicy-translation-failed.yaml @@ -0,0 +1,39 @@ +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: http-with-oidc + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + hostnames: ["www.example.com"] + rules: + - matches: + - path: + type: PathPrefix + value: /myapp # This is the path that will be protected by OIDC + backendRefs: + - name: infra-backend-v1 + port: 8080 +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: SecurityPolicy +metadata: + name: oidc-test + namespace: gateway-conformance-infra +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: http-with-oidc + oidc: + provider: + issuer: "http://keycloak.gateway-conformance-infra/realms/master" + authorizationEndpoint: "http://keycloak.gateway-conformance-infra/realms/master/protocol/openid-connect/auth" + tokenEndpoint: "http://keycloak.gateway-conformance-infra/realms/master/protocol/openid-connect/token" + clientID: "oidctest" + clientSecret: + name: "oidctest-secret" + redirectURL: "http://www.example.com/myapp/oauth2/callback" + logoutPath: "/myapp/logout" diff --git a/test/e2e/testdata/tcp-route.yaml b/test/e2e/testdata/tcp-route.yaml index 75e7d23c134..62b93547aa4 100644 --- a/test/e2e/testdata/tcp-route.yaml +++ b/test/e2e/testdata/tcp-route.yaml @@ -1,3 +1,24 @@ +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: Gateway +metadata: + name: my-tcp-gateway + namespace: gateway-conformance-infra +spec: + gatewayClassName: "{GATEWAY_CLASS_NAME}" + listeners: + - name: foo + protocol: TCP + port: 8080 + allowedRoutes: + kinds: + - kind: TCPRoute + - name: bar + protocol: TCP + port: 8090 + allowedRoutes: + kinds: + - kind: TCPRoute +--- apiVersion: gateway.networking.k8s.io/v1alpha2 kind: TCPRoute metadata: diff --git a/test/e2e/testdata/tracing-zipkin.yaml b/test/e2e/testdata/tracing-zipkin.yaml index 8cb2e10c1e6..1f7aaf0a66f 100644 --- a/test/e2e/testdata/tracing-zipkin.yaml +++ b/test/e2e/testdata/tracing-zipkin.yaml @@ -1,4 +1,68 @@ apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: eg-special-case + namespace: gateway-conformance-infra +spec: + gatewayClassName: "{GATEWAY_CLASS_NAME}" + listeners: + - name: http + port: 80 + protocol: HTTP + allowedRoutes: + namespaces: + from: All + infrastructure: + parametersRef: + group: gateway.envoyproxy.io + kind: EnvoyProxy + name: zipkin-tracing +--- +# This is a EnvoyProxy used for Zipkin tracing and will +# target to the eg-special-case Gateway +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyProxy +metadata: + name: zipkin-tracing + namespace: gateway-conformance-infra +spec: + logging: + level: + default: debug + telemetry: + tracing: + provider: + type: Zipkin + backendRefs: + - name: otel-collector + namespace: monitoring + port: 9411 + zipkin: + enable128BitTraceId: true + customTags: + "provider": + type: Literal + literal: + value: "zipkin" + "k8s.cluster.name": + type: Literal + literal: + value: "envoy-gateway" + "k8s.pod.name": + type: Environment + environment: + name: ENVOY_POD_NAME + defaultValue: "-" + "k8s.namespace.name": + type: Environment + environment: + name: ENVOY_GATEWAY_NAMESPACE + defaultValue: "envoy-gateway-system" + shutdown: + drainTimeout: 5s + minDrainDuration: 1s +--- +apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: tracing-zipkin diff --git a/test/e2e/testdata/udproute.yaml b/test/e2e/testdata/udproute.yaml index e5ea8d1244f..51abe821390 100644 --- a/test/e2e/testdata/udproute.yaml +++ b/test/e2e/testdata/udproute.yaml @@ -1,3 +1,89 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: gateway-conformance-udp + labels: + gateway-conformance: udp +--- +apiVersion: v1 +kind: Service +metadata: + name: coredns + namespace: gateway-conformance-udp + labels: + app: udp +spec: + ports: + - name: udp-dns + port: 53 + protocol: UDP + targetPort: 53 + selector: + app: udp +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: coredns + namespace: gateway-conformance-udp + labels: + app: udp +spec: + selector: + matchLabels: + app: udp + template: + metadata: + labels: + app: udp + spec: + containers: + - args: + - -conf + - /root/Corefile + image: coredns/coredns + name: coredns + volumeMounts: + - mountPath: /root + name: conf + volumes: + - configMap: + defaultMode: 420 + name: coredns + name: conf +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: coredns + namespace: gateway-conformance-udp +data: + Corefile: | + .:53 { + forward . 8.8.8.8 9.9.9.9 + log + errors + } + + foo.bar.com:53 { + whoami + } +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: Gateway +metadata: + name: udp-gateway + namespace: gateway-conformance-udp +spec: + gatewayClassName: "{GATEWAY_CLASS_NAME}" + listeners: + - name: coredns + protocol: UDP + port: 5300 + allowedRoutes: + kinds: + - kind: UDPRoute +--- apiVersion: gateway.networking.k8s.io/v1alpha2 kind: UDPRoute metadata: diff --git a/test/e2e/testdata/use-client-protocol.yaml b/test/e2e/testdata/use-client-protocol.yaml index c624da1bc17..cd63544f59c 100644 --- a/test/e2e/testdata/use-client-protocol.yaml +++ b/test/e2e/testdata/use-client-protocol.yaml @@ -4,11 +4,10 @@ metadata: name: use-client-protocol-btp namespace: gateway-conformance-infra spec: - targetRef: - group: gateway.networking.k8s.io + targetRefs: + - group: gateway.networking.k8s.io kind: HTTPRoute name: use-client-protocol - namespace: gateway-conformance-infra useClientProtocol: true --- apiVersion: gateway.networking.k8s.io/v1 diff --git a/test/e2e/testdata/wasm-http.yaml b/test/e2e/testdata/wasm-http.yaml index e684da26765..d251d5943e9 100644 --- a/test/e2e/testdata/wasm-http.yaml +++ b/test/e2e/testdata/wasm-http.yaml @@ -41,8 +41,8 @@ metadata: name: http-wasm-source-test namespace: gateway-conformance-infra spec: - targetRef: - group: gateway.networking.k8s.io + targetRefs: + - group: gateway.networking.k8s.io kind: HTTPRoute name: http-with-http-wasm-source wasm: diff --git a/test/e2e/tests/accesslog.go b/test/e2e/tests/accesslog.go index b0de4543cb7..2e1ee205b8c 100644 --- a/test/e2e/tests/accesslog.go +++ b/test/e2e/tests/accesslog.go @@ -18,6 +18,7 @@ import ( httputils "sigs.k8s.io/gateway-api/conformance/utils/http" "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" "sigs.k8s.io/gateway-api/conformance/utils/suite" + "sigs.k8s.io/gateway-api/conformance/utils/tlog" ) func init() { @@ -152,7 +153,22 @@ var ALSTest = suite.ConformanceTest{ routeNN := types.NamespacedName{Name: "accesslog-als", Namespace: ns} gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) - preCount := ALSLogCount(t, suite) + + preCount := 0 + // make sure ALS server metric endpoint is ready + if err := wait.PollUntilContextTimeout(context.TODO(), time.Second, time.Minute, true, + func(ctx context.Context) (bool, error) { + curCount, err := ALSLogCount(suite) + if err != nil { + tlog.Logf(t, "failed to get log count from loki: %v", err) + return false, nil + } + preCount = curCount + return true, nil + }); err != nil { + t.Errorf("failed to get log count from loki: %v", err) + } + expectedResponse := httputils.ExpectedResponse{ Request: httputils.Request{ Path: "/als", @@ -170,7 +186,11 @@ var ALSTest = suite.ConformanceTest{ if err := wait.PollUntilContextTimeout(context.TODO(), time.Second, time.Minute, true, func(ctx context.Context) (bool, error) { - curCount := ALSLogCount(t, suite) + curCount, err := ALSLogCount(suite) + if err != nil { + tlog.Logf(t, "failed to get log count from loki: %v", err) + return false, nil + } return preCount < curCount, nil }); err != nil { t.Errorf("failed to get log count from loki: %v", err) @@ -187,7 +207,7 @@ func runLogTest(t *testing.T, suite *suite.ConformanceTestSuite, gwAddr string, // query log count from loki preCount, err := QueryLogCountFromLoki(t, suite.Client, expectedLabels, expectedMatch) if err != nil { - t.Logf("failed to get log count from loki: %v", err) + tlog.Logf(t, "failed to get log count from loki: %v", err) return false, nil } @@ -198,7 +218,7 @@ func runLogTest(t *testing.T, suite *suite.ConformanceTestSuite, gwAddr string, if err := wait.PollUntilContextTimeout(ctx, 500*time.Millisecond, 15*time.Second, true, func(_ context.Context) (bool, error) { count, err := QueryLogCountFromLoki(t, suite.Client, expectedLabels, expectedMatch) if err != nil { - t.Logf("failed to get log count from loki: %v", err) + tlog.Logf(t, "failed to get log count from loki: %v", err) return false, nil } @@ -207,7 +227,7 @@ func runLogTest(t *testing.T, suite *suite.ConformanceTestSuite, gwAddr string, return true, nil } - t.Logf("preCount=%d, count=%d", preCount, count) + tlog.Logf(t, "preCount=%d, count=%d", preCount, count) return false, nil }); err != nil { return false, nil diff --git a/test/e2e/tests/authorization-client-ip.go b/test/e2e/tests/authorization_client_ip.go similarity index 70% rename from test/e2e/tests/authorization-client-ip.go rename to test/e2e/tests/authorization_client_ip.go index 594ae19483f..98c44705435 100644 --- a/test/e2e/tests/authorization-client-ip.go +++ b/test/e2e/tests/authorization_client_ip.go @@ -59,15 +59,7 @@ var AuthorizationClientIPTest = suite.ConformanceTest{ Namespace: ns, } - req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") - cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req) - if err != nil { - t.Errorf("failed to get expected response: %v", err) - } - - if err := http.CompareRequest(t, &req, cReq, cResp, expectedResponse); err != nil { - t.Errorf("failed to compare request and response: %v", err) - } + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) }) t.Run("first route-allowed IP", func(t *testing.T) { @@ -90,15 +82,7 @@ var AuthorizationClientIPTest = suite.ConformanceTest{ Namespace: ns, } - req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") - cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req) - if err != nil { - t.Errorf("failed to get expected response: %v", err) - } - - if err := http.CompareRequest(t, &req, cReq, cResp, expectedResponse); err != nil { - t.Errorf("failed to compare request and response: %v", err) - } + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) }) t.Run("first route-default action: allow", func(t *testing.T) { @@ -121,15 +105,7 @@ var AuthorizationClientIPTest = suite.ConformanceTest{ Namespace: ns, } - req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") - cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req) - if err != nil { - t.Errorf("failed to get expected response: %v", err) - } - - if err := http.CompareRequest(t, &req, cReq, cResp, expectedResponse); err != nil { - t.Errorf("failed to compare request and response: %v", err) - } + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) }) // Test the second route @@ -153,15 +129,7 @@ var AuthorizationClientIPTest = suite.ConformanceTest{ Namespace: ns, } - req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") - cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req) - if err != nil { - t.Errorf("failed to get expected response: %v", err) - } - - if err := http.CompareRequest(t, &req, cReq, cResp, expectedResponse); err != nil { - t.Errorf("failed to compare request and response: %v", err) - } + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) }) t.Run("second route-default action: deny", func(t *testing.T) { @@ -178,15 +146,7 @@ var AuthorizationClientIPTest = suite.ConformanceTest{ Namespace: ns, } - req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") - cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req) - if err != nil { - t.Errorf("failed to get expected response: %v", err) - } - - if err := http.CompareRequest(t, &req, cReq, cResp, expectedResponse); err != nil { - t.Errorf("failed to compare request and response: %v", err) - } + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) }) }, } diff --git a/test/e2e/tests/authorization-default-action.go b/test/e2e/tests/authorization_default_action.go similarity index 77% rename from test/e2e/tests/authorization-default-action.go rename to test/e2e/tests/authorization_default_action.go index 7cd649e3fec..820a0e09950 100644 --- a/test/e2e/tests/authorization-default-action.go +++ b/test/e2e/tests/authorization_default_action.go @@ -56,15 +56,7 @@ var AuthorizationDefaultActionTest = suite.ConformanceTest{ Namespace: ns, } - req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") - cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req) - if err != nil { - t.Errorf("failed to get expected response: %v", err) - } - - if err := http.CompareRequest(t, &req, cReq, cResp, expectedResponse); err != nil { - t.Errorf("failed to compare request and response: %v", err) - } + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) }) t.Run("Authorization with empty rules and Allow default action should allow all traffic", func(t *testing.T) { @@ -78,15 +70,7 @@ var AuthorizationDefaultActionTest = suite.ConformanceTest{ Namespace: ns, } - req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") - cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req) - if err != nil { - t.Errorf("failed to get expected response: %v", err) - } - - if err := http.CompareRequest(t, &req, cReq, cResp, expectedResponse); err != nil { - t.Errorf("failed to compare request and response: %v", err) - } + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) }) }, } diff --git a/test/e2e/tests/backend_health_check.go b/test/e2e/tests/backend_health_check.go index b8a384c692a..4936d372079 100644 --- a/test/e2e/tests/backend_health_check.go +++ b/test/e2e/tests/backend_health_check.go @@ -9,6 +9,7 @@ package tests import ( + "context" "fmt" "testing" "time" @@ -20,9 +21,10 @@ import ( "sigs.k8s.io/gateway-api/conformance/utils/http" "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" "sigs.k8s.io/gateway-api/conformance/utils/suite" + "sigs.k8s.io/gateway-api/conformance/utils/tlog" "github.com/envoyproxy/gateway/internal/gatewayapi" - "github.com/envoyproxy/gateway/test/e2e/utils/prometheus" + "github.com/envoyproxy/gateway/test/utils/prometheus" ) func init() { @@ -35,6 +37,7 @@ var BackendHealthCheckActiveHTTPTest = suite.ConformanceTest{ Manifests: []string{"testdata/backend-health-check-active-http.yaml"}, Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { t.Run("active http", func(t *testing.T) { + ctx := context.Background() ns := "gateway-conformance-infra" passRouteNN := types.NamespacedName{Name: "http-with-health-check-active-http-pass", Namespace: ns} failRouteNN := types.NamespacedName{Name: "http-with-health-check-active-http-fail", Namespace: ns} @@ -50,7 +53,7 @@ var BackendHealthCheckActiveHTTPTest = suite.ConformanceTest{ BackendTrafficPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "health-check-active-http-pass-btp", Namespace: ns}, suite.ControllerName, ancestorRef) BackendTrafficPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "health-check-active-http-fail-btp", Namespace: ns}, suite.ControllerName, ancestorRef) - promAddr, err := prometheus.Address(suite.Client, + promClient, err := prometheus.NewClient(suite.Client, types.NamespacedName{Name: "prometheus", Namespace: "monitoring"}, ) require.NoError(t, err) @@ -70,12 +73,12 @@ var BackendHealthCheckActiveHTTPTest = suite.ConformanceTest{ suite.TimeoutConfig.MaxTimeToConsistency, func(_ time.Duration) bool { // check membership_healthy stats from Prometheus - v, err := prometheus.QuerySum(promAddr, passPromQL) + v, err := promClient.QuerySum(ctx, passPromQL) if err != nil { // wait until Prometheus sync stats return false } - t.Logf("cluster pass health check: success stats query count: %v", v) + tlog.Logf(t, "cluster pass health check: success stats query count: %v", v) if v == 0 { t.Error("success is not the same as expected") @@ -93,12 +96,12 @@ var BackendHealthCheckActiveHTTPTest = suite.ConformanceTest{ suite.TimeoutConfig.MaxTimeToConsistency, func(_ time.Duration) bool { // check membership_healthy stats from Prometheus - v, err := prometheus.QuerySum(promAddr, failPromQL) + v, err := promClient.QuerySum(ctx, failPromQL) if err != nil { // wait until Prometheus sync stats return false } - t.Logf("cluster fail health check: failure stats query count: %v", v) + tlog.Logf(t, "cluster fail health check: failure stats query count: %v", v) if v == 0 { t.Error("failure is not same as expected") diff --git a/test/e2e/tests/basic_auth.go b/test/e2e/tests/basic_auth.go index 7f61067c1c8..20c2e1b6a50 100644 --- a/test/e2e/tests/basic_auth.go +++ b/test/e2e/tests/basic_auth.go @@ -58,15 +58,7 @@ var BasicAuthTest = suite.ConformanceTest{ Namespace: ns, } - req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") - cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req) - if err != nil { - t.Errorf("failed to get expected response: %v", err) - } - - if err := http.CompareRequest(t, &req, cReq, cResp, expectedResponse); err != nil { - t.Errorf("failed to compare request and response: %v", err) - } + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) }) t.Run("without Authorization header", func(t *testing.T) { @@ -94,15 +86,7 @@ var BasicAuthTest = suite.ConformanceTest{ Namespace: ns, } - req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") - cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req) - if err != nil { - t.Errorf("failed to get expected response: %v", err) - } - - if err := http.CompareRequest(t, &req, cReq, cResp, expectedResponse); err != nil { - t.Errorf("failed to compare request and response: %v", err) - } + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) }) t.Run("invalid username password", func(t *testing.T) { @@ -133,15 +117,7 @@ var BasicAuthTest = suite.ConformanceTest{ Namespace: ns, } - req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") - cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req) - if err != nil { - t.Errorf("failed to get expected response: %v", err) - } - - if err := http.CompareRequest(t, &req, cReq, cResp, expectedResponse); err != nil { - t.Errorf("failed to compare request and response: %v", err) - } + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) }) t.Run("per route configuration second route", func(t *testing.T) { @@ -172,15 +148,7 @@ var BasicAuthTest = suite.ConformanceTest{ Namespace: ns, } - req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") - cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req) - if err != nil { - t.Errorf("failed to get expected response: %v", err) - } - - if err := http.CompareRequest(t, &req, cReq, cResp, expectedResponse); err != nil { - t.Errorf("failed to compare request and response: %v", err) - } + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) }) // https://github.com/envoyproxy/gateway/issues/2507 @@ -209,15 +177,7 @@ var BasicAuthTest = suite.ConformanceTest{ Namespace: ns, } - req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") - cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req) - if err != nil { - t.Errorf("failed to get expected response: %v", err) - } - - if err := http.CompareRequest(t, &req, cReq, cResp, expectedResponse); err != nil { - t.Errorf("failed to compare request and response: %v", err) - } + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) }) }, } diff --git a/test/e2e/tests/client_mtls.go b/test/e2e/tests/client_mtls.go new file mode 100644 index 00000000000..bed591a1152 --- /dev/null +++ b/test/e2e/tests/client_mtls.go @@ -0,0 +1,117 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +//go:build e2e +// +build e2e + +package tests + +import ( + "context" + "fmt" + "testing" + "time" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/gateway-api/conformance/utils/http" + "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" + "sigs.k8s.io/gateway-api/conformance/utils/roundtripper" + "sigs.k8s.io/gateway-api/conformance/utils/suite" + "sigs.k8s.io/gateway-api/conformance/utils/tlog" +) + +func init() { + ConformanceTests = append(ConformanceTests, ClientMTLSTest) +} + +var ClientMTLSTest = suite.ConformanceTest{ + ShortName: "ClientMTLS", + Description: "Use Gateway with Client MTLS policy", + Manifests: []string{"testdata/client-mtls.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + t.Run("Use Client MTLS", func(t *testing.T) { + depNS := "envoy-gateway-system" + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "http-client-mtls", Namespace: ns} + gwNN := types.NamespacedName{Name: "client-mtls-gateway", Namespace: ns} + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + kubernetes.NamespacesMustBeReady(t, suite.Client, suite.TimeoutConfig, []string{depNS}) + certNN := types.NamespacedName{Name: "client-mtls-certificate", Namespace: ns} + + expected := http.ExpectedResponse{ + Request: http.Request{ + Host: "mtls.example.com", + Path: "/client-mtls", + }, + ExpectedRequest: &http.ExpectedRequest{ + Request: http.Request{ + Host: "mtls.example.com", + Path: "/client-mtls", + Headers: map[string]string{ + "X-Forwarded-Client-Cert": "Hash=ac77d86dd638969a0a39b4e0743370e860d1b70da58b1b08ce950417b6386a8b;Subject=\"CN=mtls.example.com,OU=Gateway,O=EnvoyProxy,L=SomeCity,ST=VA,C=US\"", + }, + }, + }, + Response: http.Response{ + StatusCode: 200, + }, + Namespace: ns, + } + + req := http.MakeRequest(t, &expected, gwAddr, "HTTPS", "https") + + // This test uses the same key/cert pair as both a client cert and server cert + // Both backend and client treat the self-signed cert as a trusted CA + cPem, keyPem, err := GetTLSSecret(suite.Client, certNN) + if err != nil { + t.Fatalf("unexpected error finding TLS secret: %v", err) + } + + WaitForConsistentMTLSResponse(t, suite.RoundTripper, req, expected, suite.TimeoutConfig.RequiredConsecutiveSuccesses, suite.TimeoutConfig.MaxTimeToConsistency, cPem, keyPem, "mtls.example.com") + }) + }, +} + +func WaitForConsistentMTLSResponse(t *testing.T, r roundtripper.RoundTripper, req roundtripper.Request, expected http.ExpectedResponse, threshold int, maxTimeToConsistency time.Duration, cPem, keyPem []byte, server string) { + http.AwaitConvergence(t, threshold, maxTimeToConsistency, func(elapsed time.Duration) bool { + req.KeyPem = keyPem + req.CertPem = cPem + req.Server = server + + cReq, cRes, err := r.CaptureRoundTrip(req) + if err != nil { + tlog.Logf(t, "Request failed, not ready yet: %v (after %v)", err.Error(), elapsed) + return false + } + + if err := http.CompareRequest(t, &req, cReq, cRes, expected); err != nil { + tlog.Logf(t, "Response expectation failed for request: %+v not ready yet: %v (after %v)", req, err, elapsed) + return false + } + + return true + }) + tlog.Logf(t, "Request passed") +} + +// GetTLSSecret fetches the named Secret and converts both cert and key to []byte +func GetTLSSecret(client client.Client, secretName types.NamespacedName) ([]byte, []byte, error) { + var cert, key []byte + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + secret := &corev1.Secret{} + err := client.Get(ctx, secretName, secret) + if err != nil { + return cert, key, fmt.Errorf("error fetching TLS Secret: %w", err) + } + cert = secret.Data["tls.crt"] + key = secret.Data["tls.key"] + + return cert, key, nil +} diff --git a/test/e2e/tests/client_timeout.go b/test/e2e/tests/client_timeout.go index 5ae5f00d106..9a11f7a706a 100644 --- a/test/e2e/tests/client_timeout.go +++ b/test/e2e/tests/client_timeout.go @@ -11,7 +11,6 @@ package tests import ( "net/http" "net/url" - "strings" "testing" "time" @@ -19,20 +18,13 @@ import ( httputils "sigs.k8s.io/gateway-api/conformance/utils/http" "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" "sigs.k8s.io/gateway-api/conformance/utils/suite" + "sigs.k8s.io/gateway-api/conformance/utils/tlog" ) func init() { ConformanceTests = append(ConformanceTests, ClientTimeoutTest) } -var largeSizeHeader = func() string { - var b strings.Builder - for i := 0; i < 5000; i++ { - b.WriteString("FakeHeaderValue") - } - return b.String() -} - var ClientTimeoutTest = suite.ConformanceTest{ ShortName: "ClientTimeout", Description: "Test that the ClientTrafficPolicy API implementation supports client timeout", @@ -48,10 +40,6 @@ var ClientTimeoutTest = suite.ConformanceTest{ req := &http.Request{ Method: "GET", URL: &url.URL{Scheme: "http", Host: gwAddr, Path: "/request-timeout"}, - Header: http.Header{ - // larger enough to trigger request timeout - "x-large-size-header": []string{largeSizeHeader()}, - }, } client := &http.Client{} @@ -66,11 +54,12 @@ var ClientTimeoutTest = suite.ConformanceTest{ } defer resp.Body.Close() - // return 408 instead of 400 when request timeout. - if http.StatusRequestTimeout == resp.StatusCode { + // return 504 instead of 400 when request timeout. + // https://github.com/envoyproxy/envoy/blob/56021dbfb10b53c6d08ed6fc811e1ff4c9ac41fd/source/common/http/utility.cc#L1409 + if http.StatusGatewayTimeout == resp.StatusCode { return true } else { - t.Logf("response status code: %d, (after %v) ", resp.StatusCode, elapsed) + tlog.Logf(t, "response status code: %d, (after %v) ", resp.StatusCode, elapsed) return false } }) diff --git a/test/e2e/tests/connection_limit.go b/test/e2e/tests/connection_limit.go index a6f334e832b..97594c97774 100644 --- a/test/e2e/tests/connection_limit.go +++ b/test/e2e/tests/connection_limit.go @@ -9,6 +9,7 @@ package tests import ( + "context" "fmt" "net" "testing" @@ -16,14 +17,16 @@ import ( "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" "sigs.k8s.io/gateway-api/conformance/utils/http" "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" "sigs.k8s.io/gateway-api/conformance/utils/suite" + "sigs.k8s.io/gateway-api/conformance/utils/tlog" "github.com/envoyproxy/gateway/internal/gatewayapi" - "github.com/envoyproxy/gateway/test/e2e/utils/prometheus" + "github.com/envoyproxy/gateway/test/utils/prometheus" ) func init() { @@ -35,6 +38,11 @@ var ConnectionLimitTest = suite.ConformanceTest{ Description: "Deny Requests over connection limit", Manifests: []string{"testdata/connection-limit.yaml"}, Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + ctx := context.Background() + + promClient, err := prometheus.NewClient(suite.Client, types.NamespacedName{Name: "prometheus", Namespace: "monitoring"}) + require.NoError(t, err) + t.Run("Close connections over limit", func(t *testing.T) { ns := "gateway-conformance-infra" routeNN := types.NamespacedName{Name: "http-with-connection-limit", Namespace: ns} @@ -49,15 +57,26 @@ var ConnectionLimitTest = suite.ConformanceTest{ } ClientTrafficPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "connection-limit-ctp", Namespace: ns}, suite.ControllerName, ancestorRef) - promAddr, err := prometheus.Address(suite.Client, - types.NamespacedName{Name: "prometheus", Namespace: "monitoring"}, - ) - require.NoError(t, err) - // we make the number of connections equal to the number of connectionLimit connections + 3 // avoid partial connection errors or interruptions - for i := 0; i < 6; i++ { + // Try to open a connection to the gateway, this will consume one connection + if err := wait.PollUntilContextTimeout(context.TODO(), time.Second, time.Minute, true, + func(_ context.Context) (done bool, err error) { + _, err = net.DialTimeout("tcp", gwAddr, 100*time.Millisecond) + if err != nil { + tlog.Logf(t, "failed to open connection: %v", err) + return false, nil + } + t.Log("opened connection 1") + return true, nil + }); err != nil { + t.Errorf("failed to open connections: %v", err) + } + + // Open the remaining 5 connections + for i := 1; i < 6; i++ { conn, err := net.Dial("tcp", gwAddr) + tlog.Logf(t, "opened connection %d", i+1) if err != nil { t.Errorf("failed to open connection: %v", err) } else { @@ -75,12 +94,12 @@ var ConnectionLimitTest = suite.ConformanceTest{ suite.TimeoutConfig.MaxTimeToConsistency, func(_ time.Duration) bool { // check connection_limit stats from Prometheus - v, err := prometheus.QuerySum(promAddr, promQL) + v, err := promClient.QuerySum(ctx, promQL) if err != nil { // wait until Prometheus sync stats return false } - t.Logf("connection_limit stats query count: %v", v) + tlog.Logf(t, "connection_limit stats query count: %v", v) // connection interruptions or other connection errors may occur // we just need to determine whether there is a connection limit stats diff --git a/test/e2e/tests/controlplane.go b/test/e2e/tests/controlplane.go index 7149c2546b6..56577c880ac 100644 --- a/test/e2e/tests/controlplane.go +++ b/test/e2e/tests/controlplane.go @@ -17,6 +17,7 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/gateway-api/conformance/utils/suite" + "sigs.k8s.io/gateway-api/conformance/utils/tlog" ) func init() { @@ -64,7 +65,7 @@ var ControlPlaneMetricTest = suite.ConformanceTest{ if err := wait.PollUntilContextTimeout(context.TODO(), time.Second, 2*time.Minute, true, func(_ context.Context) (done bool, err error) { if err := ScrapeMetrics(t, suite.Client, nn, 19001, "/metrics"); err != nil { - t.Logf("failed to get metric: %v", err) + tlog.Logf(t, "failed to get metric: %v", err) return false, nil } return true, nil diff --git a/test/e2e/tests/eg_upgrade.go b/test/e2e/tests/eg_upgrade.go index d4128102e73..5a394d134f1 100644 --- a/test/e2e/tests/eg_upgrade.go +++ b/test/e2e/tests/eg_upgrade.go @@ -9,43 +9,116 @@ package tests import ( - "net/url" + "bytes" + "context" + "fmt" "os" "testing" "time" "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/cli" - "helm.sh/helm/v3/pkg/registry" + "helm.sh/helm/v3/pkg/kube" + kerrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/cli-runtime/pkg/resource" + "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/gateway-api/conformance/utils/http" "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" "sigs.k8s.io/gateway-api/conformance/utils/suite" -) + "sigs.k8s.io/gateway-api/conformance/utils/tlog" + "sigs.k8s.io/gateway-api/pkg/consts" -func init() { - UpgradeTests = append(UpgradeTests, EGUpgradeTest) -} + "github.com/envoyproxy/gateway/internal/cmd/options" + "github.com/envoyproxy/gateway/internal/utils/helm" +) var EGUpgradeTest = suite.ConformanceTest{ ShortName: "EGUpgrade", Description: "Upgrading from the last eg version should not lead to failures", - Manifests: []string{"testdata/eg-upgrade.yaml"}, + Manifests: []string{}, Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { t.Run("Upgrade from an older eg release should succeed", func(t *testing.T) { + chartPath := "../../../charts/gateway-helm" relName := "eg" depNS := "envoy-gateway-system" lastVersionTag := os.Getenv("last_version_tag") if lastVersionTag == "" { - lastVersionTag = "v1.0.0" // Default version tag if not specified + lastVersionTag = "v1.0.2" // Default version tag if not specified + } + + // Uninstall the current version of EG + relNamespace := "envoy-gateway-system" + options.DefaultConfigFlags.Namespace = ptr.To(relNamespace) + + ht := helm.NewPackageTool() + if err := ht.Setup(); err != nil { + t.Errorf("failed to setup of packageTool: %v", err) + } + + // cleanup some resources to avoid finalizer deadlocks when deleting CRDs + t.Log("cleanup test data") + cleanUpResources(suite.Client, t) + + // Uninstall the version deployed for e2e test from the branch + // Make sure to remove all CRDs and CRs, as these may be incompatible with a latestVersion + t.Log("start uninstall envoy gateway resources") + if err := ht.RunUninstall(&helm.PackageOptions{ + ReleaseName: relName, + Wait: true, + Timeout: suite.TimeoutConfig.NamespacesMustBeReady, + }); err != nil { + t.Fatalf("failed to uninstall envoy-gateway: %v", err) + } + + err := deleteChartCRDsFromPath(depNS, chartPath, t, suite.TimeoutConfig.NamespacesMustBeReady) + if err != nil { + t.Fatalf("Failed to delete chart CRDs: %s", err.Error()) + } + + t.Log("success to uninstall envoy gateway resources") + + // Install latest version + if err := ht.RunInstall(&helm.PackageOptions{ + Version: lastVersionTag, + ReleaseName: relName, + ReleaseNamespace: relNamespace, + Wait: true, + Timeout: suite.TimeoutConfig.NamespacesMustBeReady, + }); err != nil { + t.Fatalf("failed to install envoy-gateway: %v", err) + } + + // Apply base and test manifests deleted during uninstall phase; Manifests in current branch must be compatible with the latestVersion + // Since we're applying the GWC now, we must set the controller name, as the suite cannot identify it from the GWC status + suite.ControllerName = "gateway.envoyproxy.io/gatewayclass-controller" + suite.Applier.ControllerName = "gateway.envoyproxy.io/gatewayclass-controller" + suite.Applier.GatewayClass = suite.GatewayClassName + suite.Applier.MustApplyWithCleanup(t, suite.Client, suite.TimeoutConfig, suite.BaseManifests, suite.Cleanup) + + for _, manifestLocation := range []string{"testdata/eg-upgrade.yaml"} { + tlog.Logf(t, "Applying %s", manifestLocation) + suite.Applier.MustApplyWithCleanup(t, suite.Client, suite.TimeoutConfig, manifestLocation, true) } + // wait for everything to startup + kubernetes.NamespacesMustBeReady(t, suite.Client, suite.TimeoutConfig, []string{depNS}) + + // verify latestVersion is working ns := "gateway-upgrade-infra" routeNN := types.NamespacedName{Name: "http-backend-eg-upgrade", Namespace: ns} gwNN := types.NamespacedName{Name: "ha-gateway", Namespace: ns} gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) - reqURL := url.URL{Scheme: "http", Host: http.CalculateHost(t, gwAddr, "http"), Path: "/eg-upgrade"} + kubernetes.NamespacesMustBeReady(t, suite.Client, suite.TimeoutConfig, []string{depNS}) expectOkResp := http.ExpectedResponse{ Request: http.Request{ @@ -64,32 +137,18 @@ var EGUpgradeTest = suite.ConformanceTest{ t.Errorf("failed to get expected response for the first three requests: %v", err) } - t.Log("Validate route to backend is functional", reqURL.String()) - - // Uninstall the current version of EG - err := helmUninstall(relName, depNS, t) - if err != nil { - t.Fatalf("Failed to upgrade the release: %s", err.Error()) - } - - t.Log("Install the last version tag") - err = helmInstall(relName, depNS, lastVersionTag, suite.TimeoutConfig.NamespacesMustBeReady, t) - if err != nil { - t.Fatalf("Failed to upgrade the release: %s", err.Error()) - } - - // wait for everything to startup - kubernetes.NamespacesMustBeReady(t, suite.Client, suite.TimeoutConfig, []string{depNS}) - + // Perform helm upgrade of EG t.Log("Attempting to upgrade to current version of eg deployment") - err = helmUpgradeChartFromPath(relName, depNS, "../../../charts/gateway-helm", suite.TimeoutConfig.NamespacesMustBeReady, t) + // TODO: when helm tool supports upgrade-from-source action, use it + err = upgradeChartFromPath(relName, depNS, chartPath, suite.TimeoutConfig.NamespacesMustBeReady, t) if err != nil { t.Fatalf("Failed to upgrade the release: %s", err.Error()) } + // wait for installation kubernetes.NamespacesMustBeReady(t, suite.Client, suite.TimeoutConfig, []string{depNS}) - t.Log("Confirm routing works after upgrade the eg with current main version") + t.Log("Confirm routing works after upgrading Envoy Gateway with current main version") http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectOkResp) // fire the rest of requests if err := GotExactExpectedResponse(t, 5, suite.RoundTripper, expectOkReq, expectOkResp); err != nil { @@ -99,7 +158,52 @@ var EGUpgradeTest = suite.ConformanceTest{ }, } -func helmUpgradeChartFromPath(relName, relNamespace, chartPath string, timeout time.Duration, t *testing.T) error { +func cleanUpResources(c client.Client, t *testing.T) { + gvks := []schema.GroupVersionKind{ + {Group: "gateway.networking.k8s.io", Version: "v1", Kind: "HTTPRoute"}, + {Group: "gateway.networking.k8s.io", Version: "v1", Kind: "Gateway"}, + {Group: "gateway.networking.k8s.io", Version: "v1", Kind: "GatewayClass"}, + } + for _, gvk := range gvks { + obj := &unstructured.Unstructured{} + obj.SetGroupVersionKind(gvk) + + list := &unstructured.UnstructuredList{} + list.SetGroupVersionKind(gvk) + + if err := c.List(context.Background(), list); err != nil { + t.Fatalf("failed fetching %s: %v", obj.GetObjectKind(), err) + } + + for _, o := range list.Items { + tlog.Logf(t, "deleting %s: %s/%s", o.GetObjectKind(), o.GetNamespace(), o.GetName()) + if err := c.Delete(context.Background(), &o); err != nil { + if !kerrors.IsNotFound(err) { + t.Fatalf("error deleting %s: %s/%s : %v", o.GetObjectKind(), o.GetNamespace(), o.GetName(), err) + } + } + } + + if err := wait.PollUntilContextTimeout(context.TODO(), time.Second, time.Minute, true, + func(ctx context.Context) (bool, error) { + err := c.List(ctx, list) + if err != nil { + return false, nil + } + + if len(list.Items) > 0 { + tlog.Logf(t, "Waiting for deletion of %d %s", len(list.Items), gvk.String()) + return false, nil + } + + return true, nil + }); err != nil { + t.Fatalf("failed to wait for %s deletion: %v", gvk.String(), err) + } + } +} + +func upgradeChartFromPath(relName, relNamespace, chartPath string, timeout time.Duration, t *testing.T) error { actionConfig := new(action.Configuration) if err := actionConfig.Init(cli.New().RESTClientGetter(), relNamespace, "secret", t.Logf); err != nil { return err @@ -112,73 +216,157 @@ func helmUpgradeChartFromPath(relName, relNamespace, chartPath string, timeout t upgrade.Timeout = timeout // Load the chart from a local directory. - chart, err := loader.Load(chartPath) + gatewayChart, err := loader.Load(chartPath) if err != nil { return err } - // Run the installation. - values := map[string]interface{}{ - "deployment": map[string]interface{}{ - "envoyGateway": map[string]interface{}{ - "imagePullPolicy": "IfNotPresent", - }, - }, + err = migrateChartCRDs(actionConfig, gatewayChart, timeout) + if err != nil { + return err + } + + err = updateChartCRDs(actionConfig, gatewayChart, timeout) + if err != nil { + return err } - _, err = upgrade.Run(relName, chart, values) + + _, err = upgrade.Run(relName, gatewayChart, map[string]interface{}{}) if err != nil { return err } return nil } -func helmInstall(relName, relNamespace string, tag string, timeout time.Duration, t *testing.T) error { - actionConfig := new(action.Configuration) - if err := actionConfig.Init(cli.New().RESTClientGetter(), relNamespace, "secret", t.Logf); err != nil { +func updateChartCRDs(actionConfig *action.Configuration, gatewayChart *chart.Chart, timeout time.Duration) error { + crds, err := extractCRDs(actionConfig, gatewayChart) + if err != nil { return err } - // Set installation options. - install := action.NewInstall(actionConfig) - install.ReleaseName = relName - install.Namespace = relNamespace - install.CreateNamespace = true - install.Version = tag - install.WaitForJobs = true - install.Timeout = timeout - - registryClient, err := registry.NewClient() + // Create all CRDs in the envoy gateway chart + result, err := actionConfig.KubeClient.Update(crds, crds, false) if err != nil { - return err + return fmt.Errorf("failed to create CRDs: %w", err) } - install.SetRegistryClient(registryClient) - // todo we need to explicitly reinstall the CRDs - chartPath, err := install.LocateChart("oci://docker.io/envoyproxy/gateway-helm", cli.New()) + + // We invalidate the cache and let it rebuild the cache, + // at which point no more updated CRDs exist + client, err := actionConfig.RESTClientGetter.ToDiscoveryClient() if err != nil { return err } - // Load the chart from a local directory. - chart, err := loader.Load(chartPath) - if err != nil { + client.Invalidate() + + // Wait the specified amount of time for the resource to be recognized by the cluster + if err := actionConfig.KubeClient.Wait(result.Created, timeout); err != nil { return err } - // Run the installation. - _, err = install.Run(chart, nil) + _, err = client.ServerGroups() + return err +} + +// TODO: proper migration framework required +func migrateChartCRDs(actionConfig *action.Configuration, gatewayChart *chart.Chart, timeout time.Duration) error { + crds, err := extractCRDs(actionConfig, gatewayChart) if err != nil { return err } + + for _, crd := range crds { + if crd.Name == "backendtlspolicies.gateway.networking.k8s.io" { + newVersion, err := getGWAPIVersion(crd.Object) + if err != nil { + return err + } + // https://gateway-api.sigs.k8s.io/guides/?h=upgrade#v11-upgrade-notes + if newVersion == "v1.1.0" { + helper := resource.NewHelper(crd.Client, crd.Mapping) + existingCRD, err := helper.Get(crd.Namespace, crd.Name) + if kerrors.IsNotFound(err) { + continue + } + + // previous version exists + existingVersion, err := getGWAPIVersion(existingCRD) + if err != nil { + return err + } + + if existingVersion == "v1.0.0" { + // Delete the existing instance of the BTLS CRD + _, errs := actionConfig.KubeClient.Delete([]*resource.Info{crd}) + if errs != nil { + return fmt.Errorf("failed to delete backendtlspolicies: %s", util.MultipleErrors("", errs)) + } + + if kubeClient, ok := actionConfig.KubeClient.(kube.InterfaceExt); ok { + if err := kubeClient.WaitForDelete([]*resource.Info{crd}, timeout); err != nil { + return fmt.Errorf("failed to wait for backendtlspolicies deletion: %s", err.Error()) + } + } + } + } + } + } + return nil } -func helmUninstall(relName, relNamespace string, t *testing.T) error { +func deleteChartCRDsFromPath(relNamespace, chartPath string, t *testing.T, timeout time.Duration) error { actionConfig := new(action.Configuration) if err := actionConfig.Init(cli.New().RESTClientGetter(), relNamespace, "secret", t.Logf); err != nil { return err } - uninstall := action.NewUninstall(actionConfig) - _, err := uninstall.Run(relName) // nil can be replaced with any values you wish to override + + // Load the chart from a local directory. + gatewayChart, err := loader.Load(chartPath) + if err != nil { + return err + } + + crds, err := extractCRDs(actionConfig, gatewayChart) if err != nil { return err } + + if _, errors := actionConfig.KubeClient.Delete(crds); len(errors) != 0 { + return fmt.Errorf("failed to delete CRDs error: %s", util.MultipleErrors("", errors)) + } + + if kubeClient, ok := actionConfig.KubeClient.(kube.InterfaceExt); ok { + if err := kubeClient.WaitForDelete(crds, timeout); err != nil { + return fmt.Errorf("failed to wait for crds deletion: %s", err.Error()) + } + } + return nil } + +func getGWAPIVersion(object runtime.Object) (string, error) { + accessor, err := meta.Accessor(object) + if err != nil { + return "", err + } + annotations := accessor.GetAnnotations() + newVersion, ok := annotations[consts.BundleVersionAnnotation] + if ok { + return newVersion, nil + } + return "", fmt.Errorf("failed to determine Gateway API CRD version") +} + +// extractCRDs Extract the CRDs part of the chart +func extractCRDs(config *action.Configuration, ch *chart.Chart) ([]*resource.Info, error) { + crdResInfo := make([]*resource.Info, 0, len(ch.CRDObjects())) + + for _, crd := range ch.CRDObjects() { + resInfo, err := config.KubeClient.Build(bytes.NewBufferString(string(crd.File.Data)), false) + if err != nil { + return nil, err + } + crdResInfo = append(crdResInfo, resInfo...) + } + + return crdResInfo, nil +} diff --git a/test/e2e/tests/envoy_shutdown.go b/test/e2e/tests/envoy_shutdown.go index 7634a29ae94..6b5a35f490a 100644 --- a/test/e2e/tests/envoy_shutdown.go +++ b/test/e2e/tests/envoy_shutdown.go @@ -34,10 +34,6 @@ import ( "github.com/envoyproxy/gateway/internal/infrastructure/kubernetes/proxy" ) -func init() { - UpgradeTests = append(UpgradeTests, EnvoyShutdownTest) -} - var EnvoyShutdownTest = suite.ConformanceTest{ ShortName: "EnvoyShutdown", Description: "Deleting envoy pod should not lead to failures", diff --git a/test/e2e/tests/ext_auth_grpc_service.go b/test/e2e/tests/ext_auth_grpc_service.go index 439ba8596ac..0618dc3a387 100644 --- a/test/e2e/tests/ext_auth_grpc_service.go +++ b/test/e2e/tests/ext_auth_grpc_service.go @@ -133,10 +133,8 @@ var GRPCExtAuthTest = suite.ConformanceTest{ } SecurityPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "ext-auth-test", Namespace: ns}, suite.ControllerName, ancestorRef) - podReady := corev1.PodCondition{Type: corev1.PodReady, Status: corev1.ConditionTrue} - // Wait for the grpc ext auth service pod to be ready - WaitForPods(t, suite.Client, ns, map[string]string{"app": "grpc-ext-auth"}, corev1.PodRunning, podReady) + WaitForPods(t, suite.Client, ns, map[string]string{"app": "grpc-ext-auth"}, corev1.PodRunning, PodReady) expectedResponse := http.ExpectedResponse{ Request: http.Request{ diff --git a/test/e2e/tests/httproute-rewrite-full-path.go b/test/e2e/tests/httproute_rewrite_full_path.go similarity index 100% rename from test/e2e/tests/httproute-rewrite-full-path.go rename to test/e2e/tests/httproute_rewrite_full_path.go diff --git a/test/e2e/tests/load_balancing.go b/test/e2e/tests/load_balancing.go new file mode 100644 index 00000000000..346a25965aa --- /dev/null +++ b/test/e2e/tests/load_balancing.go @@ -0,0 +1,360 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +//go:build e2e +// +build e2e + +package tests + +import ( + "context" + "encoding/json" + "fmt" + "io" + nethttp "net/http" + "net/http/cookiejar" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" + gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + "sigs.k8s.io/gateway-api/conformance/utils/http" + "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" + "sigs.k8s.io/gateway-api/conformance/utils/roundtripper" + "sigs.k8s.io/gateway-api/conformance/utils/suite" + "sigs.k8s.io/gateway-api/conformance/utils/tlog" + + "github.com/envoyproxy/gateway/internal/gatewayapi" +) + +func init() { + ConformanceTests = append(ConformanceTests, + RoundRobinLoadBalancingTest, + ConsistentHashSourceIPLoadBalancingTest, + ConsistentHashHeaderLoadBalancingTest, + ConsistentHashCookieLoadBalancingTest, + ) +} + +var RoundRobinLoadBalancingTest = suite.ConformanceTest{ + ShortName: "RoundRobinLoadBalancing", + Description: "Test for round robin load balancing type", + Manifests: []string{"testdata/load_balancing_round_robin.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + const ( + sendRequests = 100 + replicas = 3 + offset = 5 + ) + + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "round-robin-lb-route", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + + ancestorRef := gwapiv1a2.ParentReference{ + Group: gatewayapi.GroupPtr(gwapiv1.GroupName), + Kind: gatewayapi.KindPtr(gatewayapi.KindGateway), + Namespace: gatewayapi.NamespacePtr(gwNN.Namespace), + Name: gwapiv1.ObjectName(gwNN.Name), + } + BackendTrafficPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "round-robin-lb-policy", Namespace: ns}, suite.ControllerName, ancestorRef) + WaitForPods(t, suite.Client, ns, map[string]string{"app": "lb-backend-roundrobin"}, corev1.PodRunning, PodReady) + + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + + t.Run("traffic should be split evenly", func(t *testing.T) { + expectedResponse := http.ExpectedResponse{ + Request: http.Request{ + Path: "/round", + }, + Response: http.Response{ + StatusCode: 200, + }, + Namespace: ns, + } + req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") + + compareFunc := func(trafficMap map[string]int) bool { + even := sendRequests / replicas + for _, count := range trafficMap { + if !AlmostEquals(count, even, offset) { + return false + } + } + + return true + } + + if err := wait.PollUntilContextTimeout(context.TODO(), time.Second, 30*time.Second, true, func(_ context.Context) (bool, error) { + return runTrafficTest(t, suite, req, expectedResponse, sendRequests, compareFunc), nil + }); err != nil { + tlog.Errorf(t, "failed to run round robin load balancing test: %v", err) + } + }) + }, +} + +type TrafficCompareFunc func(trafficMap map[string]int) bool + +func runTrafficTest(t *testing.T, suite *suite.ConformanceTestSuite, + req roundtripper.Request, expectedResponse http.ExpectedResponse, + totalRequestCount int, compareFunc TrafficCompareFunc, +) bool { + trafficMap := make(map[string]int) + for i := 0; i < totalRequestCount; i++ { + cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req) + if err != nil { + t.Errorf("failed to get expected response: %v", err) + } + + if err := http.CompareRequest(t, &req, cReq, cResp, expectedResponse); err != nil { + t.Errorf("failed to compare request and response: %v", err) + } + + podName := cReq.Pod + if len(podName) == 0 { + // it shouldn't be missing here + tlog.Errorf(t, "failed to get pod header in response: %v", err) + } else { + trafficMap[podName]++ + } + } + + ret := compareFunc(trafficMap) + if !ret { + tlog.Logf(t, "traffic map: %v", trafficMap) + } + + return ret +} + +var ConsistentHashSourceIPLoadBalancingTest = suite.ConformanceTest{ + ShortName: "SourceIPBasedConsistentHashLoadBalancing", + Description: "Test for source IP based consistent hash load balancing type", + Manifests: []string{"testdata/load_balancing_consistent_hash_source_ip.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + const sendRequests = 10 + + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "source-ip-lb-route", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + + ancestorRef := gwapiv1a2.ParentReference{ + Group: gatewayapi.GroupPtr(gwapiv1.GroupName), + Kind: gatewayapi.KindPtr(gatewayapi.KindGateway), + Namespace: gatewayapi.NamespacePtr(gwNN.Namespace), + Name: gwapiv1.ObjectName(gwNN.Name), + } + BackendTrafficPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "source-ip-lb-policy", Namespace: ns}, suite.ControllerName, ancestorRef) + WaitForPods(t, suite.Client, ns, map[string]string{"app": "lb-backend-sourceip"}, corev1.PodRunning, PodReady) + + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + + t.Run("all traffics route to the same backend with same source ip", func(t *testing.T) { + expectedResponse := http.ExpectedResponse{ + Request: http.Request{ + Path: "/source", + }, + Response: http.Response{ + StatusCode: 200, + }, + Namespace: ns, + } + req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") + + compareFunc := func(trafficMap map[string]int) bool { + // All traffic should be routed to the same pod. + return len(trafficMap) == 1 + } + + if err := wait.PollUntilContextTimeout(context.TODO(), time.Second, 30*time.Second, true, func(_ context.Context) (bool, error) { + return runTrafficTest(t, suite, req, expectedResponse, sendRequests, compareFunc), nil + }); err != nil { + tlog.Errorf(t, "failed to run source ip based consistent hash load balancing test: %v", err) + } + }) + }, +} + +var ConsistentHashHeaderLoadBalancingTest = suite.ConformanceTest{ + ShortName: "HeaderBasedConsistentHashLoadBalancing", + Description: "Test for header based consistent hash load balancing type", + Manifests: []string{"testdata/load_balancing_consistent_hash_header.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + const sendRequests = 10 + + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "header-lb-route", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + + ancestorRef := gwapiv1a2.ParentReference{ + Group: gatewayapi.GroupPtr(gwapiv1.GroupName), + Kind: gatewayapi.KindPtr(gatewayapi.KindGateway), + Namespace: gatewayapi.NamespacePtr(gwNN.Namespace), + Name: gwapiv1.ObjectName(gwNN.Name), + } + BackendTrafficPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "header-lb-policy", Namespace: ns}, suite.ControllerName, ancestorRef) + WaitForPods(t, suite.Client, ns, map[string]string{"app": "lb-backend-header"}, corev1.PodRunning, PodReady) + + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + + expectedResponse := http.ExpectedResponse{ + Request: http.Request{ + Path: "/header", + }, + Response: http.Response{ + StatusCode: 200, + }, + Namespace: ns, + } + headers := []string{"0.0.0.0", "1.2.3.4", "4.5.6.7", "7.8.9.10", "10.11.12.13"} + + for _, header := range headers { + t.Run(header, func(t *testing.T) { + req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") + req.Headers["Lb-Test-Header"] = []string{header} + got := runTrafficTest(t, suite, req, expectedResponse, sendRequests, func(trafficMap map[string]int) bool { + // All traffic should be routed to the same pod. + return len(trafficMap) == 1 + }) + require.True(t, got) + }) + } + }, +} + +var ConsistentHashCookieLoadBalancingTest = suite.ConformanceTest{ + ShortName: "CookieBasedConsistentHashLoadBalancing", + Description: "Test for cookie based consistent hash load balancing type", + Manifests: []string{"testdata/load_balancing_consistent_hash_cookie.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + const sendRequests = 10 + + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "cookie-lb-route", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + + ancestorRef := gwapiv1a2.ParentReference{ + Group: gatewayapi.GroupPtr(gwapiv1.GroupName), + Kind: gatewayapi.KindPtr(gatewayapi.KindGateway), + Namespace: gatewayapi.NamespacePtr(gwNN.Namespace), + Name: gwapiv1.ObjectName(gwNN.Name), + } + BackendTrafficPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "cookie-lb-policy", Namespace: ns}, suite.ControllerName, ancestorRef) + WaitForPods(t, suite.Client, ns, map[string]string{"app": "lb-backend-cookie"}, corev1.PodRunning, PodReady) + + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + + t.Run("all traffics route to the same backend with same test cookie", func(t *testing.T) { + cookieJar, err := cookiejar.New(nil) + require.NoError(t, err) + + // Making request on our own since the gateway-api conformance suite does not support + // setting cookies for one request. + client := &nethttp.Client{ + Jar: cookieJar, + Transport: nethttp.DefaultTransport, + } + req, err := nethttp.NewRequest(nethttp.MethodGet, fmt.Sprintf("http://%s/cookie", gwAddr), nil) + require.NoError(t, err) + + cookieValues := []string{"abc", "def", "ghi", "jkl", "mno"} + for _, cookieValue := range cookieValues { + // Same test cookie will always hit the same endpoint. + var expectPodName string + + client.Jar.SetCookies(req.URL, []*nethttp.Cookie{ + { + Name: "Lb-Test-Cookie", + Value: cookieValue, + }, + }) + + for i := 0; i < sendRequests; i++ { + resp, err := client.Do(req) + if err != nil { + t.Errorf("failed to get response: %v", err) + } + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, nethttp.StatusOK, resp.StatusCode) + + // Parse response body. + cReq := &roundtripper.CapturedRequest{} + if resp.Header.Get("Content-Type") == "application/json" { + err = json.Unmarshal(body, cReq) + require.NoError(t, err) + } else { + t.Fatalf("unsupported response content type") + } + + podName := cReq.Pod + if len(podName) == 0 { + // it shouldn't be missing here + t.Errorf("failed to get pod header in response: %v", err) + } else { + if len(expectPodName) == 0 { + expectPodName = podName + } else { + require.Equal(t, expectPodName, podName) + } + } + + require.NoError(t, resp.Body.Close()) + } + } + }) + + t.Run("a cookie will be generated if the require cookie does not exist", func(t *testing.T) { + cookieJar, err := cookiejar.New(nil) + require.NoError(t, err) + + // Making request on our own since the gateway-api conformance suite does not support + // setting cookies for one request. + client := &nethttp.Client{ + Jar: cookieJar, + Transport: nethttp.DefaultTransport, + } + req, err := nethttp.NewRequest(nethttp.MethodGet, fmt.Sprintf("http://%s/cookie", gwAddr), nil) + require.NoError(t, err) + + // A not desired cookie has been set. + client.Jar.SetCookies(req.URL, []*nethttp.Cookie{ + { + Name: "foo", + Value: "bar", + }, + }) + + waitErr := wait.PollUntilContextTimeout(context.Background(), 1*time.Second, 60*time.Second, true, func(ctx context.Context) (bool, error) { + resp, err := client.Do(req) + if err != nil { + t.Errorf("failed to get response: %v", err) + return false, err + } + + if resp.StatusCode != nethttp.StatusOK { + return false, nil + } + + if h := resp.Header.Get("set-cookie"); len(h) > 0 && + strings.Contains(h, "Lb-Test-Cookie") && + strings.Contains(h, "Max-Age=60; SameSite=Strict") { + return true, nil + } + + tlog.Logf(t, "Cookie have not been generated yet") + return false, nil + }) + require.NoError(t, waitErr) + }) + }, +} diff --git a/test/e2e/tests/metric.go b/test/e2e/tests/metric.go index fff4809edda..b9814f41e56 100644 --- a/test/e2e/tests/metric.go +++ b/test/e2e/tests/metric.go @@ -18,6 +18,7 @@ import ( httputils "sigs.k8s.io/gateway-api/conformance/utils/http" "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" "sigs.k8s.io/gateway-api/conformance/utils/suite" + "sigs.k8s.io/gateway-api/conformance/utils/tlog" ) func init() { @@ -54,7 +55,7 @@ var MetricTest = suite.ConformanceTest{ Namespace: "envoy-gateway-system", Name: "same-namespace-gw-metrics", }, 19001, "/stats/prometheus"); err != nil { - t.Logf("failed to get metric: %v", err) + tlog.Logf(t, "failed to get metric: %v", err) return false, nil } return true, nil @@ -88,7 +89,7 @@ var MetricTest = suite.ConformanceTest{ Namespace: "monitoring", Name: "otel-collecot-prometheus", }, 19001, "/metrics"); err != nil { - t.Logf("failed to get metric: %v", err) + tlog.Logf(t, "failed to get metric: %v", err) return false, nil } return true, nil diff --git a/test/e2e/tests/multiple-gc.go b/test/e2e/tests/multiple_gc.go similarity index 100% rename from test/e2e/tests/multiple-gc.go rename to test/e2e/tests/multiple_gc.go diff --git a/test/e2e/tests/oidc.go b/test/e2e/tests/oidc.go index f279d2eddcb..2bc40d3eba9 100644 --- a/test/e2e/tests/oidc.go +++ b/test/e2e/tests/oidc.go @@ -9,19 +9,23 @@ package tests import ( + "context" "io" "net/http" "regexp" "testing" + "time" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" gwhttp "sigs.k8s.io/gateway-api/conformance/utils/http" "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" "sigs.k8s.io/gateway-api/conformance/utils/suite" + "sigs.k8s.io/gateway-api/conformance/utils/tlog" "github.com/envoyproxy/gateway/internal/gatewayapi" ) @@ -46,6 +50,10 @@ var OIDCTest = suite.ConformanceTest{ Manifests: []string{"testdata/oidc-keycloak.yaml", "testdata/oidc-securitypolicy.yaml"}, Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { t.Run("http route with oidc authentication", func(t *testing.T) { + // Add a function to dump current cluster status + t.Cleanup(func() { + CollectAndDump(t, suite.RestConfig) + }) ns := "gateway-conformance-infra" routeNN := types.NamespacedName{Name: "http-with-oidc", Namespace: ns} gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} @@ -75,18 +83,31 @@ var OIDCTest = suite.ConformanceTest{ ) require.NoError(t, err) - // Send a request to the http route with OIDC configured. - // It will be redirected to the keycloak login page - res, err := client.Get(testURL, true) - require.NoError(t, err) - require.Equal(t, 200, res.StatusCode, "Expected 200 OK") - - // Parse the response body to get the URL where the login page would post the user-entered credentials - require.NoError(t, client.ParseLoginForm(res.Body, keyCloakLoginFormID), "Failed to parse login form") + if err := wait.PollUntilContextTimeout(context.TODO(), time.Second, 5*time.Minute, true, + func(_ context.Context) (done bool, err error) { + tlog.Logf(t, "sending request to %s", testURL) + + // Send a request to the http route with OIDC configured. + // It will be redirected to the keycloak login page + res, err := client.Get(testURL, true) + require.NoError(t, err, "Failed to get the login page") + require.Equal(t, 200, res.StatusCode, "Expected 200 OK") + + // Parse the response body to get the URL where the login page would post the user-entered credentials + if err := client.ParseLoginForm(res.Body, keyCloakLoginFormID); err != nil { + tlog.Logf(t, "failed to parse login form: %v", err) + return false, nil + } + + t.Log("successfully parsed login form") + return true, nil + }); err != nil { + t.Errorf("failed to parse login form: %v", err) + } // Submit the login form to the IdP. // This will authenticate and redirect back to the application - res, err = client.Login(map[string]string{"username": username, "password": password, "credentialId": ""}) + res, err := client.Login(map[string]string{"username": username, "password": password, "credentialId": ""}) require.NoError(t, err, "Failed to login to the IdP") // Verify that we get the expected response from the application diff --git a/test/e2e/tests/oidc_testclient.go b/test/e2e/tests/oidc_testclient.go index d2bd9f364ce..2f1cc4d5983 100644 --- a/test/e2e/tests/oidc_testclient.go +++ b/test/e2e/tests/oidc_testclient.go @@ -212,7 +212,7 @@ func extractFromData(responseBody string, match formMatch, includeFromInputs boo // Find the form with the specified ID or match criteria form := findForm(doc, match) if form == nil { - return "", "", nil, fmt.Errorf("%s not found", match) + return "", "", nil, fmt.Errorf("%s not found in %s", match, responseBody) } var ( diff --git a/test/e2e/tests/retry.go b/test/e2e/tests/retry.go index c067df3752d..ca18d09c014 100644 --- a/test/e2e/tests/retry.go +++ b/test/e2e/tests/retry.go @@ -9,6 +9,7 @@ package tests import ( + "context" "fmt" "testing" "time" @@ -18,8 +19,9 @@ import ( "sigs.k8s.io/gateway-api/conformance/utils/http" "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" "sigs.k8s.io/gateway-api/conformance/utils/suite" + "sigs.k8s.io/gateway-api/conformance/utils/tlog" - "github.com/envoyproxy/gateway/test/e2e/utils/prometheus" + "github.com/envoyproxy/gateway/test/utils/prometheus" ) func init() { @@ -31,6 +33,11 @@ var RetryTest = suite.ConformanceTest{ Description: "Test that the BackendTrafficPolicy API implementation supports retry", Manifests: []string{"testdata/retry.yaml"}, Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + ctx := context.Background() + + promClient, err := prometheus.NewClient(suite.Client, types.NamespacedName{Name: "prometheus", Namespace: "monitoring"}) + require.NoError(t, err) + t.Run("retry-on-500", func(t *testing.T) { ns := "gateway-conformance-infra" routeNN := types.NamespacedName{Name: "retry-route", Namespace: ns} @@ -47,16 +54,14 @@ var RetryTest = suite.ConformanceTest{ Namespace: ns, } - promAddr, err := prometheus.Address(suite.Client, types.NamespacedName{Name: "prometheus", Namespace: "monitoring"}) - require.NoError(t, err) promQL := fmt.Sprintf(`envoy_cluster_upstream_rq_retry{envoy_cluster_name="httproute/%s/%s/rule/0"}`, routeNN.Namespace, routeNN.Name) before := float64(0) - v, err := prometheus.QuerySum(promAddr, promQL) + v, err := promClient.QuerySum(ctx, promQL) if err == nil { before = v } - t.Logf("query count %s before: %v", promQL, before) + tlog.Logf(t, "query count %s before: %v", promQL, before) req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req) @@ -73,11 +78,11 @@ var RetryTest = suite.ConformanceTest{ suite.TimeoutConfig.MaxTimeToConsistency, func(_ time.Duration) bool { // check retry stats from Prometheus - v, err := prometheus.QuerySum(promAddr, promQL) + v, err := promClient.QuerySum(ctx, promQL) if err != nil { return false } - t.Logf("query count %s after: %v", promQL, v) + tlog.Logf(t, "query count %s after: %v", promQL, v) delta := int64(v - before) // numRetries is 5, so delta mod 5 equals 0 diff --git a/test/e2e/tests/securitypolicy_transaltion_failed.go b/test/e2e/tests/securitypolicy_transaltion_failed.go new file mode 100644 index 00000000000..539fd4f726a --- /dev/null +++ b/test/e2e/tests/securitypolicy_transaltion_failed.go @@ -0,0 +1,62 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +//go:build e2e +// +build e2e + +package tests + +import ( + "testing" + + "k8s.io/apimachinery/pkg/types" + gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" + gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + "sigs.k8s.io/gateway-api/conformance/utils/http" + "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" + "sigs.k8s.io/gateway-api/conformance/utils/suite" + + "github.com/envoyproxy/gateway/internal/gatewayapi" +) + +func init() { + ConformanceTests = append(ConformanceTests, FailedSecurityPolicyDirectResponseTest) +} + +// FailedSecurityPolicyDirectResponseTest tests the direct 500 response for HTTPRoute targeted by a failed SecurityPolicy. +var FailedSecurityPolicyDirectResponseTest = suite.ConformanceTest{ + ShortName: "FailedSecurityPolicyDirectResponse", + Description: "Test direct 500 response when failed to translate SecurityPolicy", + Manifests: []string{"testdata/securitypolicy-translation-failed.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + t.Run("http route with failed SecurityPolicy", func(t *testing.T) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "http-with-oidc", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + + ancestorRef := gwapiv1a2.ParentReference{ + Group: gatewayapi.GroupPtr(gwapiv1.GroupName), + Kind: gatewayapi.KindPtr(gatewayapi.KindGateway), + Namespace: gatewayapi.NamespacePtr(gwNN.Namespace), + Name: gwapiv1.ObjectName(gwNN.Name), + } + SecurityPolicyMustFail(t, suite.Client, types.NamespacedName{Name: "oidc-test", Namespace: ns}, suite.ControllerName, ancestorRef, "") + + expectedResponse := http.ExpectedResponse{ + Request: http.Request{ + Host: "www.example.com", + Path: "/myapp", + }, + Response: http.Response{ + StatusCode: 500, + }, + Namespace: ns, + } + + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) + }) + }, +} diff --git a/test/e2e/tests/tcp_route.go b/test/e2e/tests/tcp_route.go index 8a703e08b92..71e43f17164 100644 --- a/test/e2e/tests/tcp_route.go +++ b/test/e2e/tests/tcp_route.go @@ -29,6 +29,7 @@ import ( "sigs.k8s.io/gateway-api/conformance/utils/http" "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" "sigs.k8s.io/gateway-api/conformance/utils/suite" + "sigs.k8s.io/gateway-api/conformance/utils/tlog" ) func init() { @@ -85,7 +86,7 @@ func GatewayAndTCPRoutesMustBeAccepted(t *testing.T, c client.Client, timeoutCon tcpRoute := &gwapiv1a2.TCPRoute{} err := c.Get(context.Background(), routeNNs[0], tcpRoute) if err != nil { - t.Logf("error fetching TCPRoute: %v", err) + tlog.Logf(t, "error fetching TCPRoute: %v", err) } gwAddr, err := WaitForGatewayAddress(t, c, timeoutConfig, gw.NamespacedName, string(*tcpRoute.Spec.ParentRefs[0].SectionName)) @@ -134,7 +135,7 @@ func WaitForGatewayAddress(t *testing.T, client client.Client, timeoutConfig con gw := &gwapiv1.Gateway{} err := client.Get(ctx, gwName, gw) if err != nil { - t.Logf("error fetching Gateway: %v", err) + tlog.Logf(t, "error fetching Gateway: %v", err) return false, fmt.Errorf("error fetching Gateway: %w", err) } diff --git a/test/e2e/tests/tracing.go b/test/e2e/tests/tracing.go index 2fe6b448481..bd169780121 100644 --- a/test/e2e/tests/tracing.go +++ b/test/e2e/tests/tracing.go @@ -11,23 +11,15 @@ package tests import ( "context" "fmt" - "net" - "net/http" - "net/url" - "strings" "testing" "time" - "github.com/go-logfmt/logfmt" - "github.com/gogo/protobuf/jsonpb" // nolint: depguard // tempopb use gogo/protobuf - "github.com/grafana/tempo/pkg/tempopb" - corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" - "sigs.k8s.io/controller-runtime/pkg/client" httputils "sigs.k8s.io/gateway-api/conformance/utils/http" "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" "sigs.k8s.io/gateway-api/conformance/utils/suite" + "sigs.k8s.io/gateway-api/conformance/utils/tlog" "github.com/envoyproxy/gateway/internal/utils/naming" ) @@ -69,7 +61,7 @@ var OpenTelemetryTracingTest = suite.ConformanceTest{ func(ctx context.Context) (bool, error) { count, err := QueryTraceFromTempo(t, suite.Client, tags) if err != nil { - t.Logf("failed to get trace count from tempo: %v", err) + tlog.Logf(t, "failed to get trace count from tempo: %v", err) return false, nil } @@ -114,94 +106,39 @@ var ZipkinTracingTest = suite.ConformanceTest{ // should make them kept consistent "service.name": fmt.Sprintf("%s/%s", gwNN.Namespace, gwNN.Name), } - // let's wait for the log to be sent to stdout if err := wait.PollUntilContextTimeout(context.TODO(), time.Second, time.Minute, true, func(ctx context.Context) (bool, error) { - count, err := QueryTraceFromTempo(t, suite.Client, tags) + preCount, err := QueryTraceFromTempo(t, suite.Client, tags) if err != nil { - t.Logf("failed to get trace count from tempo: %v", err) + tlog.Logf(t, "failed to get trace count from tempo: %v", err) return false, nil } - if count > 0 { - return true, nil + httputils.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) + + // looks like we need almost 15 seconds to get the trace from Tempo? + err = wait.PollUntilContextTimeout(context.TODO(), time.Second, 15*time.Second, true, func(ctx context.Context) (done bool, err error) { + curCount, err := QueryTraceFromTempo(t, suite.Client, tags) + if err != nil { + tlog.Logf(t, "failed to get curCount count from tempo: %v", err) + return false, nil + } + + if curCount > preCount { + return true, nil + } + + return false, nil + }) + if err != nil { + tlog.Logf(t, "failed to get current count from tempo: %v", err) + return false, nil } - return false, nil + + return true, nil }); err != nil { t.Errorf("failed to get trace from tempo: %v", err) } }) }, } - -// QueryTraceFromTempo queries span count from tempo -// TODO: move to utils package if needed -func QueryTraceFromTempo(t *testing.T, c client.Client, tags map[string]string) (int, error) { - svc := corev1.Service{} - if err := c.Get(context.Background(), types.NamespacedName{ - Namespace: "monitoring", - Name: "tempo", - }, &svc); err != nil { - return -1, err - } - host := "" - for _, ing := range svc.Status.LoadBalancer.Ingress { - if ing.IP != "" { - host = ing.IP - break - } - } - - tagsQueryParam, err := createTagsQueryParam(tags) - if err != nil { - return -1, err - } - - tempoURL := url.URL{ - Scheme: "http", - Host: net.JoinHostPort(host, "3100"), - Path: "/api/search", - } - query := tempoURL.Query() - query.Add("start", fmt.Sprintf("%d", time.Now().Add(-10*time.Minute).Unix())) // query traces from last 10 minutes - query.Add("end", fmt.Sprintf("%d", time.Now().Unix())) - query.Add("tags", tagsQueryParam) - tempoURL.RawQuery = query.Encode() - - req, err := http.NewRequest("GET", tempoURL.String(), nil) - if err != nil { - return -1, err - } - - t.Logf("send request to %s", tempoURL.String()) - res, err := http.DefaultClient.Do(req) - if err != nil { - return -1, err - } - - if res.StatusCode != http.StatusOK { - return -1, fmt.Errorf("failed to query tempo, url=%s, status=%s", tempoURL.String(), res.Status) - } - - tempoResponse := &tempopb.SearchResponse{} - if err := jsonpb.Unmarshal(res.Body, tempoResponse); err != nil { - return -1, err - } - - total := len(tempoResponse.Traces) - t.Logf("get response from tempo, url=%s, response=%v, total=%d", tempoURL.String(), tempoResponse, total) - return total, nil -} - -// copy from https://github.com/grafana/tempo/blob/c0127c78c368319433c7c67ca8967adbfed2259e/cmd/tempo-query/tempo/plugin.go#L361 -func createTagsQueryParam(tags map[string]string) (string, error) { - tagsBuilder := &strings.Builder{} - tagsEncoder := logfmt.NewEncoder(tagsBuilder) - for k, v := range tags { - err := tagsEncoder.EncodeKeyval(k, v) - if err != nil { - return "", err - } - } - return tagsBuilder.String(), nil -} diff --git a/test/e2e/tests/udproute.go b/test/e2e/tests/udproute.go index aed3258990d..4eb160139c1 100644 --- a/test/e2e/tests/udproute.go +++ b/test/e2e/tests/udproute.go @@ -28,6 +28,7 @@ import ( "sigs.k8s.io/gateway-api/conformance/utils/config" "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" "sigs.k8s.io/gateway-api/conformance/utils/suite" + "sigs.k8s.io/gateway-api/conformance/utils/tlog" ) func init() { @@ -51,12 +52,13 @@ var UDPRouteTest = suite.ConformanceTest{ if err := wait.PollUntilContextTimeout(context.TODO(), time.Second, time.Minute, true, func(_ context.Context) (done bool, err error) { - t.Logf("performing DNS query %s on %s", domain, gwAddr) - _, err = dns.Exchange(msg, gwAddr) + tlog.Logf(t, "performing DNS query %s on %s", domain, gwAddr) + r, err := dns.Exchange(msg, gwAddr) if err != nil { - t.Logf("failed to perform a UDP query: %v", err) + tlog.Logf(t, "failed to perform a UDP query: %v", err) return false, nil } + tlog.Logf(t, "got DNS response: %s", r.String()) return true, nil }); err != nil { t.Errorf("failed to perform DNS query: %v", err) @@ -158,31 +160,31 @@ func parentsForRouteMatch(t *testing.T, routeName types.NamespacedName, expected t.Helper() if len(expected) != len(actual) { - t.Logf("Route %s/%s expected %d Parents got %d", routeName.Namespace, routeName.Name, len(expected), len(actual)) + tlog.Logf(t, "Route %s/%s expected %d Parents got %d", routeName.Namespace, routeName.Name, len(expected), len(actual)) return false } for i, expectedParent := range expected { actualParent := actual[i] if actualParent.ControllerName != expectedParent.ControllerName { - t.Logf("Route %s/%s ControllerName doesn't match", routeName.Namespace, routeName.Name) + tlog.Logf(t, "Route %s/%s ControllerName doesn't match", routeName.Namespace, routeName.Name) return false } if !reflect.DeepEqual(actualParent.ParentRef.Group, expectedParent.ParentRef.Group) { - t.Logf("Route %s/%s expected ParentReference.Group to be %v, got %v", routeName.Namespace, routeName.Name, expectedParent.ParentRef.Group, actualParent.ParentRef.Group) + tlog.Logf(t, "Route %s/%s expected ParentReference.Group to be %v, got %v", routeName.Namespace, routeName.Name, expectedParent.ParentRef.Group, actualParent.ParentRef.Group) return false } if !reflect.DeepEqual(actualParent.ParentRef.Kind, expectedParent.ParentRef.Kind) { - t.Logf("Route %s/%s expected ParentReference.Kind to be %v, got %v", routeName.Namespace, routeName.Name, expectedParent.ParentRef.Kind, actualParent.ParentRef.Kind) + tlog.Logf(t, "Route %s/%s expected ParentReference.Kind to be %v, got %v", routeName.Namespace, routeName.Name, expectedParent.ParentRef.Kind, actualParent.ParentRef.Kind) return false } if actualParent.ParentRef.Name != expectedParent.ParentRef.Name { - t.Logf("Route %s/%s ParentReference.Name doesn't match", routeName.Namespace, routeName.Name) + tlog.Logf(t, "Route %s/%s ParentReference.Name doesn't match", routeName.Namespace, routeName.Name) return false } if !reflect.DeepEqual(actualParent.ParentRef.Namespace, expectedParent.ParentRef.Namespace) { if namespaceRequired || actualParent.ParentRef.Namespace != nil { - t.Logf("Route %s/%s expected ParentReference.Namespace to be %v, got %v", routeName.Namespace, routeName.Name, expectedParent.ParentRef.Namespace, actualParent.ParentRef.Namespace) + tlog.Logf(t, "Route %s/%s expected ParentReference.Namespace to be %v, got %v", routeName.Namespace, routeName.Name, expectedParent.ParentRef.Namespace, actualParent.ParentRef.Namespace) return false } } @@ -191,7 +193,7 @@ func parentsForRouteMatch(t *testing.T, routeName types.NamespacedName, expected } } - t.Logf("Route %s/%s Parents matched expectations", routeName.Namespace, routeName.Name) + tlog.Logf(t, "Route %s/%s Parents matched expectations", routeName.Namespace, routeName.Name) return true } @@ -199,7 +201,7 @@ func conditionsMatch(t *testing.T, expected, actual []metav1.Condition) bool { t.Helper() if len(actual) < len(expected) { - t.Logf("Expected more conditions to be present") + tlog.Logf(t, "Expected more conditions to be present") return false } for _, condition := range expected { @@ -208,7 +210,7 @@ func conditionsMatch(t *testing.T, expected, actual []metav1.Condition) bool { } } - t.Logf("Conditions matched expectations") + tlog.Logf(t, "Conditions matched expectations") return true } @@ -226,13 +228,13 @@ func findConditionInList(t *testing.T, conditions []metav1.Condition, condName, if expectedReason == "" || cond.Reason == expectedReason { return true } - t.Logf("%s condition Reason set to %s, expected %s", condName, cond.Reason, expectedReason) + tlog.Logf(t, "%s condition Reason set to %s, expected %s", condName, cond.Reason, expectedReason) } - t.Logf("%s condition set to Status %s with Reason %v, expected Status %s", condName, cond.Status, cond.Reason, expectedStatus) + tlog.Logf(t, "%s condition set to Status %s with Reason %v, expected Status %s", condName, cond.Status, cond.Reason, expectedStatus) } } - t.Logf("%s was not in conditions list [%v]", condName, conditions) + tlog.Logf(t, "%s was not in conditions list [%v]", condName, conditions) return false } diff --git a/test/e2e/tests/use_client_protocol.go b/test/e2e/tests/use_client_protocol.go index 1ea6029bf4f..0aa51afd077 100644 --- a/test/e2e/tests/use_client_protocol.go +++ b/test/e2e/tests/use_client_protocol.go @@ -44,6 +44,7 @@ var UseClientProtocolTest = suite.ConformanceTest{ Namespace: ns, } + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req) if err != nil { @@ -68,6 +69,7 @@ var UseClientProtocolTest = suite.ConformanceTest{ Namespace: ns, } + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) req = http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") cReq, cResp, err = suite.RoundTripper.CaptureRoundTrip(req) if err != nil { @@ -90,6 +92,7 @@ var UseClientProtocolTest = suite.ConformanceTest{ Namespace: ns, } + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) req = http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") cReq, cResp, err = suite.RoundTripper.CaptureRoundTrip(req) if err != nil { @@ -115,6 +118,7 @@ var UseClientProtocolTest = suite.ConformanceTest{ Namespace: ns, } + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) req = http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") cReq, cResp, err = suite.RoundTripper.CaptureRoundTrip(req) if err != nil { diff --git a/test/e2e/tests/utils.go b/test/e2e/tests/utils.go index b29d2b48d0d..c08b17685a3 100644 --- a/test/e2e/tests/utils.go +++ b/test/e2e/tests/utils.go @@ -11,6 +11,7 @@ import ( "errors" "fmt" "io" + "net" "net/http" "net/url" "strings" @@ -20,7 +21,10 @@ import ( "fortio.org/fortio/fhttp" "fortio.org/fortio/periodic" flog "fortio.org/log" + "github.com/go-logfmt/logfmt" + "github.com/gogo/protobuf/jsonpb" // nolint: depguard // tempopb use gogo/protobuf "github.com/google/go-cmp/cmp" + "github.com/grafana/tempo/pkg/tempopb" dto "github.com/prometheus/client_model/go" "github.com/prometheus/common/expfmt" "github.com/stretchr/testify/require" @@ -29,6 +33,7 @@ import ( "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" "sigs.k8s.io/gateway-api/conformance/utils/config" @@ -36,14 +41,17 @@ import ( "sigs.k8s.io/gateway-api/conformance/utils/tlog" egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" + tb "github.com/envoyproxy/gateway/internal/troubleshoot" ) const defaultServiceStartupTimeout = 5 * time.Minute +var PodReady = corev1.PodCondition{Type: corev1.PodReady, Status: corev1.ConditionTrue} + // WaitForPods waits for the pods in the given namespace and with the given selector // to be in the given phase and condition. func WaitForPods(t *testing.T, cl client.Client, namespace string, selectors map[string]string, phase corev1.PodPhase, condition corev1.PodCondition) { - t.Logf("waiting for %s/[%s] to be %v...", namespace, selectors, phase) + tlog.Logf(t, "waiting for %s/[%s] to be %v...", namespace, selectors, phase) require.Eventually(t, func() bool { pods := &corev1.PodList{} @@ -73,7 +81,7 @@ func WaitForPods(t *testing.T, cl client.Client, namespace string, selectors map } } - t.Logf("pod %s/%s status: %v", p.Namespace, p.Name, p.Status) + tlog.Logf(t, "pod %s/%s status: %v", p.Namespace, p.Name, p.Status) return false } @@ -93,16 +101,44 @@ func SecurityPolicyMustBeAccepted(t *testing.T, client client.Client, policyName } if policyAcceptedByAncestor(policy.Status.Ancestors, controllerName, ancestorRef) { + tlog.Logf(t, "SecurityPolicy has been accepted: %v", policy) return true, nil } - t.Logf("SecurityPolicy not yet accepted: %v", policy) + tlog.Logf(t, "SecurityPolicy not yet accepted: %v", policy) return false, nil }) require.NoErrorf(t, waitErr, "error waiting for SecurityPolicy to be accepted") } +// SecurityPolicyMustFail waits for an SecurityPolicy to fail with the specified reason. +func SecurityPolicyMustFail( + t *testing.T, client client.Client, policyName types.NamespacedName, + controllerName string, ancestorRef gwapiv1a2.ParentReference, message string, +) { + t.Helper() + + policy := &egv1a1.SecurityPolicy{} + waitErr := wait.PollUntilContextTimeout( + context.Background(), 1*time.Second, 60*time.Second, + true, func(ctx context.Context) (bool, error) { + err := client.Get(ctx, policyName, policy) + if err != nil { + return false, fmt.Errorf("error fetching SecurityPolicy: %w", err) + } + + if policyFailAcceptedByAncestor(policy.Status.Ancestors, controllerName, ancestorRef, message) { + tlog.Logf(t, "SecurityPolicy has been failed: %v", policy) + return true, nil + } + + return false, nil + }) + + require.NoErrorf(t, waitErr, "error waiting for SecurityPolicy to fail with message: %s policy %v", message, policy) +} + // BackendTrafficPolicyMustBeAccepted waits for the specified BackendTrafficPolicy to be accepted. func BackendTrafficPolicyMustBeAccepted(t *testing.T, client client.Client, policyName types.NamespacedName, controllerName string, ancestorRef gwapiv1a2.ParentReference) { t.Helper() @@ -118,7 +154,7 @@ func BackendTrafficPolicyMustBeAccepted(t *testing.T, client client.Client, poli return true, nil } - t.Logf("BackendTrafficPolicy not yet accepted: %v", policy) + tlog.Logf(t, "BackendTrafficPolicy not yet accepted: %v", policy) return false, nil }) @@ -140,7 +176,7 @@ func ClientTrafficPolicyMustBeAccepted(t *testing.T, client client.Client, polic return true, nil } - t.Logf("ClientTrafficPolicy not yet accepted: %v", policy) + tlog.Logf(t, "ClientTrafficPolicy not yet accepted: %v", policy) return false, nil }) @@ -180,7 +216,7 @@ func runLoadAndWait(t *testing.T, timeoutConfig config.TimeoutConfig, done chan res, err := fhttp.RunHTTPTest(&opts) if err != nil { done <- false - t.Logf("failed to create load: %v", err) + tlog.Logf(t, "failed to create load: %v", err) } // collect stats @@ -189,7 +225,7 @@ func runLoadAndWait(t *testing.T, timeoutConfig config.TimeoutConfig, done chan failedReq := totalReq - okReq errorReq := res.ErrorsDurationHistogram.Count timedOut := res.ActualDuration == opts.Duration - t.Logf("Load completed after %s with %d requests, %d success, %d failures and %d errors", res.ActualDuration, totalReq, okReq, failedReq, errorReq) + tlog.Logf(t, "Load completed after %s with %d requests, %d success, %d failures and %d errors", res.ActualDuration, totalReq, okReq, failedReq, errorReq) if okReq == totalReq && errorReq == 0 && !timedOut { done <- true @@ -227,7 +263,7 @@ func EnvoyExtensionPolicyMustFail( } if policyFailAcceptedByAncestor(policy.Status.Ancestors, controllerName, ancestorRef, message) { - t.Logf("EnvoyExtensionPolicy has been failed: %v", policy) + tlog.Logf(t, "EnvoyExtensionPolicy has been failed: %v", policy) return true, nil } @@ -264,11 +300,11 @@ func EnvoyExtensionPolicyMustBeAccepted(t *testing.T, client client.Client, poli } if policyAcceptedByAncestor(policy.Status.Ancestors, controllerName, ancestorRef) { - t.Logf("EnvoyExtensionPolicy has been accepted: %v", policy) + tlog.Logf(t, "EnvoyExtensionPolicy has been accepted: %v", policy) return true, nil } - t.Logf("EnvoyExtensionPolicy not yet accepted: %v", policy) + tlog.Logf(t, "EnvoyExtensionPolicy not yet accepted: %v", policy) return false, nil }) @@ -281,7 +317,7 @@ func ScrapeMetrics(t *testing.T, c client.Client, nn types.NamespacedName, port return err } - t.Logf("scraping metrics from %s", url) + tlog.Logf(t, "scraping metrics from %s", url) metrics, err := RetrieveMetrics(url, time.Second) if err != nil { @@ -344,7 +380,7 @@ func RetrieveMetric(url string, name string, timeout time.Duration) (*dto.Metric return mf, nil } - return nil, fmt.Errorf("metric %s not found", name) + return nil, nil } func WaitForLoadBalancerAddress(t *testing.T, client client.Client, timeout time.Duration, nn types.NamespacedName) (string, error) { @@ -369,18 +405,23 @@ func WaitForLoadBalancerAddress(t *testing.T, client client.Client, timeout time return ipAddr, nil } -func ALSLogCount(t *testing.T, suite *suite.ConformanceTestSuite) int { +func ALSLogCount(suite *suite.ConformanceTestSuite) (int, error) { metricPath, err := RetrieveURL(suite.Client, types.NamespacedName{ Namespace: "monitoring", Name: "envoy-als", }, 19001, "/metrics") if err != nil { - t.Fatalf("failed to get metric url: %v", err) + return -1, err } countMetric, err := RetrieveMetric(metricPath, "log_count", time.Second) if err != nil { - t.Fatalf("failed to get metric: %v", err) + return -1, err + } + + // metric not found or empty + if countMetric == nil { + return 0, nil } total := 0 @@ -390,7 +431,7 @@ func ALSLogCount(t *testing.T, suite *suite.ConformanceTestSuite) int { } } - return total + return total, nil } // QueryLogCountFromLoki queries log count from loki @@ -427,7 +468,7 @@ func QueryLogCountFromLoki(t *testing.T, c client.Client, keyValues map[string]s if err != nil { return -1, err } - t.Logf("get response from loki, query=%s, status=%s", q, res.Status) + tlog.Logf(t, "get response from loki, query=%s, status=%s", q, res.Status) b, err := io.ReadAll(res.Body) if err != nil { @@ -447,7 +488,7 @@ func QueryLogCountFromLoki(t *testing.T, c client.Client, keyValues map[string]s for _, res := range lokiResponse.Data.Result { total += len(res.Values) } - t.Logf("get response from loki, query=%s, total=%d", q, total) + tlog.Logf(t, "get response from loki, query=%s, total=%d", q, total) return total, nil } @@ -461,3 +502,84 @@ type LokiQueryResponse struct { } } } + +// QueryTraceFromTempo queries span count from tempo +func QueryTraceFromTempo(t *testing.T, c client.Client, tags map[string]string) (int, error) { + svc := corev1.Service{} + if err := c.Get(context.Background(), types.NamespacedName{ + Namespace: "monitoring", + Name: "tempo", + }, &svc); err != nil { + return -1, err + } + host := "" + for _, ing := range svc.Status.LoadBalancer.Ingress { + if ing.IP != "" { + host = ing.IP + break + } + } + + tagsQueryParam, err := createTagsQueryParam(tags) + if err != nil { + return -1, err + } + + tempoURL := url.URL{ + Scheme: "http", + Host: net.JoinHostPort(host, "3100"), + Path: "/api/search", + } + query := tempoURL.Query() + query.Add("start", fmt.Sprintf("%d", time.Now().Add(-10*time.Minute).Unix())) // query traces from last 10 minutes + query.Add("end", fmt.Sprintf("%d", time.Now().Unix())) + query.Add("tags", tagsQueryParam) + tempoURL.RawQuery = query.Encode() + + req, err := http.NewRequest("GET", tempoURL.String(), nil) + if err != nil { + return -1, err + } + + tlog.Logf(t, "send request to %s", tempoURL.String()) + res, err := http.DefaultClient.Do(req) + if err != nil { + return -1, err + } + + if res.StatusCode != http.StatusOK { + return -1, fmt.Errorf("failed to query tempo, url=%s, status=%s", tempoURL.String(), res.Status) + } + + tempoResponse := &tempopb.SearchResponse{} + if err := jsonpb.Unmarshal(res.Body, tempoResponse); err != nil { + return -1, err + } + + total := len(tempoResponse.Traces) + tlog.Logf(t, "get response from tempo, url=%s, response=%v, total=%d", tempoURL.String(), tempoResponse, total) + return total, nil +} + +// copy from https://github.com/grafana/tempo/blob/c0127c78c368319433c7c67ca8967adbfed2259e/cmd/tempo-query/tempo/plugin.go#L361 +func createTagsQueryParam(tags map[string]string) (string, error) { + tagsBuilder := &strings.Builder{} + tagsEncoder := logfmt.NewEncoder(tagsBuilder) + for k, v := range tags { + err := tagsEncoder.EncodeKeyval(k, v) + if err != nil { + return "", err + } + } + return tagsBuilder.String(), nil +} + +// CollectAndDump collects and dumps the cluster data for troubleshooting and log. +// This function should be call within t.Cleanup. +func CollectAndDump(t *testing.T, rest *rest.Config) { + result := tb.CollectResult(context.TODO(), rest, "", "envoy-gateway") + for r, data := range result { + tlog.Logf(t, "filename: %s", r) + tlog.Logf(t, "data: \n%s", data) + } +} diff --git a/test/e2e/tests/wasm_oci.go b/test/e2e/tests/wasm_oci.go index 1a41187092b..69c6b0ad6c1 100644 --- a/test/e2e/tests/wasm_oci.go +++ b/test/e2e/tests/wasm_oci.go @@ -37,6 +37,7 @@ import ( "sigs.k8s.io/gateway-api/conformance/utils/http" "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" "sigs.k8s.io/gateway-api/conformance/utils/suite" + "sigs.k8s.io/gateway-api/conformance/utils/tlog" egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" "github.com/envoyproxy/gateway/internal/gatewayapi" @@ -303,7 +304,7 @@ func pushWasmImageForTest(t *testing.T, suite *suite.ConformanceTestSuite, regis if err == nil { break } - t.Logf("failed to push image: %v", err) + tlog.Logf(t, "failed to push image: %v", err) } if err != nil { t.Fatalf("failed to push image: %v", err) @@ -316,7 +317,7 @@ func pushWasmImageForTest(t *testing.T, suite *suite.ConformanceTestSuite, regis t.Fatalf("failed to get image digest: %v", err) } - t.Logf("pushed image %s with digest: %s", tag, digest.Hex) + tlog.Logf(t, "pushed image %s with digest: %s", tag, digest.Hex) return digest.Hex } diff --git a/test/e2e/tests/weighted_backend.go b/test/e2e/tests/weighted_backend.go index 250c6ef4cc3..5bab56f53b8 100644 --- a/test/e2e/tests/weighted_backend.go +++ b/test/e2e/tests/weighted_backend.go @@ -23,14 +23,14 @@ func init() { ConformanceTests = append(ConformanceTests, WeightEqualTest, WeightBlueGreenTest, WeightCompleteRolloutTest) } -const sendRequest = 50 - var WeightEqualTest = suite.ConformanceTest{ ShortName: "WeightEqualBackend", Description: "Resource with Weight Backend enabled, and use the all backend weight is equal", Manifests: []string{"testdata/weighted-backend-all-equal.yaml"}, Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { t.Run("all backends receive the same weight of traffic", func(t *testing.T) { + const sendRequests = 50 + ns := "gateway-conformance-infra" weightEqualRoute := types.NamespacedName{Name: "weight-equal-http-route", Namespace: ns} gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} @@ -52,11 +52,11 @@ var WeightEqualTest = suite.ConformanceTest{ // Since we only route to pods with "infra-backend-v 1" and "infra-backend-v 2" prefixes // So here we use fixed weight values expected := map[string]int{ - "infra-backend-v1": sendRequest * .5, - "infra-backend-v2": sendRequest * .5, + "infra-backend-v1": sendRequests * .5, + "infra-backend-v2": sendRequests * .5, } weightMap := make(map[string]int) - for i := 0; i < sendRequest; i++ { + for i := 0; i < sendRequests; i++ { cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req) if err != nil { t.Errorf("failed to get expected response: %v", err) @@ -95,6 +95,8 @@ var WeightBlueGreenTest = suite.ConformanceTest{ Manifests: []string{"testdata/weighted-backend-bluegreen.yaml"}, Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { t.Run("all backends receive the blue green weight of traffic", func(t *testing.T) { + const sendRequests = 50 + ns := "gateway-conformance-infra" weightEqualRoute := types.NamespacedName{Name: "weight-bluegreen-http-route", Namespace: ns} gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} @@ -116,11 +118,11 @@ var WeightBlueGreenTest = suite.ConformanceTest{ // Since we only route to pods with "infra-backend-v 1" and "infra-backend-v 2" prefixes // So here we use fixed weight values expected := map[string]int{ - "infra-backend-v1": sendRequest * .9, - "infra-backend-v2": sendRequest * .1, + "infra-backend-v1": sendRequests * .9, + "infra-backend-v2": sendRequests * .1, } weightMap := make(map[string]int) - for i := 0; i < sendRequest; i++ { + for i := 0; i < sendRequests; i++ { cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req) if err != nil { t.Errorf("failed to get expected response: %v", err) @@ -159,6 +161,8 @@ var WeightCompleteRolloutTest = suite.ConformanceTest{ Manifests: []string{"testdata/weight-backend-completing-rollout.yaml"}, Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { t.Run("all backends receive the complete rollout weight of traffic", func(t *testing.T) { + const sendRequests = 50 + ns := "gateway-conformance-infra" weightEqualRoute := types.NamespacedName{Name: "weight-complete-rollout-http-route", Namespace: ns} gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} @@ -180,11 +184,11 @@ var WeightCompleteRolloutTest = suite.ConformanceTest{ // Since we only route to pods with "infra-backend-v 1" and "infra-backend-v 2" prefixes // So here we use fixed weight values expected := map[string]int{ - "infra-backend-v1": sendRequest * 1, - "infra-backend-v2": sendRequest * 0, + "infra-backend-v1": sendRequests * 1, + "infra-backend-v2": sendRequests * 0, } weightMap := make(map[string]int) - for i := 0; i < sendRequest; i++ { + for i := 0; i < sendRequests; i++ { cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req) if err != nil { t.Errorf("failed to get expected response: %v", err) diff --git a/test/e2e/upgrade/eg_upgrade_test.go b/test/e2e/upgrade/eg_upgrade_test.go index ec043318915..7ef3c406e31 100644 --- a/test/e2e/upgrade/eg_upgrade_test.go +++ b/test/e2e/upgrade/eg_upgrade_test.go @@ -13,57 +13,53 @@ import ( "io/fs" "testing" - "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/util/sets" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/config" "sigs.k8s.io/gateway-api/conformance/utils/flags" "sigs.k8s.io/gateway-api/conformance/utils/suite" + "sigs.k8s.io/gateway-api/conformance/utils/tlog" "sigs.k8s.io/gateway-api/pkg/features" "github.com/envoyproxy/gateway/test/e2e" "github.com/envoyproxy/gateway/test/e2e/tests" + kubetest "github.com/envoyproxy/gateway/test/utils/kubernetes" ) func TestEGUpgrade(t *testing.T) { flag.Parse() - cfg, err := config.GetConfig() - require.NoError(t, err) - - c, err := client.New(cfg, client.Options{}) - require.NoError(t, err) - - // Install all the scheme to kubernetes client. - e2e.CheckInstallScheme(t, c) + c, cfg := kubetest.NewClient(t) if flags.RunTest != nil && *flags.RunTest != "" { - t.Logf("Running E2E test %s with %s GatewayClass\n cleanup: %t\n debug: %t", + tlog.Logf(t, "Running E2E test %s with %s GatewayClass\n cleanup: %t\n debug: %t", *flags.RunTest, *flags.GatewayClassName, *flags.CleanupBaseResources, *flags.ShowDebug) } else { - t.Logf("Running E2E tests with %s GatewayClass\n cleanup: %t\n debug: %t", + tlog.Logf(t, "Running E2E tests with %s GatewayClass\n cleanup: %t\n debug: %t", *flags.GatewayClassName, *flags.CleanupBaseResources, *flags.ShowDebug) } cSuite, err := suite.NewConformanceTestSuite(suite.ConformanceOptions{ Client: c, + RestConfig: cfg, GatewayClassName: *flags.GatewayClassName, Debug: *flags.ShowDebug, CleanupBaseResources: *flags.CleanupBaseResources, - ManifestFS: []fs.FS{e2e.Manifests}, + ManifestFS: []fs.FS{e2e.UpgradeManifests}, RunTest: *flags.RunTest, - // SupportedFeatures cannot be empty, so we set it to SupportGateway - // All e2e tests should leave Features empty. - SupportedFeatures: sets.New[features.SupportedFeature](features.SupportGateway), - SkipTests: []string{ - tests.EGUpgradeTest.ShortName, // https://github.com/envoyproxy/gateway/issues/3311 - }, + BaseManifests: "upgrade/manifests.yaml", + SupportedFeatures: sets.New[features.SupportedFeature](features.SupportGateway), + SkipTests: []string{}, }) if err != nil { t.Fatalf("Failed to create test suite: %v", err) } - t.Logf("Running %d Upgrade tests", len(tests.UpgradeTests)) + // upgrade tests should be executed in a specific order + tests.UpgradeTests = []suite.ConformanceTest{ + tests.EnvoyShutdownTest, + tests.EGUpgradeTest, + } + + tlog.Logf(t, "Running %d Upgrade tests", len(tests.UpgradeTests)) cSuite.Setup(t, tests.UpgradeTests) err = cSuite.Run(t, tests.UpgradeTests) diff --git a/test/e2e/upgrade/manifests.yaml b/test/e2e/upgrade/manifests.yaml new file mode 100644 index 00000000000..363ede65779 --- /dev/null +++ b/test/e2e/upgrade/manifests.yaml @@ -0,0 +1,125 @@ +# Adapted from https://github.com/kubernetes-sigs/gateway-api/blob/main/conformance/base/manifests.yaml +# This file contains limited base resources that are required for upgrade tests +kind: GatewayClass +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: upgrade +spec: + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parametersRef: + group: gateway.envoyproxy.io + kind: EnvoyProxy + name: upgrade-config + namespace: envoy-gateway-system +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyProxy +metadata: + name: upgrade-config + namespace: envoy-gateway-system +spec: + provider: + type: Kubernetes + kubernetes: + envoyDeployment: + replicas: 2 + patch: + type: StrategicMerge + value: + spec: + template: + spec: + containers: + - name: envoy + readinessProbe: + initialDelaySeconds: 5 +--- +apiVersion: v1 +kind: Namespace +metadata: + name: gateway-conformance-infra + labels: + gateway-conformance: infra +--- +apiVersion: v1 +kind: Namespace +metadata: + name: gateway-upgrade-infra +--- +apiVersion: v1 +kind: Namespace +metadata: + name: gateway-conformance-app-backend + labels: + gateway-conformance: backend +--- +apiVersion: v1 +kind: Namespace +metadata: + name: gateway-conformance-web-backend + labels: + gateway-conformance: backend +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: ha-gateway + namespace: gateway-upgrade-infra +spec: + gatewayClassName: upgrade + listeners: + - allowedRoutes: + namespaces: + from: Same + name: http1 + port: 80 + protocol: HTTP +--- +apiVersion: v1 +kind: Service +metadata: + name: infra-backend + namespace: gateway-upgrade-infra +spec: + selector: + app: infra-backend + ports: + - protocol: TCP + port: 8080 + targetPort: 3000 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: infra-backend + namespace: gateway-upgrade-infra + labels: + app: infra-backend +spec: + replicas: 2 + selector: + matchLabels: + app: infra-backend + template: + metadata: + labels: + app: infra-backend + spec: + containers: + - name: infra-backend + # From https://github.com/kubernetes-sigs/gateway-api/blob/main/conformance/echo-basic/echo-basic.go + image: gcr.io/k8s-staging-gateway-api/echo-basic:v20231214-v1.0.0-140-gf544a46e + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: SERVICE_NAME + value: infra-backend + resources: + requests: + cpu: 10m diff --git a/test/helm/gateway-addons-helm/default.out.yaml b/test/helm/gateway-addons-helm/default.out.yaml index 471eb590d77..060791f94d5 100644 --- a/test/helm/gateway-addons-helm/default.out.yaml +++ b/test/helm/gateway-addons-helm/default.out.yaml @@ -5395,270 +5395,7 @@ data: "version": 1, "weekStart": "" } - envoy-gateway-resource.json: |- - { - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": { - "type": "grafana", - "uid": "-- Grafana --" - }, - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - } - ] - }, - "description": "Envoy Gateway Memory and CPU Usage", - "editable": true, - "fiscalYearStartMonth": 0, - "graphTooltip": 0, - "links": [], - "panels": [ - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 0 - }, - "id": 5, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "maxHeight": 600, - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "PBFA97CFB590B2093" - }, - "disableTextWrap": false, - "editorMode": "code", - "expr": "sum by(namespace) (container_memory_working_set_bytes{container=\"envoy-gateway\"}/1024/1024)", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "__auto", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Envoy Gateway Memory Usage (MiB)", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 0 - }, - "id": 6, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "maxHeight": 600, - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "PBFA97CFB590B2093" - }, - "disableTextWrap": false, - "editorMode": "code", - "expr": "sum by(namespace) (rate(container_cpu_usage_seconds_total{container=\"envoy-gateway\"}[5m]) * 1000)", - "fullMetaSearch": false, - "includeNullMetadata": false, - "instant": false, - "legendFormat": "__auto", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Envoy Gateway CPU Time (ms)", - "type": "timeseries" - } - ], - "schemaVersion": 39, - "tags": [ - "Control Plane" - ], - "templating": { - "list": [ - { - "current": { - "selected": false, - "text": "Prometheus", - "value": "PBFA97CFB590B2093" - }, - "hide": 0, - "includeAll": false, - "multi": false, - "name": "datasource", - "options": [], - "query": "prometheus", - "queryValue": "", - "refresh": 1, - "regex": "", - "skipUrlSync": false, - "type": "datasource" - } - ] - }, - "time": { - "from": "now-15m", - "to": "now" - }, - "timeRangeUpdatedDuringEditOrView": false, - "timepicker": {}, - "timezone": "", - "title": "Envoy Gateway Resources", - "uid": "edq1b2tldspa8d", - "version": 1, - "weekStart": "" - } - envoy-global.json: |- + envoy-proxy-global.json: |- { "annotations": { "list": [ @@ -8641,261 +8378,255 @@ data: "version": 1, "weekStart": "" } - envoy-pod-resource.json: |- + resources-monitor.gen.json: |- { - "annotations": { - "list": [ + "description": "Memory and CPU Usage Monitor for Envoy Gateway and Envoy Proxy.\n", + "graphTooltip": 1, + "panels": [ { - "builtIn": 1, - "datasource": { - "type": "grafana", - "uid": "-- Grafana --" - }, - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - } - ] - }, - "description": "Envoy Pod Memory and CPU Usage", - "editable": true, - "fiscalYearStartMonth": 0, - "graphTooltip": 0, - "id": 4, - "links": [], - "liveNow": false, - "panels": [ - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 1, + "panels": [ ], + "title": "Envoy Gateway", + "type": "row" }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" + { + "datasource": { + "type": "datasource", + "uid": "-- Mixed --" + }, + "fieldConfig": { + "defaults": { + "custom": { + "fillOpacity": 10, + "scaleDistribution": { + "log": 10, + "type": "log" + }, + "showPoints": "never" + }, + "unit": "s" } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 0 - }, - "id": 2, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "maxHeight": 600, - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "editorMode": "code", - "exemplar": false, - "expr": "sum by(pod) (container_memory_working_set_bytes{container=~\"envoy\"}/1000000)", - "instant": false, - "range": true, - "refId": "A" - } - ], - "title": "Memory Working Set Envoy Pods(mb)", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 1 + }, + "id": 2, + "interval": "1m", + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max" + ], + "displayMode": "table" + } + }, + "pluginVersion": "v11.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "expr": "sum by (namespace) (\n rate(\n container_cpu_usage_seconds_total{\n container=\"envoy-gateway\"\n }\n [$__rate_interval])\n)\n", + "intervalFactor": 2, + "legendFormat": "{{namespace}}\n" + } + ], + "title": "CPU Usage", + "type": "timeseries" }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" + { + "datasource": { + "type": "datasource", + "uid": "-- Mixed --" + }, + "fieldConfig": { + "defaults": { + "custom": { + "fillOpacity": 10, + "scaleDistribution": { + "log": 2, + "type": "log" + }, + "showPoints": "never" + }, + "unit": "bytes" } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 1 + }, + "id": 3, + "interval": "1m", + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max" + ], + "displayMode": "table" + } + }, + "pluginVersion": "v11.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "expr": "sum by (namespace) (\n container_memory_working_set_bytes{container=\"envoy-gateway\"}\n)\n", + "intervalFactor": 2, + "legendFormat": "{{namespace}}\n" + } + ], + "title": "Memory Usage", + "type": "timeseries" }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 0 + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 9 + }, + "id": 4, + "panels": [ ], + "title": "Envoy Proxy", + "type": "row" }, - "id": 1, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "maxHeight": 600, - "mode": "single", - "sort": "none" - } + { + "datasource": { + "type": "datasource", + "uid": "-- Mixed --" + }, + "fieldConfig": { + "defaults": { + "custom": { + "fillOpacity": 10, + "scaleDistribution": { + "log": 10, + "type": "log" + }, + "showPoints": "never" + }, + "unit": "s" + } + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 10 + }, + "id": 5, + "interval": "1m", + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max" + ], + "displayMode": "table" + } + }, + "pluginVersion": "v11.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "expr": "sum by (pod) (\n rate(\n container_cpu_usage_seconds_total{\n container=\"envoy\"\n }\n [$__rate_interval])\n)\n", + "intervalFactor": 2, + "legendFormat": "{{pod}}\n" + } + ], + "title": "CPU Usage", + "type": "timeseries" }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "PBFA97CFB590B2093" - }, - "editorMode": "builder", - "expr": "sum by(pod) (rate(container_cpu_usage_seconds_total{container=\"envoy\"}[5m]))", - "instant": false, - "range": true, - "refId": "A" - } - ], - "title": "CPU Usage Envoy Pods", - "type": "timeseries" - } - ], - "refresh": "", - "schemaVersion": 39, - "tags": [ - "Data Plane" - ], - "templating": { - "list": [ { - "current": { - "selected": false, - "text": "Prometheus", - "value": "PBFA97CFB590B2093" - }, - "hide": 0, - "includeAll": false, - "multi": false, - "name": "datasource", - "options": [], - "query": "prometheus", - "refresh": 1, - "regex": "", - "skipUrlSync": false, - "type": "datasource" + "datasource": { + "type": "datasource", + "uid": "-- Mixed --" + }, + "fieldConfig": { + "defaults": { + "custom": { + "fillOpacity": 10, + "scaleDistribution": { + "log": 2, + "type": "log" + }, + "showPoints": "never" + }, + "unit": "bytes" + } + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 10 + }, + "id": 6, + "interval": "1m", + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max" + ], + "displayMode": "table" + } + }, + "pluginVersion": "v11.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "expr": "sum by (pod) (\n container_memory_working_set_bytes{container=\"envoy\"}\n)\n", + "intervalFactor": 2, + "legendFormat": "{{pod}}\n" + } + ], + "title": "Memory Usage", + "type": "timeseries" } - ] - }, - "time": { - "from": "now-5m", - "to": "now" - }, - "timeRangeUpdatedDuringEditOrView": false, - "timepicker": {}, - "timezone": "", - "title": "Envoy Pod Resources", - "uid": "f2279235-80b7-4c85-84f4-f25a3bf3eac0", - "version": 1, - "weekStart": "" + ], + "schemaVersion": 39, + "templating": { + "list": [ + { + "name": "datasource", + "query": "prometheus", + "type": "datasource" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timezone": "utc", + "title": "Resources Monitor", + "uid": "f7aeb41676b7865cf31ae49691325f91" } --- # Source: gateway-addons-helm/charts/fluent-bit/templates/clusterrole.yaml @@ -9328,7 +9059,7 @@ spec: dnsPolicy: ClusterFirst containers: - name: fluent-bit - image: "cr.fluentbit.io/fluent/fluent-bit:2.1.4" + image: "fluent/fluent-bit:2.1.4" imagePullPolicy: Always ports: - name: http diff --git a/test/helm/gateway-addons-helm/e2e.out.yaml b/test/helm/gateway-addons-helm/e2e.out.yaml index 3cc07fe217f..df3d02ade7e 100644 --- a/test/helm/gateway-addons-helm/e2e.out.yaml +++ b/test/helm/gateway-addons-helm/e2e.out.yaml @@ -5427,270 +5427,7 @@ data: "version": 1, "weekStart": "" } - envoy-gateway-resource.json: |- - { - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": { - "type": "grafana", - "uid": "-- Grafana --" - }, - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - } - ] - }, - "description": "Envoy Gateway Memory and CPU Usage", - "editable": true, - "fiscalYearStartMonth": 0, - "graphTooltip": 0, - "links": [], - "panels": [ - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 0 - }, - "id": 5, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "maxHeight": 600, - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "PBFA97CFB590B2093" - }, - "disableTextWrap": false, - "editorMode": "code", - "expr": "sum by(namespace) (container_memory_working_set_bytes{container=\"envoy-gateway\"}/1024/1024)", - "fullMetaSearch": false, - "includeNullMetadata": true, - "instant": false, - "legendFormat": "__auto", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Envoy Gateway Memory Usage (MiB)", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 0 - }, - "id": 6, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "maxHeight": 600, - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "PBFA97CFB590B2093" - }, - "disableTextWrap": false, - "editorMode": "code", - "expr": "sum by(namespace) (rate(container_cpu_usage_seconds_total{container=\"envoy-gateway\"}[5m]) * 1000)", - "fullMetaSearch": false, - "includeNullMetadata": false, - "instant": false, - "legendFormat": "__auto", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Envoy Gateway CPU Time (ms)", - "type": "timeseries" - } - ], - "schemaVersion": 39, - "tags": [ - "Control Plane" - ], - "templating": { - "list": [ - { - "current": { - "selected": false, - "text": "Prometheus", - "value": "PBFA97CFB590B2093" - }, - "hide": 0, - "includeAll": false, - "multi": false, - "name": "datasource", - "options": [], - "query": "prometheus", - "queryValue": "", - "refresh": 1, - "regex": "", - "skipUrlSync": false, - "type": "datasource" - } - ] - }, - "time": { - "from": "now-15m", - "to": "now" - }, - "timeRangeUpdatedDuringEditOrView": false, - "timepicker": {}, - "timezone": "", - "title": "Envoy Gateway Resources", - "uid": "edq1b2tldspa8d", - "version": 1, - "weekStart": "" - } - envoy-global.json: |- + envoy-proxy-global.json: |- { "annotations": { "list": [ @@ -8673,261 +8410,255 @@ data: "version": 1, "weekStart": "" } - envoy-pod-resource.json: |- + resources-monitor.gen.json: |- { - "annotations": { - "list": [ + "description": "Memory and CPU Usage Monitor for Envoy Gateway and Envoy Proxy.\n", + "graphTooltip": 1, + "panels": [ { - "builtIn": 1, - "datasource": { - "type": "grafana", - "uid": "-- Grafana --" - }, - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - } - ] - }, - "description": "Envoy Pod Memory and CPU Usage", - "editable": true, - "fiscalYearStartMonth": 0, - "graphTooltip": 0, - "id": 4, - "links": [], - "liveNow": false, - "panels": [ - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 1, + "panels": [ ], + "title": "Envoy Gateway", + "type": "row" }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" + { + "datasource": { + "type": "datasource", + "uid": "-- Mixed --" + }, + "fieldConfig": { + "defaults": { + "custom": { + "fillOpacity": 10, + "scaleDistribution": { + "log": 10, + "type": "log" + }, + "showPoints": "never" + }, + "unit": "s" } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 0 - }, - "id": 2, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "maxHeight": 600, - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "editorMode": "code", - "exemplar": false, - "expr": "sum by(pod) (container_memory_working_set_bytes{container=~\"envoy\"}/1000000)", - "instant": false, - "range": true, - "refId": "A" - } - ], - "title": "Memory Working Set Envoy Pods(mb)", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 1 + }, + "id": 2, + "interval": "1m", + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max" + ], + "displayMode": "table" + } + }, + "pluginVersion": "v11.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "expr": "sum by (namespace) (\n rate(\n container_cpu_usage_seconds_total{\n container=\"envoy-gateway\"\n }\n [$__rate_interval])\n)\n", + "intervalFactor": 2, + "legendFormat": "{{namespace}}\n" + } + ], + "title": "CPU Usage", + "type": "timeseries" }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" + { + "datasource": { + "type": "datasource", + "uid": "-- Mixed --" + }, + "fieldConfig": { + "defaults": { + "custom": { + "fillOpacity": 10, + "scaleDistribution": { + "log": 2, + "type": "log" + }, + "showPoints": "never" + }, + "unit": "bytes" } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 1 + }, + "id": 3, + "interval": "1m", + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max" + ], + "displayMode": "table" + } + }, + "pluginVersion": "v11.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "expr": "sum by (namespace) (\n container_memory_working_set_bytes{container=\"envoy-gateway\"}\n)\n", + "intervalFactor": 2, + "legendFormat": "{{namespace}}\n" + } + ], + "title": "Memory Usage", + "type": "timeseries" }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 0 + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 9 + }, + "id": 4, + "panels": [ ], + "title": "Envoy Proxy", + "type": "row" }, - "id": 1, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "maxHeight": 600, - "mode": "single", - "sort": "none" - } + { + "datasource": { + "type": "datasource", + "uid": "-- Mixed --" + }, + "fieldConfig": { + "defaults": { + "custom": { + "fillOpacity": 10, + "scaleDistribution": { + "log": 10, + "type": "log" + }, + "showPoints": "never" + }, + "unit": "s" + } + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 10 + }, + "id": 5, + "interval": "1m", + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max" + ], + "displayMode": "table" + } + }, + "pluginVersion": "v11.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "expr": "sum by (pod) (\n rate(\n container_cpu_usage_seconds_total{\n container=\"envoy\"\n }\n [$__rate_interval])\n)\n", + "intervalFactor": 2, + "legendFormat": "{{pod}}\n" + } + ], + "title": "CPU Usage", + "type": "timeseries" }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "PBFA97CFB590B2093" - }, - "editorMode": "builder", - "expr": "sum by(pod) (rate(container_cpu_usage_seconds_total{container=\"envoy\"}[5m]))", - "instant": false, - "range": true, - "refId": "A" - } - ], - "title": "CPU Usage Envoy Pods", - "type": "timeseries" - } - ], - "refresh": "", - "schemaVersion": 39, - "tags": [ - "Data Plane" - ], - "templating": { - "list": [ { - "current": { - "selected": false, - "text": "Prometheus", - "value": "PBFA97CFB590B2093" - }, - "hide": 0, - "includeAll": false, - "multi": false, - "name": "datasource", - "options": [], - "query": "prometheus", - "refresh": 1, - "regex": "", - "skipUrlSync": false, - "type": "datasource" + "datasource": { + "type": "datasource", + "uid": "-- Mixed --" + }, + "fieldConfig": { + "defaults": { + "custom": { + "fillOpacity": 10, + "scaleDistribution": { + "log": 2, + "type": "log" + }, + "showPoints": "never" + }, + "unit": "bytes" + } + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 10 + }, + "id": 6, + "interval": "1m", + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max" + ], + "displayMode": "table" + } + }, + "pluginVersion": "v11.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "expr": "sum by (pod) (\n container_memory_working_set_bytes{container=\"envoy\"}\n)\n", + "intervalFactor": 2, + "legendFormat": "{{pod}}\n" + } + ], + "title": "Memory Usage", + "type": "timeseries" } - ] - }, - "time": { - "from": "now-5m", - "to": "now" - }, - "timeRangeUpdatedDuringEditOrView": false, - "timepicker": {}, - "timezone": "", - "title": "Envoy Pod Resources", - "uid": "f2279235-80b7-4c85-84f4-f25a3bf3eac0", - "version": 1, - "weekStart": "" + ], + "schemaVersion": 39, + "templating": { + "list": [ + { + "name": "datasource", + "query": "prometheus", + "type": "datasource" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timezone": "utc", + "title": "Resources Monitor", + "uid": "f7aeb41676b7865cf31ae49691325f91" } --- # Source: gateway-addons-helm/charts/fluent-bit/templates/clusterrole.yaml @@ -9317,7 +9048,7 @@ spec: dnsPolicy: ClusterFirst containers: - name: fluent-bit - image: "cr.fluentbit.io/fluent/fluent-bit:2.1.4" + image: "fluent/fluent-bit:2.1.4" imagePullPolicy: Always ports: - name: http diff --git a/test/benchmark/suite/client.go b/test/utils/kubernetes/client.go similarity index 71% rename from test/benchmark/suite/client.go rename to test/utils/kubernetes/client.go index 3db4c16c52d..e0052463e65 100644 --- a/test/benchmark/suite/client.go +++ b/test/utils/kubernetes/client.go @@ -3,17 +3,16 @@ // The full text of the Apache license is available in the LICENSE file at // the root of the repo. -//go:build benchmark -// +build benchmark - -package suite +package kubernetes import ( "testing" "github.com/stretchr/testify/require" batchv1 "k8s.io/api/batch/v1" + "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/config" gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" gwapiv1a3 "sigs.k8s.io/gateway-api/apis/v1alpha3" @@ -22,6 +21,19 @@ import ( egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" ) +func NewClient(t *testing.T) (client.Client, *rest.Config) { + cfg, err := config.GetConfig() + require.NoError(t, err) + + c, err := client.New(cfg, client.Options{}) + require.NoError(t, err) + + // Install all the scheme to kubernetes client. + CheckInstallScheme(t, c) + + return c, cfg +} + func CheckInstallScheme(t *testing.T, c client.Client) { require.NoError(t, gwapiv1a3.Install(c.Scheme())) require.NoError(t, gwapiv1a2.Install(c.Scheme())) diff --git a/test/e2e/utils/prometheus/prometheus.go b/test/utils/prometheus/prometheus.go similarity index 50% rename from test/e2e/utils/prometheus/prometheus.go rename to test/utils/prometheus/prometheus.go index 641f802609f..c59a8f12ebb 100644 --- a/test/e2e/utils/prometheus/prometheus.go +++ b/test/utils/prometheus/prometheus.go @@ -3,9 +3,6 @@ // The full text of the Apache license is available in the LICENSE file at // the root of the repo. -//go:build e2e -// +build e2e - package prometheus import ( @@ -21,27 +18,47 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -func Address(c client.Client, nn types.NamespacedName) (string, error) { +type Client struct { + prom.Client + + address string + name string + namespace string +} + +// NewClient returns a prometheus client based on the namespaced name of prometheus-server. +func NewClient(kubeClient client.Client, nn types.NamespacedName) (*Client, error) { svc := &corev1.Service{} - if err := c.Get(context.TODO(), nn, svc); err != nil { - return "", fmt.Errorf("failed to get service: %w", err) + if err := kubeClient.Get(context.Background(), nn, svc); err != nil { + return nil, fmt.Errorf("failed to get service %s: %w", nn.String(), err) } + + var addr string for _, ing := range svc.Status.LoadBalancer.Ingress { - if ing.IP != "" { - return fmt.Sprintf("http://%s", ing.IP), nil + if len(ing.IP) > 0 { + addr = fmt.Sprintf("http://%s", ing.IP) } } - return "", fmt.Errorf("no ingress found") -} + if len(addr) == 0 { + return nil, fmt.Errorf("no ingress found for %s", nn.String()) + } -func RawQuery(address string, promQL string) (model.Value, error) { - c, err := prom.NewClient(prom.Config{Address: address}) + c, err := prom.NewClient(prom.Config{Address: addr}) if err != nil { return nil, err } - v, _, err := prompapiv1.NewAPI(c).Query(context.Background(), promQL, time.Now()) + return &Client{ + Client: c, + address: addr, + name: nn.Name, + namespace: nn.Namespace, + }, nil +} + +func (c *Client) RawQuery(ctx context.Context, promQL string) (model.Value, error) { + v, _, err := prompapiv1.NewAPI(c.Client).Query(ctx, promQL, time.Now()) if err != nil { return nil, err } @@ -55,17 +72,17 @@ func RawQuery(address string, promQL string) (model.Value, error) { return nil, fmt.Errorf("value not found (query: %v)", promQL) } return v, nil - default: - return nil, fmt.Errorf("unhandled value type: %v", v.Type()) + return nil, fmt.Errorf("unsupported value type: %v", v.Type()) } } -func QuerySum(address string, promQL string) (float64, error) { - val, err := RawQuery(address, promQL) +func (c *Client) QuerySum(ctx context.Context, promQL string) (float64, error) { + val, err := c.RawQuery(ctx, promQL) if err != nil { return 0, err } + got, err := sum(val) if err != nil { return 0, fmt.Errorf("could not find metric value: %w", err) @@ -73,6 +90,21 @@ func QuerySum(address string, promQL string) (float64, error) { return got, nil } +func (c *Client) QueryAvg(ctx context.Context, promQL string) (float64, error) { + val, err := c.RawQuery(ctx, promQL) + if err != nil { + return 0, err + } + + got, err := sum(val) + if err != nil { + return 0, fmt.Errorf("could not find metric value: %w", err) + } + + got /= float64(val.(model.Vector).Len()) + return got, nil +} + func sum(val model.Value) (float64, error) { if val.Type() != model.ValVector { return 0, fmt.Errorf("value not a model.Vector; was %s", val.Type().String()) diff --git a/tools/docker/envoy-gateway/Dockerfile b/tools/docker/envoy-gateway/Dockerfile index 79b85852c9c..9d018cbf0e7 100644 --- a/tools/docker/envoy-gateway/Dockerfile +++ b/tools/docker/envoy-gateway/Dockerfile @@ -4,7 +4,7 @@ RUN mkdir -p /var/lib/eg # Use distroless as minimal base image to package the manager binary # Refer to https://github.com/GoogleContainerTools/distroless for more details -FROM gcr.io/distroless/static:nonroot@sha256:e9ac71e2b8e279a8372741b7a0293afda17650d926900233ec3a7b2b7c22a246 +FROM gcr.io/distroless/static:nonroot@sha256:8dd8d3ca2cf283383304fd45a5c9c74d5f2cd9da8d3b077d720e264880077c65 ARG TARGETPLATFORM COPY $TARGETPLATFORM/envoy-gateway /usr/local/bin/ COPY --from=source --chown=65532:65532 /var/lib /var/lib diff --git a/tools/github-actions/setup-deps/action.yaml b/tools/github-actions/setup-deps/action.yaml index d69c611ab6e..16bd7db1c0c 100644 --- a/tools/github-actions/setup-deps/action.yaml +++ b/tools/github-actions/setup-deps/action.yaml @@ -4,7 +4,9 @@ description: Install host system dependencies runs: using: composite steps: - - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + - shell: bash + run: sudo apt-get install libbtrfs-dev -y + - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.1 with: go-version: 1.22.x cache: true diff --git a/tools/hack/create-cluster.sh b/tools/hack/create-cluster.sh index 992ee5def92..93160727cbf 100755 --- a/tools/hack/create-cluster.sh +++ b/tools/hack/create-cluster.sh @@ -24,6 +24,10 @@ for _ in $(seq 1 "${NUM_WORKERS}"); do done fi +## Check if kind cluster already exists. +if tools/bin/kind get clusters | grep -q "${CLUSTER_NAME}"; then + echo "Cluster ${CLUSTER_NAME} already exists." +else ## Create kind cluster. if [[ -z "${KIND_NODE_TAG}" ]]; then cat << EOF | tools/bin/kind create cluster --name "${CLUSTER_NAME}" --config - @@ -34,6 +38,8 @@ else ${KIND_CFG} EOF fi +fi + ## Install MetalLB. kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/"${METALLB_VERSION}"/config/manifests/metallb-native.yaml diff --git a/tools/linter/codespell/.codespell.skip b/tools/linter/codespell/.codespell.skip index 00240f7e100..c39a3ea1d9b 100644 --- a/tools/linter/codespell/.codespell.skip +++ b/tools/linter/codespell/.codespell.skip @@ -14,4 +14,6 @@ go.sum bin ./charts *.js -*/testdata/*.yaml \ No newline at end of file +*/testdata/*.yaml +./site/public/* +./site/node_modules/* diff --git a/tools/linter/yamllint/.yamllint b/tools/linter/yamllint/.yamllint index 4a9e282a263..43af358a3b0 100644 --- a/tools/linter/yamllint/.yamllint +++ b/tools/linter/yamllint/.yamllint @@ -9,6 +9,8 @@ ignore: | charts/gateway-addons-helm/ bin/install.yaml test/helm/ + examples/extension-server/charts/extension-server + site/node_modules/ rules: braces: diff --git a/tools/make/common.mk b/tools/make/common.mk index 3dd383e7ee4..4d5d42a7626 100644 --- a/tools/make/common.mk +++ b/tools/make/common.mk @@ -38,7 +38,7 @@ endif REV=$(shell git rev-parse --short HEAD) # Supported Platforms for building multiarch binaries. -PLATFORMS ?= darwin_amd64 darwin_arm64 linux_amd64 linux_arm64 +PLATFORMS ?= darwin_amd64 darwin_arm64 linux_amd64 linux_arm64 # Set a specific PLATFORM ifeq ($(origin PLATFORM), undefined) @@ -94,20 +94,20 @@ endef define USAGE_OPTIONS Options: - \033[36mBINS\033[0m + \033[36mBINS\033[0m The binaries to build. Default is all of cmd. This option is available when using: make build|build-multiarch Example: \033[36mmake build BINS="envoy-gateway"\033[0m - \033[36mIMAGES\033[0m + \033[36mIMAGES\033[0m Backend images to make. Default is all of cmds. This option is available when using: make image|image-multiarch|push|push-multiarch Example: \033[36mmake image-multiarch IMAGES="envoy-gateway"\033[0m - \033[36mPLATFORM\033[0m + \033[36mPLATFORM\033[0m The specified platform to build. This option is available when using: make build|image Example: \033[36mmake build BINS="envoy-gateway" PLATFORM="linux_amd64""\033[0m Supported Platforms: linux_amd64 linux_arm64 darwin_amd64 darwin_arm64 - \033[36mPLATFORMS\033[0m + \033[36mPLATFORMS\033[0m The multiple platforms to build. This option is available when using: make build-multiarch Example: \033[36mmake build-multiarch BINS="envoy-gateway" PLATFORMS="linux_amd64 linux_arm64"\033[0m @@ -119,7 +119,7 @@ export USAGE_OPTIONS .PHONY: generate generate: ## Generate go code from templates and tags -generate: kube-generate docs-api helm-generate go.generate +generate: kube-generate docs-api helm-generate go.generate release-notes-docs copy-current-release-docs ## help: Show this help info. .PHONY: help diff --git a/tools/make/docs.mk b/tools/make/docs.mk index a8c59c29999..9ceb9f73d20 100644 --- a/tools/make/docs.mk +++ b/tools/make/docs.mk @@ -1,20 +1,35 @@ DOCS_OUTPUT_DIR := site/public RELEASE_VERSIONS ?= $(foreach v,$(wildcard ${ROOT_DIR}/docs/*),$(notdir ${v})) +# TODO: github.com does not allow access too often, there are a lot of 429 errors +# find a way to remove github.com from ignore list +# TODO: example.com is not a valid domain, we should remove it from ignore list +# TODO: https://www.gnu.org/software/make became unstable, we should remove it from ignore list later +LINKINATOR_IGNORE := "github.com githubusercontent.com example.com github.io gnu.org _print" +CLEAN_NODE_MODULES ?= true ##@ Docs .PHONY: docs -docs: docs.clean helm-readme-gen docs-api ## Generate Envoy Gateway Docs Sources +docs: docs.clean helm-readme-gen docs-api copy-current-release-docs ## Generate Envoy Gateway Docs Sources @$(LOG_TARGET) cd $(ROOT_DIR)/site && npm install cd $(ROOT_DIR)/site && npm run build:production cp tools/hack/get-egctl.sh $(DOCS_OUTPUT_DIR) +.PHONY: copy-current-release-docs +copy-current-release-docs: ## Copy the current release docs to the docs folder + @$(LOG_TARGET) + @CURRENT_RELEASE=$(shell ls $(ROOT_DIR)/site/content/en | grep -E '^v[0-9]+\.[0-9]+$$' | sort | tail -n 1); \ + echo "Copying the current release $$CURRENT_RELEASE docs to the docs folder"; \ + rm -rf $(ROOT_DIR)/site/content/en/docs; \ + mkdir -p $(ROOT_DIR)/site/content/en/docs; \ + cp -r $(ROOT_DIR)/site/content/en/$$CURRENT_RELEASE/** $(ROOT_DIR)/site/content/en/docs + .PHONY: docs-release docs-release: docs-release-prepare release-notes-docs docs-release-gen docs ## Generate Envoy Gateway Release Docs .PHONY: docs-serve -docs-serve: ## Start Envoy Gateway Site Locally +docs-serve: copy-current-release-docs ## Start Envoy Gateway Site Locally @$(LOG_TARGET) cd $(ROOT_DIR)/site && npm run serve @@ -26,7 +41,9 @@ clean: docs.clean docs.clean: @$(LOG_TARGET) rm -rf $(DOCS_OUTPUT_DIR) +ifeq ($(CLEAN_NODE_MODULES),true) rm -rf site/node_modules +endif rm -rf site/resources rm -f site/package-lock.json rm -f site/.hugo_build.lock @@ -88,8 +105,9 @@ docs-release-prepare: .PHONY: docs-release-gen docs-release-gen: @$(LOG_TARGET) - @$(call log, "Added Release Doc: site/content/en/$(TAG)") - cp -r site/content/en/latest site/content/en/$(TAG) + $(eval DOC_VERSION := $(shell cat VERSION | cut -d "." -f 1,2)) + @$(call log, "Added Release Doc: site/content/en/$(DOC_VERSION)") + cp -r site/content/en/latest/ site/content/en/$(DOC_VERSION)/ @for DOC in $(shell ls site/content/en/latest/user); do \ cp site/content/en/$(TAG)/user/$$DOC $(OUTPUT_DIR)/$$DOC ; \ cat $(OUTPUT_DIR)/$$DOC | sed "s;v0.0.0-latest;$(TAG);g" | sed "s;latest;$(TAG);g" > $(OUTPUT_DIR)/$(TAG)-$$DOC ; \ @@ -98,18 +116,16 @@ docs-release-gen: done @echo '[[params.versions]]' >> site/hugo.toml - @echo ' version = "$(TAG)"' >> site/hugo.toml - @echo ' url = "/$(TAG)"' >> site/hugo.toml + @echo ' version = "$(DOC_VERSION)"' >> site/hugo.toml + @echo ' url = "/$(DOC_VERSION)"' >> site/hugo.toml .PHONY: docs-check-links -docs-check-links: +docs-check-links: # Check for broken links in the docs @$(LOG_TARGET) - # Check for broken links, right now we are focusing on the v1.0.0 - # github.com does not allow access too often, there are a lot of 429 errors - # TODO: find a way to remove github.com from ignore list - # TODO: example.com is not a valid domain, we should remove it from ignore list - linkinator site/public/ -r --concurrency 25 --skip "github.com example.com github.io _print v0.6.0 v0.5.0 v0.4.0 v0.3.0 v0.2.0" + linkinator site/public/ -r --concurrency 25 --skip $(LINKINATOR_IGNORE) release-notes-docs: $(tools/release-notes-docs) @$(LOG_TARGET) - $(tools/release-notes-docs) release-notes/$(TAG).yaml site/content/en/latest/releases/; \ + @for file in $(wildcard release-notes/*.yaml); do \ + $(tools/release-notes-docs) $$file site/content/en/news/releases/notes; \ + done diff --git a/tools/make/golang.mk b/tools/make/golang.mk index f4fa9573c81..4c0d38bf83e 100644 --- a/tools/make/golang.mk +++ b/tools/make/golang.mk @@ -75,11 +75,17 @@ go.clean: ## Clean the building output files @$(LOG_TARGET) rm -rf $(OUTPUT_DIR) +.PHONY: go.mod.tidy +go.mod.tidy: ## Update and check dependences with go mod tidy. + @$(LOG_TARGET) + go mod tidy -compat=$(GO_VERSION) + # run go mod tidy in examples/extension-server directory + cd examples/extension-server && go mod tidy -compat=$(GO_VERSION) + .PHONY: go.mod.lint lint: go.mod.lint -go.mod.lint: +go.mod.lint: go.mod.tidy ## Check if go.mod is clean @$(LOG_TARGET) - @go mod tidy -compat=$(GO_VERSION) @if test -n "$$(git status -s -- go.mod go.sum)"; then \ git diff --exit-code go.mod; \ git diff --exit-code go.sum; \ diff --git a/tools/make/helm.mk b/tools/make/helm.mk index 94d2b9c3f93..88852e0deb4 100644 --- a/tools/make/helm.mk +++ b/tools/make/helm.mk @@ -48,7 +48,7 @@ helm-generate: done .PHONY: helm-generate.% -helm-generate.%: +helm-generate.%: $(tools/jsonnet) $(tools/jb) $(eval COMMAND := $(word 1,$(subst ., ,$*))) $(eval CHART_NAME := $(COMMAND)) @if test -f "charts/${CHART_NAME}/values.tmpl.yaml"; then \ @@ -57,6 +57,18 @@ helm-generate.%: fi helm dependency update charts/${CHART_NAME} helm lint charts/${CHART_NAME} + + # The jb does not support self-assigned jsonnetfile, so entering working dir before executing jb. + @if [ ${CHART_NAME} == "gateway-addons-helm" ]; then \ + $(call log, "Run jsonnet generate for dashboards in chart: ${CHART_NAME}!"); \ + workDir="charts/${CHART_NAME}/dashboards"; \ + cd $$workDir && ../../../$(tools/jb) install && cd ../../..; \ + for file in $$(find $${workDir} -maxdepth 1 -name '*.libsonnet'); do \ + name=$$(basename $$file .libsonnet); \ + $(tools/jsonnet) -J $${workDir}/vendor $${workDir}/$${name}.libsonnet > $${workDir}/$${name}.gen.json; \ + done \ + fi + $(call log, "Run helm template for chart: ${CHART_NAME}!"); @for file in $(wildcard test/helm/${CHART_NAME}/*.in.yaml); do \ filename=$$(basename $${file}); \ diff --git a/tools/make/kube.mk b/tools/make/kube.mk index ddbe3fdd5d1..354781bd9fd 100644 --- a/tools/make/kube.mk +++ b/tools/make/kube.mk @@ -11,15 +11,16 @@ GATEWAY_RELEASE_URL ?= https://github.com/kubernetes-sigs/gateway-api/releases/d WAIT_TIMEOUT ?= 15m BENCHMARK_TIMEOUT ?= 60m -BENCHMARK_CPU_LIMITS ?= 1000 # unit: 'm' -BENCHMARK_MEMORY_LIMITS ?= 1024 # unit: 'Mi' +BENCHMARK_CPU_LIMITS ?= 1000m +BENCHMARK_MEMORY_LIMITS ?= 1024Mi BENCHMARK_RPS ?= 10000 BENCHMARK_CONNECTIONS ?= 100 BENCHMARK_DURATION ?= 60 +BENCHMARK_REPORT_DIR ?= benchmark_report E2E_RUN_TEST ?= -E2E_RUN_EG_UPGRADE_TESTS ?= false E2E_CLEANUP ?= true +E2E_TEST_ARGS ?= -v -tags e2e -timeout 15m # Set Kubernetes Resources Directory Path ifeq ($(origin KUBE_PROVIDER_DIR),undefined) @@ -39,7 +40,7 @@ CONTROLLERGEN_OBJECT_FLAGS := object:headerFile="$(ROOT_DIR)/tools/boilerplate/ .PHONY: manifests manifests: $(tools/controller-gen) generate-gwapi-manifests ## Generate WebhookConfiguration and CustomResourceDefinition objects. @$(LOG_TARGET) - $(tools/controller-gen) crd:allowDangerousTypes=true paths="./..." output:crd:artifacts:config=charts/gateway-helm/crds/generated + $(tools/controller-gen) crd:allowDangerousTypes=true paths="./api/..." output:crd:artifacts:config=charts/gateway-helm/crds/generated .PHONY: generate-gwapi-manifests generate-gwapi-manifests: @@ -72,12 +73,21 @@ kube-deploy: manifests helm-generate.gateway-helm ## Install Envoy Gateway into helm install eg charts/gateway-helm --set deployment.envoyGateway.imagePullPolicy=$(IMAGE_PULL_POLICY) -n envoy-gateway-system --create-namespace --debug --timeout='$(WAIT_TIMEOUT)' --wait --wait-for-jobs .PHONY: kube-deploy-for-benchmark-test -kube-deploy-for-benchmark-test: manifests helm-generate ## Install Envoy Gateway for benchmark test purpose only. +kube-deploy-for-benchmark-test: manifests helm-generate ## Install Envoy Gateway and prometheus-server for benchmark test purpose only. @$(LOG_TARGET) + # Install Envoy Gateway helm install eg charts/gateway-helm --set deployment.envoyGateway.imagePullPolicy=$(IMAGE_PULL_POLICY) \ - --set deployment.envoyGateway.resources.limits.cpu=$(BENCHMARK_CPU_LIMITS)m \ - --set deployment.envoyGateway.resources.limits.memory=$(BENCHMARK_MEMORY_LIMITS)Mi \ + --set deployment.envoyGateway.resources.limits.cpu=$(BENCHMARK_CPU_LIMITS) \ + --set deployment.envoyGateway.resources.limits.memory=$(BENCHMARK_MEMORY_LIMITS) \ -n envoy-gateway-system --create-namespace --debug --timeout='$(WAIT_TIMEOUT)' --wait --wait-for-jobs + # Install Prometheus-server only + helm install eg-addons charts/gateway-addons-helm --set loki.enabled=false \ + --set tempo.enabled=false \ + --set grafana.enabled=false \ + --set fluent-bit.enabled=false \ + --set opentelemetry-collector.enabled=false \ + --set prometheus.enabled=true \ + -n monitoring --create-namespace --debug --timeout='$(WAIT_TIMEOUT)' --wait --wait-for-jobs .PHONY: kube-undeploy kube-undeploy: manifests ## Uninstall the Envoy Gateway into the Kubernetes cluster specified in ~/.kube/config. @@ -119,7 +129,7 @@ experimental-conformance: create-cluster kube-install-image kube-deploy run-expe benchmark: create-cluster kube-install-image kube-deploy-for-benchmark-test run-benchmark delete-cluster ## Create a kind cluster, deploy EG into it, run Envoy Gateway benchmark test, and clean up. .PHONY: e2e -e2e: create-cluster kube-install-image kube-deploy install-ratelimit run-e2e delete-cluster +e2e: create-cluster kube-install-image kube-deploy install-ratelimit install-e2e-telemetry run-e2e delete-cluster .PHONY: install-ratelimit install-ratelimit: @@ -131,29 +141,24 @@ install-ratelimit: tools/hack/deployment-exists.sh "app.kubernetes.io/name=envoy-ratelimit" "envoy-gateway-system" kubectl wait --timeout=5m -n envoy-gateway-system deployment/envoy-ratelimit --for=condition=Available -.PHONY: run-e2e -run-e2e: install-e2e-telemetry +.PHONY: e2e-prepare +e2e-prepare: ## Prepare the environment for running e2e tests @$(LOG_TARGET) kubectl wait --timeout=5m -n envoy-gateway-system deployment/envoy-ratelimit --for=condition=Available kubectl wait --timeout=5m -n envoy-gateway-system deployment/envoy-gateway --for=condition=Available kubectl apply -f test/config/gatewayclass.yaml + +.PHONY: run-e2e +run-e2e: e2e-prepare ## Run e2e tests + @$(LOG_TARGET) ifeq ($(E2E_RUN_TEST),) - go test -v -tags e2e ./test/e2e --gateway-class=envoy-gateway --debug=true --cleanup-base-resources=false - go test -v -tags e2e ./test/e2e/merge_gateways --gateway-class=merge-gateways --debug=true --cleanup-base-resources=false - go test -v -tags e2e ./test/e2e/multiple_gc --debug=true --cleanup-base-resources=false - go test -v -tags e2e ./test/e2e/upgrade --gateway-class=upgrade --debug=true --cleanup-base-resources=$(E2E_CLEANUP) + go test $(E2E_TEST_ARGS) ./test/e2e --gateway-class=envoy-gateway --debug=true --cleanup-base-resources=false + go test $(E2E_TEST_ARGS) ./test/e2e/merge_gateways --gateway-class=merge-gateways --debug=true --cleanup-base-resources=false + go test $(E2E_TEST_ARGS) ./test/e2e/multiple_gc --debug=true --cleanup-base-resources=true + go test $(E2E_TEST_ARGS) ./test/e2e/upgrade --gateway-class=upgrade --debug=true --cleanup-base-resources=$(E2E_CLEANUP) else -ifeq ($(E2E_RUN_EG_UPGRADE_TESTS),false) - go test -v -tags e2e ./test/e2e/merge_gateways --gateway-class=merge-gateways --debug=true --cleanup-base-resources=false \ - --run-test $(E2E_RUN_TEST) - go test -v -tags e2e ./test/e2e --gateway-class=envoy-gateway --debug=true --cleanup-base-resources=$(E2E_CLEANUP) \ + go test $(E2E_TEST_ARGS) ./test/e2e --gateway-class=envoy-gateway --debug=true --cleanup-base-resources=$(E2E_CLEANUP) \ --run-test $(E2E_RUN_TEST) - go test -v -tags e2e ./test/e2e/multiple_gc --debug=true --cleanup-base-resources=$(E2E_CLEANUP) \ - --run-test $(E2E_RUN_TEST) -else - go test -v -tags e2e ./test/e2e/upgrade --gateway-class=upgrade --debug=true --cleanup-base-resources=$(E2E_CLEANUP) \ - --run-test $(E2E_RUN_TEST) -endif endif .PHONY: run-benchmark @@ -163,7 +168,7 @@ run-benchmark: install-benchmark-server ## Run benchmark tests kubectl wait --timeout=$(WAIT_TIMEOUT) -n benchmark-test deployment/nighthawk-test-server --for=condition=Available kubectl wait --timeout=$(WAIT_TIMEOUT) -n envoy-gateway-system deployment/envoy-gateway --for=condition=Available kubectl apply -f test/benchmark/config/gatewayclass.yaml - go test -v -tags benchmark -timeout $(BENCHMARK_TIMEOUT) ./test/benchmark --rps=$(BENCHMARK_RPS) --connections=$(BENCHMARK_CONNECTIONS) --duration=$(BENCHMARK_DURATION) --report-save-path=benchmark_report.md + go test -v -tags benchmark -timeout $(BENCHMARK_TIMEOUT) ./test/benchmark --rps=$(BENCHMARK_RPS) --connections=$(BENCHMARK_CONNECTIONS) --duration=$(BENCHMARK_DURATION) --report-save-dir=$(BENCHMARK_REPORT_DIR) .PHONY: install-benchmark-server install-benchmark-server: ## Install nighthawk server for benchmark test @@ -186,6 +191,13 @@ install-e2e-telemetry: helm-generate.gateway-addons-helm helm upgrade -i eg-addons charts/gateway-addons-helm --set grafana.enabled=false,opentelemetry-collector.enabled=true -n monitoring --create-namespace --timeout='$(WAIT_TIMEOUT)' --wait --wait-for-jobs # Change loki service type from ClusterIP to LoadBalancer kubectl patch service loki -n monitoring -p '{"spec": {"type": "LoadBalancer"}}' + # Wait service Ready + kubectl rollout status --watch --timeout=5m -n monitoring deployment/prometheus + kubectl rollout status --watch --timeout=5m statefulset/loki -n monitoring + kubectl rollout status --watch --timeout=5m statefulset/tempo -n monitoring + # Restart otel-collector to make sure otlp exporter worked + kubectl rollout restart -n monitoring deployment/otel-collector + kubectl rollout status --watch --timeout=5m -n monitoring deployment/otel-collector .PHONY: uninstall-e2e-telemetry uninstall-e2e-telemetry: diff --git a/tools/make/lint.mk b/tools/make/lint.mk index 10a7dbd02ec..8f9b306d05d 100644 --- a/tools/make/lint.mk +++ b/tools/make/lint.mk @@ -15,7 +15,7 @@ lint: ## Run all linter of code sources, including golint, yamllint, whitenoise .PHONY: lint-deps lint-deps: ## Everything necessary to lint -GOLANGCI_LINT_FLAGS ?= $(if $(GITHUB_ACTION),--out-format=github-actions) +GOLANGCI_LINT_FLAGS ?= $(if $(GITHUB_ACTION),--out-format=colored-line-number) .PHONY: lint.golint lint: lint.golint lint-deps: $(tools/golangci-lint) @@ -69,8 +69,15 @@ lint.shellcheck: $(tools/shellcheck) @$(LOG_TARGET) $(tools/shellcheck) tools/hack/*.sh +.PHONY: lint.fix-golint +lint-deps: $(tools/gci) +lint.fix-golint: ## Run all linter of code sources and fix the issues. + @$(LOG_TARGET) + $(MAKE) lint.golint GOLANGCI_LINT_FLAGS="--fix" + find . -name "*.go" | xargs $(tools/gci) write --skip-generated -s Standard -s Default -s "Prefix(github.com/envoyproxy/gateway)" + .PHONY: gen-check -gen-check: generate manifests protos go.testdata.complete +gen-check: format generate manifests protos go.testdata.complete @$(LOG_TARGET) @if [ ! -z "`git status --porcelain`" ]; then \ $(call errorlog, ERROR: Some files need to be updated, please run 'make generate', 'make manifests' and 'make protos' to include any changed files to your PR); \ diff --git a/tools/make/tools.mk b/tools/make/tools.mk index 4943764a972..6fee3e59b73 100644 --- a/tools/make/tools.mk +++ b/tools/make/tools.mk @@ -14,6 +14,7 @@ $(tools.bindir)/%: $(tools.srcdir)/%.sh # tools/controller-gen = $(tools.bindir)/controller-gen tools/golangci-lint = $(tools.bindir)/golangci-lint +tools/gci = $(tools.bindir)/gci tools/kustomize = $(tools.bindir)/kustomize tools/kind = $(tools.bindir)/kind tools/setup-envtest = $(tools.bindir)/setup-envtest @@ -22,6 +23,8 @@ tools/buf = $(tools.bindir)/buf tools/protoc-gen-go = $(tools.bindir)/protoc-gen-go tools/protoc-gen-go-grpc = $(tools.bindir)/protoc-gen-go-grpc tools/helm-docs = $(tools.bindir)/helm-docs +tools/jsonnet = $(tools.bindir)/jsonnet +tools/jb = $(tools.bindir)/jb $(tools.bindir)/%: $(tools.srcdir)/%/pin.go $(tools.srcdir)/%/go.mod cd $(