diff --git a/.github/markdown_lint_config.json b/.github/markdown_lint_config.json new file mode 100644 index 00000000000..4550512017d --- /dev/null +++ b/.github/markdown_lint_config.json @@ -0,0 +1,52 @@ +{ + "MD001": true, + "MD002": false, + "MD003": false, + "MD004": false, + "MD005": false, + "MD006": false, + "MD007": false, + "MD008": false, + "MD009": false, + "MD010": false, + "MD011": false, + "MD012": false, + "MD013": false, + "MD014": false, + "MD015": false, + "MD016": false, + "MD017": false, + "MD018": false, + "MD019": false, + "MD020": false, + "MD021": false, + "MD022": false, + "MD023": false, + "MD024": false, + "MD025": false, + "MD026": false, + "MD027": false, + "MD028": false, + "MD029": false, + "MD030": false, + "MD031": true, + "MD032": false, + "MD033": false, + "MD034": false, + "MD035": false, + "MD036": false, + "MD037": true, + "MD038": true, + "MD039": false, + "MD040": false, + "MD041": false, + "MD042": false, + "MD043": false, + "MD044": false, + "MD045": false, + "MD046": false, + "MD047": false, + "MD048": false, + "MD049": false, + "MD050": false +} diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml index b4586c3cada..d0976ede653 100644 --- a/.github/workflows/build_and_test.yaml +++ b/.github/workflows/build_and_test.yaml @@ -3,13 +3,13 @@ on: push: branches: - "main" - - "release-v*" + - "release/v*" paths-ignore: - "**/*.png" pull_request: branches: - "main" - - "release-v*" + - "release/v*" paths-ignore: - "**/*.png" jobs: @@ -28,16 +28,25 @@ jobs: - uses: ./tools/github-actions/setup-deps - run: make -k gen-check + license-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: ./tools/github-actions/setup-deps + - run: make -k licensecheck + build-and-test: runs-on: ubuntu-latest + needs: [lint, gen-check, license-check] steps: - uses: actions/checkout@v3 - uses: ./tools/github-actions/setup-deps # test - - run: make go.test.coverage + - name: Run Coverage Tests + run: make go.test.coverage - name: Upload coverage to Codecov - uses: codecov/codecov-action@v2 + uses: codecov/codecov-action@v3 with: fail_ci_if_error: true files: ./coverage.xml @@ -45,10 +54,26 @@ jobs: verbose: true # build - - run: make build-multiarch + - name: Build Multiarch EG Binaries + run: make build-multiarch PLATFORMS="linux_amd64 linux_arm64" # conformance - - name: Run Conformance Tests + - name: Run Conformance Tests (v1.24.0) + env: + KIND_NODE_TAG: v1.24.0 + CONFORMANCE_UNIQUE_PORTS: false + run: make conformance + + - name: Run Conformance Tests (v1.23.6) + env: + KIND_NODE_TAG: v1.23.6 + CONFORMANCE_UNIQUE_PORTS: false + run: make conformance + + - name: Run Conformance Tests (v1.22.9) + env: + KIND_NODE_TAG: v1.22.9 + CONFORMANCE_UNIQUE_PORTS: false run: make conformance # build and push image @@ -59,14 +84,15 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} - - run: make image.multiarch.setup + - name: Setup Multiarch Environment + run: make image.multiarch.setup - name: Build and Push EG Commit Image if: github.event_name == 'push' # tag is set to the short SHA of the commit - run: make image.push.multiarch PLATFORMS="linux_amd64 linux_arm64" + run: make image.push.multiarch PLATFORMS="linux_amd64 linux_arm64" IMAGE=envoyproxy/gateway-dev - name: Build and Push EG Latest Image if: github.event_name == 'push' && github.ref == 'refs/heads/main' # tag is set to `latest` when pushing to main branch - run: make image.push.multiarch TAG=latest PLATFORMS="linux_amd64 linux_arm64" + run: make image.push.multiarch TAG=latest PLATFORMS="linux_amd64 linux_arm64" IMAGE=envoyproxy/gateway-dev diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index f8ba90a3e68..bbc8d57b012 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -3,24 +3,37 @@ on: push: branches: - "main" - - "release-v*" paths-ignore: - "**/*.png" - # pull_request: - # branches: - # - "main" - # - "release-v*" - # paths-ignore: - # - "**/*.png" + pull_request: + branches: + - "main" + paths-ignore: + - "**/*.png" + jobs: + docs-lint: + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v3 + + - name: Run markdown linter + uses: nosborn/github-action-markdown-cli@v3.1.0 + with: + files: docs/* + config_file: ".github/markdown_lint_config.json" + docs-build: + if: github.event_name == 'push' runs-on: ubuntu-latest + needs: docs-lint steps: - uses: actions/checkout@v3 - uses: ./tools/github-actions/setup-deps - # docs - - run: make docs + - name: Generate EG Pages + run: make docs # Upload docs for GitHub Pages - name: Upload GitHub Pages artifact @@ -33,6 +46,7 @@ jobs: # This workflow contains a single job called "build" docs-publish: + if: github.event_name == 'push' runs-on: ubuntu-latest needs: docs-build diff --git a/.github/workflows/latest_release.yaml b/.github/workflows/latest_release.yaml new file mode 100644 index 00000000000..9488a5e480a --- /dev/null +++ b/.github/workflows/latest_release.yaml @@ -0,0 +1,51 @@ +name: Latest Release + +on: + push: + branches: + - "main" + paths-ignore: + - "**/*.png" + +jobs: + latest-release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Generate Release Manifests + run: make generate-manifests IMAGE=envoyproxy/gateway-dev TAG=latest OUTPUT_DIR=release-artifacts + + # Ignore the error when we delete the latest release, it might not exist. + - name: Delete the Latest Release + continue-on-error: true + run: | + gh release delete latest --repo $GITHUB_REPOSITORY + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository_owner }}/${{ github.event.repository.name }} + + # Ignore the error when we delete the latest tag, it might not exist. + - name: Delete the Latest Tag + continue-on-error: true + run: + gh api --method DELETE /repos/$GITHUB_REPOSITORY/git/refs/tags/latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository_owner }}/${{ github.event.repository.name }} + + - name: Recreate the Latest Release and Tag + uses: softprops/action-gh-release@v1 + with: + draft: false + prerelease: true + tag_name: latest + files: | + release-artifacts/install.yaml + release-artifacts/quickstart.yaml + body: | + This is the "latest" release of **Envoy Gateway**, which contains the most recent commits from the main branch. + + This release **might not be stable**. + + It is only intended for developers wishing to try out the latest features in Envoy Gateway, some of which may not be fully implemented. diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index ef0a318ffb0..efcc8ade072 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -15,8 +15,8 @@ jobs: id: vars shell: bash run: | - echo "::set-output name=release_tag::$(echo ${GITHUB_REF##*/})" - echo "::set-output name=sha_short::$(git rev-parse --short HEAD)" + echo "release_tag=$(echo ${GITHUB_REF##*/})" >> $GITHUB_ENV + echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_ENV - name: Login to DockerHub uses: docker/login-action@v2 @@ -25,16 +25,15 @@ jobs: password: ${{ secrets.DOCKERHUB_PASSWORD }} - name: Retag and push existing gateway-dev image run: | - skopeo copy --all docker://docker.io/envoyproxy/gateway-dev:${{ steps.vars.outputs.sha_short }} docker://docker.io/envoyproxy/gateway:${{ steps.vars.outputs.release_tag }} + skopeo copy --all docker://docker.io/envoyproxy/gateway-dev:${{ env.sha_short }} docker://docker.io/envoyproxy/gateway:${{ env.release_tag }} - - name: Generate Release Manifests - run: make release-manifests TAG=${{ steps.vars.outputs.release_tag}} + - name: Generate Release Artifacts + run: make generate-artifacts IMAGE=envoyproxy/gateway TAG=${{ env.release_tag}} OUTPUT_DIR=release-artifacts - name: Upload Release Manifests uses: softprops/action-gh-release@v1 with: files: | - release-artifacts/gatewayapi-crds.yaml release-artifacts/install.yaml - - + release-artifacts/quickstart.yaml + release-artifacts/release-notes.yaml diff --git a/.gitignore b/.gitignore index d8a1fb4f35e..24c1a01fee3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .DS_Store bin/ -/docs/html + +docs/html # Intellij *.iml @@ -15,3 +16,11 @@ release-artifacts/ # Outputs coverage.xml + +# `go mod vendor` +vendor/ + +# TLS assets that may have been created for secure gateways. +*.crt +*.csr +*.key diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000000..973f62f91c9 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,46 @@ +run: + deadline: 10m + +linters: + enable: + - bodyclose + - gofmt + - goimports + - revive + - gosec + - misspell + - scopelint + - unconvert + - unparam + - goheader + - gocritic + +linters-settings: + gofmt: + simplify: true + unparam: + check-exported: false + goheader: + # Note that because the format is different (this needs no comment markers), + # updating this text means also updating /tools/boilerplate.txt so that + # `make generate` will update the generated files correctly. + template: |- + 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. + +issues: + exclude-rules: + - path: zz_generated + linters: + - goimports + - linters: + - staticcheck + text: "SA1019:" + - path: test/e2e + linters: + - bodyclose + # Show the complete output + max-issues-per-linter: 0 + max-same-issues: 0 diff --git a/CODEOWNERS.md b/CODEOWNERS.md index 3051c49994a..4f7b014ddd7 100644 --- a/CODEOWNERS.md +++ b/CODEOWNERS.md @@ -1,3 +1,3 @@ # The following owners, listed in alphabetical order, own everything # in the repo. -* @alexgervais @arkodg @danehans @LukeShu @skriss @youngnick +* @AliceProxy @arkodg @danehans @LukeShu @skriss @Xunzhuo @youngnick diff --git a/DEVELOPER.md b/DEVELOPER.md deleted file mode 100644 index fb5639e483c..00000000000 --- a/DEVELOPER.md +++ /dev/null @@ -1,87 +0,0 @@ -# Developer documentation - -Envoy Gateway is built using a [make][make]-based build system. Our CI is based on [Github Actions][gha] -(see: [workflows](.github/workflows)). - -## Prerequisites - -### go -* Version: 1.18.2 -* 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. - -## Quick start -* Run `make help` to see all the available targets to build, test and run `envoy-gateway`. - -### Building the `envoy-gateway` binary -* Run `make build` to build the binary that gets generated in the `bin/` directory - -### Running tests -* Run `make test` to run the golang tests. - -### Running code linters -* Run `make lint` to make sure your code passes all the linter checks. - -### 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. - -### Creating a Kind Cluster to deploy Envoy Gateway -* Run `make create-cluster` to create a [Kind][kind] cluster. -* Run `make kube-install-image` to build an image and load it into the Kind cluster. - -**_NOTE:_** Envoy Gateway is tested against Kubernetes v1.24.0. - -### Deploying Envoy Gateway in Kubernetes -* Run `IMAGE=envoyproxy/gateway-dev TAG=latest make kube-deploy` to deploy Envoy Gateway resources, including the Gateway API CRDs, -with the `envoyproxy/gateway-dev:latest` Envoy Gateway image into a Kubernetes cluster (linked to the current kube context). -* Run `make kube-undeploy` to delete the resources from the cluster created using `kube-deploy`. - -**_NOTE:_** Replace `IMAGE` with your registry's image name. - -### Configure a demo setup -* Run `make kube-demo` to deploy a demo backend service, gatewayclass, gateway and httproute resource -(similar to steps outlined in the [Quickstart](https://github.com/envoyproxy/gateway/blob/main/docs/user/QUICKSTART.md) 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 -* Run `make conformance` to run Gateway API Conformance tests using `envoy-gateway` in a -local Kind cluster. Go [here](https://gateway-api.sigs.k8s.io/concepts/conformance/) to learn -more about the tests. - -**_NOTE:_** Conformance tests against a kind cluster is currently unsupported on Mac computers. -As a workaround, you could run this against your own Kubernetes cluster (such as Kubernetes on Docker Desktop) using this command - -`IMAGE=docker.io/you/gateway-dev make push-multiarch && IMAGE=docker.io/you/gateway-dev make kube-deploy && make run-conformance` -which builds and pushes the Envoy-Gateway image to your hub, deploys Envoy Gateway resources into your cluster -and runs the Gateway API conformance tests. - -### 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 Gateway deployment so that it can be accessed locally. - -`kubectl port-forward deploy/envoy-default-eg -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](https://www.envoyproxy.io/docs/envoy/v1.23.0/operations/admin#operations-admin-interface) that may be helpful when debugging. - -[make]: https://www.gnu.org/software/make/ -[gha]: https://docs.github.com/en/actions -[kind]: https://kind.sigs.k8s.io/ diff --git a/DOCS.md b/DOCS.md deleted file mode 100644 index 4f9eb8a412d..00000000000 --- a/DOCS.md +++ /dev/null @@ -1,43 +0,0 @@ -# Working on the Envoy Gateway Docs - -The documentation for the Envoy Gateway lives in the `docs/` directory. It's -written using [reStructuredText], so you'll need a working familiarity with -that! - -## Documentation Structure - -The root of the site is in `docs/index.rst`. This is probably where to start -if you're trying to understand how things fit together. - -It's important to note that a given document _must_ have a reference in some -`.. toctree::` section for the document to be reachable. Not everything needs -to be in `docs/index.rst`'s `toctree` though. - -## Documentation Workflow - -To work with the docs, just edit reStructuredText files in `docs`, then run - -```bash -make docs -``` - -This will create `docs/html` with the built HTML pages. You can view the docs -either simply by pointing a web browser at the `file://` path to your -`docs/html`, or by firing up a static webserver from that directory, e.g. - -``` -cd docs/html ; python3 -m http.server -``` - -### What About Markdown Files? - -There are currently still some pre-RST Markdown files in the `docs/` directory. -Those should be turned into RST files and brought into the brave new world. - -## Publishing Docs - -Whenever docs are pushed to `main`, CI will publish the built docs to GitHub -Pages. For more details, see `.github/workflows/docs.yaml`. - -[reStructuredText]: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html - diff --git a/README.md b/README.md index cc9ecb13788..d750c980ba1 100644 --- a/README.md +++ b/README.md @@ -10,19 +10,19 @@ Kubernetes-based application gateway. * [Blog][blog] introducing Envoy Gateway. * [Goals](GOALS.md) -* [Quickstart](./docs/user/QUICKSTART.md) to use Envoy Gateway in a few simple steps. -* [Roadmap](./docs/design/ROADMAP.md) +* [Quickstart](./docs/latest/user/quickstart.md) to use Envoy Gateway in a few simple steps. +* [Roadmap](./docs/latest/design/roadmap.md) ## Contact -* Slack: Go to [Envoy Gateway Channel](https://envoyproxy.slack.com/archives/C03E6NHLESV). -* [Google Group][group]: Envoy Gateway developer discussion (APIs, feature design, etc.). +* Slack: Join the [Envoy Slack workspace][] if you're not already a member. Otherwise, use the + [Envoy Gateway channel][] to start collaborating with the community. ## Contributing -* [Code of conduct](CODE_OF_CONDUCT.md) -* [Contributing guide](CONTRIBUTING.md) -* [Developer guide](DEVELOPER.md) +* [Code of conduct](./docs/latest/dev/CODE_OF_CONDUCT.md) +* [Contributing guide](./docs/latest/dev/CONTRIBUTING.md) +* [Developer guide](docs/latest/dev/README.md) ## Community Meeting @@ -33,3 +33,5 @@ The Envoy Gateway team meets every Tuesday and Thursday. Refer to the meeting de [meeting]: https://docs.google.com/document/d/1leqwsHX8N-XxNEyTflYjRur462ukFxd19Rnk3Uzy55I/edit?usp=sharing [group]: https://groups.google.com/forum/#!forum/envoy-gateway-developers [blog]: https://blog.envoyproxy.io/introducing-envoy-gateway-ad385cc59532 +[Envoy Slack workspace]: https://communityinviter.com/apps/envoyproxy/envoy +[Envoy Gateway channel]: https://envoyproxy.slack.com/archives/C03E6NHLESV diff --git a/VERSION b/VERSION new file mode 100644 index 00000000000..1474d00f01c --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +v0.2.0 diff --git a/api/config/v1alpha1/envoygateway_types.go b/api/config/v1alpha1/envoygateway_types.go index f9f742b24b8..08c24edafd5 100644 --- a/api/config/v1alpha1/envoygateway_types.go +++ b/api/config/v1alpha1/envoygateway_types.go @@ -1,3 +1,8 @@ +// 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 v1alpha1 import ( diff --git a/api/config/v1alpha1/envoyproxy_types.go b/api/config/v1alpha1/envoyproxy_types.go index edaaa2aa30f..2899b7c072b 100644 --- a/api/config/v1alpha1/envoyproxy_types.go +++ b/api/config/v1alpha1/envoyproxy_types.go @@ -1,3 +1,8 @@ +// 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 v1alpha1 import ( diff --git a/api/config/v1alpha1/groupversion_info.go b/api/config/v1alpha1/groupversion_info.go index 992e6bd3162..9a4070f6231 100644 --- a/api/config/v1alpha1/groupversion_info.go +++ b/api/config/v1alpha1/groupversion_info.go @@ -1,3 +1,8 @@ +// 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 v1alpha1 contains API Schema definitions for the config v1alpha1 API group. // //+kubebuilder:object:generate=true diff --git a/api/config/v1alpha1/helpers.go b/api/config/v1alpha1/helpers.go index d3578fed74c..6b1fa4c5517 100644 --- a/api/config/v1alpha1/helpers.go +++ b/api/config/v1alpha1/helpers.go @@ -1,3 +1,8 @@ +// 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 v1alpha1 import ( diff --git a/api/config/v1alpha1/zz_generated.deepcopy.go b/api/config/v1alpha1/zz_generated.deepcopy.go index bc0ebd8c291..96aaf745227 100644 --- a/api/config/v1alpha1/zz_generated.deepcopy.go +++ b/api/config/v1alpha1/zz_generated.deepcopy.go @@ -1,6 +1,11 @@ //go:build !ignore_autogenerated // +build !ignore_autogenerated +// 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. + // Code generated by controller-gen. DO NOT EDIT. package v1alpha1 diff --git a/cmd/envoy-gateway/main.go b/cmd/envoy-gateway/main.go index 63a30e0885b..f817151187a 100644 --- a/cmd/envoy-gateway/main.go +++ b/cmd/envoy-gateway/main.go @@ -1,16 +1,7 @@ -// Copyright The Envoy Project Authors - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at - -// http://www.apache.org/licenses/LICENSE-2.0 - -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// 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 main diff --git a/docs/about_docs.rst b/docs/about_docs.rst deleted file mode 100644 index 7ca351e03ef..00000000000 --- a/docs/about_docs.rst +++ /dev/null @@ -1,5 +0,0 @@ -About the documentation -======================= - -The Envoy Gateway documentation is **VERY MUCH A WORK IN PROGRESS**. - diff --git a/docs/design/gatewayapi-translator.md b/docs/design/gatewayapi-translator.md deleted file mode 100644 index b79abc314ea..00000000000 --- a/docs/design/gatewayapi-translator.md +++ /dev/null @@ -1,242 +0,0 @@ -# Gateway API Translator Design - -## Assumptions -- initially target core conformance features only, to be followed by extended conformance features - -## Inputs and Outputs - -The main inputs to the Gateway API translator are: -- the GatewayClass to process -- Gateways, HTTPRoutes, Services, Secrets - -The outputs of the Gateway API translator are: -- IR -- status updates for GatewayClass, Gateways, HTTPRoutes - -## Listener Compatibility -Since Envoy Gateway handles all Gateways for a given GatewayClass, we need to determine the compatibility of _all_ Listeners across _all_ of those Gateways. - -The rules are: -- for a given port number, every Listener using that port number must have a compatible protocol (either all HTTP, or all HTTPS/TLS). -- for a given port number, every Listener using that port number must have a distinct hostname (at most one Listener per port can have no hostname). - -Listeners sharing a port that are not mutually compatible will be marked as "Conflicted: true" with an appropriate reason. - -### Listener Compatibility Examples - -#### Example 1: Gateways with compatible Listeners (same port & protocol, different hostnames) - -```yaml -kind: Gateway -apiVersion: gateway.networking.k8s.io/v1beta1 -metadata: - name: gateway-1 - namespace: envoy-gateway -spec: - gatewayClassName: envoy-gateway - listeners: - - name: http - protocol: HTTP - port: 80 - allowedRoutes: - namespaces: - from: All - hostname: *.envoygateway.io ---- -kind: Gateway -apiVersion: gateway.networking.k8s.io/v1beta1 -metadata: - name: gateway-2 - namespace: envoy-gateway -spec: - gatewayClassName: envoy-gateway - listeners: - - name: http - protocol: HTTP - port: 80 - allowedRoutes: - namespaces: - from: All - hostname: whales.envoygateway.io -``` - -#### Example 2: Gateways with compatible Listeners (same port & protocol, one hostname specified, one not) - -```yaml -kind: Gateway -apiVersion: gateway.networking.k8s.io/v1beta1 -metadata: - name: gateway-1 - namespace: envoy-gateway -spec: - gatewayClassName: envoy-gateway - listeners: - - name: http - protocol: HTTP - port: 80 - allowedRoutes: - namespaces: - from: All - hostname: *.envoygateway.io ---- -kind: Gateway -apiVersion: gateway.networking.k8s.io/v1beta1 -metadata: - name: gateway-2 - namespace: envoy-gateway -spec: - gatewayClassName: envoy-gateway - listeners: - - name: http - protocol: HTTP - port: 80 - allowedRoutes: - namespaces: - from: All -``` - -#### Example 3: Gateways with incompatible Listeners (same port, protocol and hostname) - -```yaml -kind: Gateway -apiVersion: gateway.networking.k8s.io/v1beta1 -metadata: - name: gateway-1 - namespace: envoy-gateway -spec: - gatewayClassName: envoy-gateway - listeners: - - name: http - protocol: HTTP - port: 80 - allowedRoutes: - namespaces: - from: All - hostname: whales.envoygateway.io ---- -kind: Gateway -apiVersion: gateway.networking.k8s.io/v1beta1 -metadata: - name: gateway-2 - namespace: envoy-gateway -spec: - gatewayClassName: envoy-gateway - listeners: - - name: http - protocol: HTTP - port: 80 - allowedRoutes: - namespaces: - from: All - hostname: whales.envoygateway.io -``` - -#### Example 4: Gateways with incompatible Listeners (neither specify a hostname) - -```yaml -kind: Gateway -apiVersion: gateway.networking.k8s.io/v1beta1 -metadata: - name: gateway-1 - namespace: envoy-gateway -spec: - gatewayClassName: envoy-gateway - listeners: - - name: http - protocol: HTTP - port: 80 - allowedRoutes: - namespaces: - from: All ---- -kind: Gateway -apiVersion: gateway.networking.k8s.io/v1beta1 -metadata: - name: gateway-2 - namespace: envoy-gateway -spec: - gatewayClassName: envoy-gateway - listeners: - - name: http - protocol: HTTP - port: 80 - allowedRoutes: - namespaces: - from: All -``` - -## Computing Status - -Gateway API specifies a rich set of status fields & conditions for each resource. -To be conformant, Envoy Gateway needs to compute the appropriate status fields and conditions as it's processing resources. - -Status needs to be computed and set for: -- the GatewayClass (gatewayclass.status.conditions) -- each Listener for each Gateway (gateway.status.listeners) -- each Gateway, based on its Listeners' statuses (gateway.status.conditions) -- each ParentRef for each Route (route.status.parents) - -The Gateway API translator will take the approach of populating status on the resources themselves as they're being processed, and then passing those statuses off to another component to persist the updates to the Kubernetes API or other backend. - -## Outline - -The following roughly outlines the translation process. -Each step may produce (1) IR; and (2) status updates on Gateway API resources. - -``` -1. Process Gateway Listeners - - validate unique hostnames/ports/protcols - - validate/compute supported kinds - - validate allowed namespaces (validate selector if specified) - - validate TLS details if specified, resolve secret ref - -2. Process HTTPRoutes - - foreach route rule: - - compute matches - - [core] path exact, path prefix - - [core] header exact - - [extended] query param exact - - [extended] HTTP method - - compute filters - - [core] request header modifier (set/add/remove) - - [core] request redirect (hostname, statuscode) - - [extended] request mirror - - compute backends - - [core] Kubernetes services - - foreach route parent ref: - - get matching listeners (check Gateway, section name, listener validation status, listener allowed routes, hostname intersection) - - foreach matching listener: - - foreach hostname intersection with route: - - add each computed route rule to host -``` - -## Context Structs - -To help store, access and manipulate information as it's processed during the translation process, a set of context structs will be used. -These structs will wrap a given Gateway API type, and add additional fields and methods to support processing. -For example, below is a partial sketch of the ListenerContext struct: - -```go -type listenerContext struct { - // The Listener. - listener *gatewayapi_v1beta1.Listener - - // The Gateway this Listener belongs to. - gateway *gatewayapi_v1beta1.Gateway - - // The TLS Secret for this Listener, if applicable. - tlsSecret *corev1.Secret -} - -// Sets a Listener condition on the Listener's Gateway's .status.listeners. -func (lctx *ListenerContext) SetCondition(type string, status bool, reason string, message string) { - ... -} - -// Returns whether or not the Listener allows a given Route kind. -func (lctx *ListenerContext) AllowsKind(kind gatewayapi_v1beta1.Kind) bool { - ... -} -``` - -The exact specs of these structs will be worked out at implementation time. diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 00000000000..28a7e5e4af7 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,5 @@ + + + + + diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index 4304b533b8a..00000000000 --- a/docs/index.rst +++ /dev/null @@ -1,24 +0,0 @@ -Envoy Gateway: envoyproxy + Gateway API -======================================= - -Release |version| (Envoy |envoyVersion|, Gateway API |gatewayAPIVersion|) - -.. image:: https://img.shields.io/badge/slack-join-orange.svg - :target: https://envoyproxy.io/slack - :alt: Join the Envoy Slack - -Envoy Gateway is an open source project for managing Envoy Proxy as a -standalone or Kubernetes-based application gateway. - -.. note:: - - This project is under active development. Many, many features are not - complete. We would love for you to :doc:`get involved`. - -.. toctree:: - :maxdepth: 2 - - intro/index - intro/compatibility - about_docs - get_involved diff --git a/docs/intro/index.rst b/docs/intro/index.rst deleted file mode 100644 index 7291480d7d9..00000000000 --- a/docs/intro/index.rst +++ /dev/null @@ -1,15 +0,0 @@ -Introduction -============ - -Envoy Gateway is an open source project for managing Envoy Proxy as a -standalone or Kubernetes-based application gateway. It uses the Gateway -API as its sole configuration language. - -Many things are in the scope of Envoy Gateway. Many things are not. Many -things (like support for non-Kubernetes instances) will be in scope later, -but are not now. - -.. note:: - - This project is under active development. Many, many features are not - complete. We would love for you to :doc:`get involved<../get_involved>`. diff --git a/docs/latest/about_docs.rst b/docs/latest/about_docs.rst new file mode 100644 index 00000000000..ecaa28247b9 --- /dev/null +++ b/docs/latest/about_docs.rst @@ -0,0 +1,9 @@ +About the Documentation +======================= + +Learn how to contribute to Envoy Gateway documentation. + +.. toctree:: + :maxdepth: 1 + + dev/DOCS diff --git a/docs/latest/conf.py b/docs/latest/conf.py new file mode 100644 index 00000000000..e5d4e3e423b --- /dev/null +++ b/docs/latest/conf.py @@ -0,0 +1,41 @@ +import os +import re + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.duration', + 'sphinx.ext.autosectionlabel', + 'myst_parser', +] + +html_theme = 'alabaster' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +version = os.environ["BUILD_VERSION"] +envoyVersion = os.environ["ENVOY_VERSION"] +gatewayAPIVersion = os.environ["GATEWAYAPI_VERSION"] + +project = 'Envoy Gateway' +author = 'Envoy Gateway Project Authors' + +copyright = 'Envoy Gateway Project Authors | GitHub | Latest Docs' + +source_suffix = { + '.rst': 'restructuredtext', + '.md': 'markdown', +} + +variables_to_export = [ + "version", + "envoyVersion", + "gatewayAPIVersion", +] + +frozen_locals = dict(locals()) +rst_epilog = '\n'.join(map(lambda x: f".. |{x}| replace:: {frozen_locals[x]}", variables_to_export)) +del frozen_locals diff --git a/docs/latest/design/authentication_policy.md b/docs/latest/design/authentication_policy.md new file mode 100644 index 00000000000..761d93bccc1 --- /dev/null +++ b/docs/latest/design/authentication_policy.md @@ -0,0 +1,56 @@ +# Envoy Gateway Authentication Policy API + +## Overview + +<<<<<<< HEAD:docs/latest/design/authentication_policy.md +<<<<<<< HEAD:docs/latest/design/authentication_policy.md +This authentication policy declares the authentication mechanisms, to be enforced on connection and request going though Envoy Gateway. This includes the credential (X.509, JWT, etc), parameters (cipher suites, key algorithms) +======= +This authen policy is to declare the authentication mechanisms, to be enforce on connection and request going though Envoy Gateway. This includes the credential (X.509, JWT, etc), parameters (cipher suites, key algorithms) +>>>>>>> 9a6ed41 (Add authn policy design with JWT only):docs/design/authentication_policy.md +======= +This authentication policy declares the authentication mechanisms, to be enforced on connection and request going though Envoy Gateway. This includes the credential (X.509, JWT, etc), parameters (cipher suites, key algorithms) +>>>>>>> b5c4755 (Update docs/design/authentication_policy.md):docs/design/authentication_policy.md +The policy is similar to [OpenAPI 3.1 security objects](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#securitySchemeObject) without the API key part, and should be easily translatable from it with some additions. + +## Authentication mechanisms +This policy should support the following authentication mechanisms: +- JWT Bearer Token +- mutualTLS (client certificate) +- OAuth2 +- OIDC +- External authentication + +In the first phase, Envoy Gateway will implement JWT Bearer Token authentication. + +In general those policy translates into Envoy HTTP filters at HTTP connection manager level, and route specific settings will be applied for each route. These APIs are expressed in a Policy CRD and attached to Gateway API resources with [Policy Attachement](https://gateway-api.sigs.k8s.io/references/policy-attachment/). + +## JWT Bearer Token + +A JWT Bearer Token authentication policy will look like the following: + +``` +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: Authentication +metadata: + name: productpage +spec: + type: jwt + jwt: + iss: https://www.okta.com + aud: bookinfo.com + jwksUri: https://bookinfo.com/jwt/public-key/jwks.json + targetRef: + kind: HTTPRoute + name: httpbin +``` + +<<<<<<< HEAD:docs/latest/design/authentication_policy.md +<<<<<<< HEAD:docs/latest/design/authentication_policy.md +JWT Bearer token will be translate to Envoy's JWT authentication filter. The JWKS URI need to be translated to a separate cluster for JWKS fetch and refresh. +======= +JWT Bearer token will be translate to Envoy's JWT authentication filter. The JWKS URI need to be translated to a separate cluster for JWKS fetch and refersh. +>>>>>>> 9a6ed41 (Add authn policy design with JWT only):docs/design/authentication_policy.md +======= +JWT Bearer token will be translate to Envoy's JWT authentication filter. The JWKS URI need to be translated to a separate cluster for JWKS fetch and refresh. +>>>>>>> 3cb97e1 (Update docs/design/authentication_policy.md):docs/design/authentication_policy.md diff --git a/docs/design/CONFIG_API.md b/docs/latest/design/config-api.md similarity index 92% rename from docs/design/CONFIG_API.md rename to docs/latest/design/config-api.md index fbbfb2aa494..3696860dd54 100644 --- a/docs/design/CONFIG_API.md +++ b/docs/latest/design/config-api.md @@ -1,28 +1,19 @@ -Configuration API Design -=================== - -# Table of Contents -1. [Motivation](#motivation) -2. [Goals](#goals) -3. [Non-Goals](#non-goals) -4. [Control Plane API](#control_plane_api) - 1. [Gateway Type](#gateway) - 2. [Provider Type](#provider) - 3. [Configuration Examples](#control_plane_configuration) -6. [Data Plane API](#data_plane_api) - 1. [Configuration Examples](#data_plane_configuration) +# Configuration API Design ## Motivation + [Issue 51][issue_51] specifies the need to design an API for configuring Envoy Gateway. The control plane is configured statically at startup and the data plane is configured dynamically through Kubernetes resources, primarily [Gateway API][gw_api] objects. Refer to the Envoy Gateway [design doc][design_doc] for additional details regarding Envoy Gateway terminology and configuration. ## Goals + * Define an __initial__ API to configure Envoy Gateway at startup. * Define an __initial__ API for configuring the managed data plane, e.g. Envoy proxies. ## Non-Goals + * Implementation of the configuration APIs. * Define the `status` subresource of the configuration APIs. * Define a __complete__ set of APIs for configuring Envoy Gateway. As stated in the [Goals](#goals), this document @@ -32,6 +23,7 @@ Envoy Gateway terminology and configuration. * Specify tooling for managing the API, e.g. generate protos, CRDs, controller RBAC, etc. ## Control Plane API + The `EnvoyGateway` API defines the control plane configuration, e.g. Envoy Gateway. Key points of this API are: * It will define Envoy Gateway's startup configuration file. If the file does not exist, Envoy Gateway will start up @@ -44,6 +36,7 @@ The `EnvoyGateway` API defines the control plane configuration, e.g. Envoy Gatew * If data plane static configuration is required in the future, Envoy Gateway will use a separate file for this purpose. The `v1alpha1` version and `config.gateway.envoyproxy.io` API group get generated: + ```go // gateway/api/config/v1alpha1/doc.go @@ -53,7 +46,8 @@ The `v1alpha1` version and `config.gateway.envoyproxy.io` API group get generate package v1alpha1 ``` -The initial `EnvoyGateway` API being proposed: +The initial `EnvoyGateway` API: + ```go // gateway/api/config/v1alpha1/envoygateway.go @@ -139,30 +133,37 @@ type FileProvider struct { // TODO: Add config as use cases are better understood. } ``` + __Note:__ Provider-specific configuration is defined in the `{$PROVIDER_NAME}Provider` API. ### Gateway + Gateway defines desired configuration of [Gateway API][gw_api] controllers that reconcile and translate Gateway API resources into the Intermediate Representation (IR). Refer to the Envoy Gateway [design doc][design_doc] for additional details. ### Provider + Provider defines the desired configuration of an Envoy Gateway provider. A provider is an infrastructure component that Envoy Gateway calls to establish its runtime configuration. Provider is a [union type][union]. Therefore, Envoy Gateway can be configured with only one provider based on the `type` discriminator field. Refer to the Envoy Gateway [design doc][design_doc] for additional details. ### Control Plane Configuration + The configuration file is defined by the EnvoyGateway API type. At startup, Envoy Gateway searches for the configuration at "/etc/envoy-gateway/config.yaml". Start Envoy Gateway: + ```shell $ ./envoy-gateway ``` + Since the configuration file does not exist, Envoy Gateway will start with default configuration parameters. The Kubernetes provider can be configured explicitly using `provider.kubernetes`: + ```yaml $ cat << EOF > /etc/envoy-gateway/config.yaml apiVersion: config.gateway.envoyproxy.io/v1alpha1 @@ -172,9 +173,11 @@ provider: kubernetes: {} EOF ``` + This configuration will cause Envoy Gateway to use the Kubernetes provider with default configuration parameters. The Kubernetes provider can be configured using the `provider` field. For example, the `foo` field can be set to "bar": + ```yaml $ cat << EOF > /etc/envoy-gateway/config.yaml apiVersion: config.gateway.envoyproxy.io/v1alpha1 @@ -185,11 +188,13 @@ provider: foo: bar EOF ``` + __Note:__ The Provider API from the Kubernetes package is currently undefined and `foo: bar` is provided for illustration purposes only. The same API structure is followed for each supported provider. The following example causes Envoy Gateway to use the File provider: + ```yaml $ cat << EOF > /etc/envoy-gateway/config.yaml apiVersion: config.gateway.envoyproxy.io/v1alpha1 @@ -200,12 +205,14 @@ provider: foo: bar EOF ``` + __Note:__ The Provider API from the File package is currently undefined and `foo: bar` is provided for illustration purposes only. Gateway API-related configuration is expressed through the `gateway` field. If unspecified, Envoy Gateway will use default configuration parameters for `gateway`. The following example causes the [GatewayClass][gc] controller to manage GatewayClasses with controllerName `foo` instead of the default `gateway.envoyproxy.io/gatewayclass-controller`: + ```yaml $ cat << EOF > /etc/envoy-gateway/config.yaml apiVersion: config.gateway.envoyproxy.io/v1alpha1 @@ -215,24 +222,27 @@ gateway: ``` With any of the above configuration examples, Envoy Gateway can be started without any additional arguments: + ```shell $ ./envoy-gateway ``` ## Data Plane API + The data plane is configured dynamically through Kubernetes resources, primarily [Gateway API][gw_api] objects. Optionally, the data plane infrastructure can be configured by referencing a [custom resource (CR)][cr] through -`spec.parametersRef` of the managed GatewayClass. The `envoyproxies` API defines the data plane infrastructure +`spec.parametersRef` of the managed GatewayClass. The `EnvoyProxy` API defines the data plane infrastructure configuration and is represented as the CR referenced by the managed GatewayClass. Key points of this API are: -* If unreferenced by `spec.parametersRef`, default parameters will be used to configure the data plane infrastructure, - e.g. expose Envoy network endpoints using a LoadBalancer service. +* If unreferenced by `gatewayclass.spec.parametersRef`, default parameters will be used to configure the data plane + infrastructure, e.g. expose Envoy network endpoints using a LoadBalancer service. * Envoy Gateway will follow Gateway API [recommendations][gc] regarding updates to the EnvoyProxy CR: > It is recommended that this resource be used as a template for Gateways. This means that a Gateway is based on the > state of the GatewayClass at the time it was created and changes to the GatewayClass or associated parameters are > not propagated down to existing Gateways. -The initial `envoyproxies` API being proposed: +The initial `EnvoyProxy` API: + ```go // gateway/api/config/v1alpha1/envoyproxy.go @@ -262,12 +272,15 @@ type EnvoyProxyStatus struct { // Undefined by this design spec. } ``` + The EnvoyProxySpec and EnvoyProxyStatus fields will be defined in the future as proxy infrastructure configuration use cases are better understood. ### Data Plane Configuration + GatewayClass and Gateway resources define the data plane infrastructure. Note that all examples assume Envoy Gateway is running with the Kubernetes provider. + ```yaml apiVersion: gateway.networking.k8s.io/v1beta1 kind: GatewayClass @@ -287,12 +300,14 @@ spec: protocol: HTTP port: 80 ``` + Since the GatewayClass does not define `spec.parametersRef`, the data plane is provisioned using default configuration -parameters. All Envoy proxies will be configured with a http listener and a Kubernetes LoadBalancer service listening +parameters. The Envoy proxies will be configured with a http listener and a Kubernetes LoadBalancer service listening on port 80. The following example will configure the data plane to use a ClusterIP service instead of the default LoadBalancer service: + ```yaml apiVersion: gateway.networking.k8s.io/v1beta1 kind: GatewayClass @@ -324,13 +339,12 @@ spec: networkPublishing: type: ClusterIPService ``` + __Note:__ The NetworkPublishing API is currently undefined and is provided here for illustration purposes only. [issue_51]: https://github.com/envoyproxy/gateway/issues/51 [design_doc]: https://github.com/envoyproxy/gateway/blob/main/docs/design/SYSTEM_DESIGN.md -[xds]: https://github.com/cncf/xds [gw_api]: https://gateway-api.sigs.k8s.io/ -[config_guide]: https://github.com/envoyproxy/gateway/blob/main/docs/CONFIG.md [gc]: https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.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/docs/latest/design/gatewayapi-translator.md b/docs/latest/design/gatewayapi-translator.md new file mode 100644 index 00000000000..1480c5f4257 --- /dev/null +++ b/docs/latest/design/gatewayapi-translator.md @@ -0,0 +1,250 @@ +# Gateway API Translator Design + +The Gateway API translates external resources, e.g. GatewayClass, from the configured Provider to the Intermediate +Representation (IR). + +## Assumptions + +Initially target core conformance features only, to be followed by extended conformance features. + +## Inputs and Outputs + +The main inputs to the Gateway API translator are: + +- GatewayClass, Gateway, HTTPRoute, TLSRoute, Service, ReferenceGrant, Namespace, and Secret resources. + +__Note:__ ReferenceGrant is not fully implemented as of v0.2. + +The outputs of the Gateway API translator are: + +- Xds and Infra Internal Representations (IRs). +- Status updates for GatewayClass, Gateways, HTTPRoutes + +## Listener Compatibility + +Envoy Gateway follows Gateway API listener compatibility spec: +> Each listener in a Gateway must have a unique combination of Hostname, Port, and Protocol. An implementation MAY group +> Listeners by Port and then collapse each group of Listeners into a single Listener if the implementation determines +> that the Listeners in the group are “compatible”. + +__Note:__ Envoy Gateway does not collapse listeners across multiple Gateways. + +### Listener Compatibility Examples + +#### Example 1: Gateway with compatible Listeners (same port & protocol, different hostnames) + +```yaml +kind: Gateway +apiVersion: gateway.networking.k8s.io/v1beta1 +metadata: + name: gateway-1 + namespace: envoy-gateway +spec: + gatewayClassName: envoy-gateway + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All + hostname: *.envoygateway.io + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All + hostname: whales.envoygateway.io +``` + +#### Example 2: Gateway with compatible Listeners (same port & protocol, one hostname specified, one not) + +```yaml +kind: Gateway +apiVersion: gateway.networking.k8s.io/v1beta1 +metadata: + name: gateway-1 + namespace: envoy-gateway +spec: + gatewayClassName: envoy-gateway + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All + hostname: *.envoygateway.io + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All +``` + +#### Example 3: Gateway with incompatible Listeners (same port, protocol and hostname) + +```yaml +kind: Gateway +apiVersion: gateway.networking.k8s.io/v1beta1 +metadata: + name: gateway-1 + namespace: envoy-gateway +spec: + gatewayClassName: envoy-gateway + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All + hostname: whales.envoygateway.io + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All + hostname: whales.envoygateway.io +``` + +#### Example 4: Gateway with incompatible Listeners (neither specify a hostname) + +```yaml +kind: Gateway +apiVersion: gateway.networking.k8s.io/v1beta1 +metadata: + name: gateway-1 + namespace: envoy-gateway +spec: + gatewayClassName: envoy-gateway + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All +``` + +## Computing Status + +Gateway API specifies a rich set of status fields & conditions for each resource. To achieve conformance, Envoy Gateway +must compute the appropriate status fields and conditions for managed resources. + +Status is computed and set for: + +- The managed GatewayClass (`gatewayclass.status.conditions`). +- Each managed Gateway, based on its Listeners' status (`gateway.status.conditions`). For the Kubernetes provider, the + Envoy Deployment and Service status are also included to calculate Gateway status. +- Listeners for each Gateway (`gateway.status.listeners`). +- The ParentRef for each Route (`route.status.parents`). + +The Gateway API translator is responsible for calculating status conditions while translating Gateway API resources to +the IR and publishing status over the [message bus][]. The Status Manager subscribes to these status messages and +updates the resource status using the configured provider. For example, the Status Manager uses a Kubernetes client to +update resource status on the Kubernetes API server. + +## Outline + +The following roughly outlines the translation process. Each step may produce (1) IR; and (2) status updates on Gateway +API resources. + +1. Process Gateway Listeners + - Validate unique hostnames, ports, and protocols. + - Validate and compute supported kinds. + - Validate allowed namespaces (validate selector if specified). + - Validate TLS fields if specified, including resolving referenced Secrets. + +2. Process HTTPRoutes + - foreach route rule: + - compute matches + - [core] path exact, path prefix + - [core] header exact + - [extended] query param exact + - [extended] HTTP method + - compute filters + - [core] request header modifier (set/add/remove) + - [core] request redirect (hostname, statuscode) + - [extended] request mirror + - compute backends + - [core] Kubernetes services + - foreach route parent ref: + - get matching listeners (check Gateway, section name, listener validation status, listener allowed routes, hostname intersection) + - foreach matching listener: + - foreach hostname intersection with route: + - add each computed route rule to host + +## Context Structs + +To help store, access and manipulate information as it's processed during the translation process, a set of context +structs are used. These structs wrap a given Gateway API type, and add additional fields and methods to support +processing. + +`GatewayContext` wraps a Gateway and provides helper methods for setting conditions, accessing Listeners, etc. + +```go +type GatewayContext struct { + // The managed Gateway + *v1beta1.Gateway + + // A list of Gateway ListenerContexts. + listeners []*ListenerContext +} +``` + +`ListenerContext` wraps a Listener and provides helper methods for setting conditions and other status information on +the associated Gateway. + +```go +type ListenerContext struct { + // The Gateway listener. + *v1beta1.Listener + + // The Gateway this Listener belongs to. + gateway *v1beta1.Gateway + + // An index used for managing this listener in the list of Gateway listeners. + listenerStatusIdx int + + // Only Routes in namespaces selected by the selector may be attached + // to the Gateway this listener belongs to. + namespaceSelector labels.Selector + + // The TLS Secret for this Listener, if applicable. + tlsSecret *v1.Secret +} +``` + +`RouteContext` represents a generic Route object (HTTPRoute, TLSRoute, etc.) that can reference Gateway objects. + +```go +type RouteContext interface { + client.Object + + // GetRouteType returns the Kind of the Route object, HTTPRoute, + // TLSRoute, TCPRoute, UDPRoute etc. + GetRouteType() string + + // GetHostnames returns the hosts targeted by the Route object. + GetHostnames() []string + + // GetParentReferences returns the ParentReference of the Route object. + GetParentReferences() []v1beta1.ParentReference + + // GetRouteParentContext returns RouteParentContext by using the Route + // objects' ParentReference. + GetRouteParentContext(forParentRef v1beta1.ParentReference) *RouteParentContext +} +``` + +[message bus]: watching.md diff --git a/docs/latest/design/request-authentication.md b/docs/latest/design/request-authentication.md new file mode 100644 index 00000000000..be7b8bd3f53 --- /dev/null +++ b/docs/latest/design/request-authentication.md @@ -0,0 +1,610 @@ +# Request Authentication + +## Overview + +[Issue 336][] specifies the need for exposing a user-facing API to configure request authentication. Request +authentication is defined as an authentication mechanism to be enforced by Envoy on a per-request basis. A connection +will be rejected if it contains invalid authentication information, based on the `Authentication` API type proposed in +this design document. + +Envoy Gateway leverages [Gateway API][] for configuring managed Envoy proxies. Gateway API defines core, extended, and +implementation-specific API [support levels][] for implementors such as Envoy Gateway to expose features. Since +implementing request authentication is not covered by `Core` or `Extended` APIs, an `Implementation-specific` API will +be created for this purpose. + +## Goals + +* Define an API for configuring request authentication. +* Implement [JWT] as the first supported authentication type. +* Allow users that manage routes, e.g. [HTTPRoute][], to authenticate matching requests before forwarding to a backend + service. +* Support HTTPRoutes as an Authentication API referent. HTTPRoute provides multiple [extension points][]. The + [HTTPRouteFilter][] is the extension point supported by the Authentication API. + +## Non-Goals + +* Allow infrastructure administrators to override or establish default authentication policies. +* Support referents other than HTTPRoute. +* Support Gateway API extension points other than HTTPRouteFilter. + +## Use-Cases + +These use-cases are presented as an aid for how users may attempt to utilize the outputs of the design. They are not an +exhaustive list of features for authentication support in Envoy Gateway. + +As a Service Producer, I need the ability to: +* Authenticate a request before forwarding it to a backend service. +* Have different authentication mechanisms per route rule. +* Choose from different authentication mechanisms supported by Envoy, e.g. OIDC. + +### Security API Group + +A new API group, `security.gateway.envoyproxy.io` is introduced to group security-related APIs. This will allow security +APIs to be versioned, changed, etc. over time. + +### Authentication API Type + +The Authentication API type defines authentication configuration for authenticating requests through managed Envoy +proxies. + +```go +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +) + +type Authentication struct { + metav1.TypeMeta + metav1.ObjectMeta + + // Spec defines the desired state of the Authentication type. + Spec AuthenticationSpec + + // Note: The status sub-resource has been excluded but may be added in the future. +} + +// AuthenticationSpec defines the desired state of the Authentication type. +// +union +type AuthenticationSpec struct { + // Type defines the type of authentication provider to use. Supported provider + // types are: + // + // * JWT + // + // JWT defines the JSON Web Token (JWT) authentication provider type. + // + // +unionDiscriminator + Type AuthenticationType `json:"type"` + + // JWT defines the JSON Web Token (JWT) authentication provider type. When multiple + // jwtProviders 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 + // + // +kubebuilder:validation:MaxItems=4 + // +optional + JwtProviders []JwtAuthenticationProvider `json:"jwtProviders,omitempty"` +} + +// AuthenticationType is a type of authentication provider. +// +kubebuilder:validation:Enum=JWT +type AuthenticationType string + +const ( + // JwtAuthenticationProviderType is the JWT authentication provider type. + JwtAuthenticationProviderType AuthenticationType = "JWT" +) + +// JwtAuthenticationProvider defines the JSON Web Token (JWT) authentication provider type +// and how JWTs should be verified: +type JwtAuthenticationProvider struct { + // 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. + // + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + Name string `json:"name"` + + // Issuer is the principal that issued the JWT. For additional details, see: + // + // https://tools.ietf.org/html/rfc7519#section-4.1.1 + // + // Example: + // issuer: https://auth.example.com + // + // If not provided, the JWT issuer is not checked. + // + // +kubebuilder:validation:MaxLength=253 + // +optional + Issuer string `json:"issuer,omitempty"` + + // Audiences is a list of JWT audiences allowed to access. For additional details, see: + // + // https://tools.ietf.org/html/rfc7519#section-4.1.3 + // + // Example: + // audiences: + // - foo.apps.example.com + // bar.apps.example.com + // + // If not provided, JWT audiences are not checked. + // + // +kubebuilder:validation:MaxItems=8 + // +optional + Audiences []string `json:"audiences,omitempty"` + + // RemoteJWKS defines how to fetch and cache JSON Web Key Sets (JWKS) from a remote + // HTTP/HTTPS endpoint. + RemoteJWKS RemoteJWKS `json:"remoteJWKS"` + + // TODO: Add TBD JWT fields based on defined use cases. +} + +// RemoteJWKS defines how to fetch and cache JSON Web Key Sets (JWKS) from a remote +// HTTP/HTTPS endpoint. +type RemoteJWKS struct { + // Uri is the HTTP/HTTPS URI to fetch the JWKS. + // + // Example: + // uri: https://www.googleapis.com/oauth2/v1/certs + // + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + Uri string `json:"uri"` + + // TODO: Add TBD remote JWKS fields based on defined use cases. +} +``` + +The status subresource is not included in the Authentication API. Status will be surfaced by an HTTPRoute that +references an Authentication. For example, an HTTPRoute will surface the `ResolvedRefs=False` status condition if it +references an Authentication that does not exist. It may be beneficial to add status fields in the future based on +defined use-cases. For example, a remote JWKS can be validated based on the specified URI and have an appropriate +status condition surfaced. + +#### Authentication Example + +The following is an Authentication example with one JWT authentication provider: + +```yaml +apiVersion: security.gateway.envoyproxy.io/v1alpha1 +kind: Authentication +metadata: + name: example +spec: + type: JWT + jwtProviders: + - name: example + issuer: https://www.example.com + audiences: + - foo.com + remoteJwks: + uri: https://foo.com/jwt/public-key/jwks.json + +status: + +``` + +__Note:__ `type` is a union type, allowing only one of any supported provider type such as `jwtProviders` to be +specified. + +The following is an example HTTPRoute configured to use the above JWT authentication provider: + +```yaml +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: HTTPRoute +metadata: + name: example +spec: + parentRefs: + - name: eg + hostnames: + - www.example.com + rules: + - matches: + - path: + type: PathPrefix + value: /foo + filters: + - type: ExtensionRef + extensionRef: + group: security.gateway.envoyproxy.io + kind: Authentication + name: example + backendRefs: + - name: backend + port: 3000 +``` + +Requests for `www.example.com/foo` will be authenticated using the referenced JWT provider before being forwarded to the +backend service named "backend". + +## Implementation Details + +The JWT authentication type is translated to an Envoy [JWT authentication filter][] and a cluster is created for each +remote JWKS. The following examples provide additional details on how Gateway API and Authentication resources are +translated into Envoy configuration. + +### Example 1: One Route with One JWT Provider + +The following cluster is created from the above HTTPRoute and Authentication: + +```yaml +dynamic_clusters: + - name: foo.com|443 + load_assignment: + cluster_name: foo.com|443 + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: foo.com + port_value: 443 + transport_socket: + name: envoy.transport_sockets.tls + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + sni: foo.com + common_tls_context: + validation_context: + match_subject_alt_names: + - exact: "*.foo.com" + trusted_ca: + filename: /etc/ssl/certs/ca-certificates.crt +``` + +A JWT authentication HTTP filter is added to the HTTP Connection Manager. For example: + +```yaml +dynamic_resources: + dynamic_listeners: + - name: example_listener + address: + socket_address: + address: 1.2.3.4 + port_value: 80 + filter_chains: + - filters: + - name: envoy.http_connection_manager + http_filters: + - name: envoy.filters.http.jwt_authn + typed_config: + "@type": type.googleapis.com/envoy.config.filter.http.jwt_authn.v2alpha.JwtAuthentication +``` + +This JWT authentication filter contains two fields: +* The `providers` field specifies how a JWT should be verified, such as where to extract the token, where to fetch the + public key (JWKS) and where to output its payload. This field is built from the source resource `namespace-name`, and + the JWT provider name of an Authentication. +* The `rules` field specifies matching rules and their requirements. If a request matches a rule, its requirement + applies. The requirement specifies which JWT providers should be used. This field is built from a HTTPRoute + `matches` rule that references the Authentication extended filter. When a referenced Authentication specifies + multiple `jwtProviders`, the JWT is considered valid if __any__ of the providers successfully validate the JWT. + +The following JWT Authentication filter `providers` configuration is created from the above Authentication. + +```yaml +providers: + example: + issuer: https://www.example.com + audiences: + - foo.com + remote_jwks: + http_uri: + uri: https://foo.com/jwt/public-key/jwks.json + cluster: example_jwks_cluster + timeout: 1s +``` + +The following JWT Authentication filter `rules` configuration is created from the above HTTPRoute. + +```yaml +rules: + - match: + prefix: /foo + requires: + provider_name: default-example-example +``` + +### Example 2: Two HTTPRoutes with Different Authentications + +The following example contains: +* Two HTTPRoutes with different hostnames. +* Each HTTPRoute references a different Authentication +* Each Authentication contains a different JWT provider. + +```yaml +apiVersion: security.gateway.envoyproxy.io/v1alpha1 +kind: Authentication +metadata: + name: example1 +spec: + type: JWT + jwtProviders: + - name: example1 + issuer: https://www.example1.com + audiences: + - foo.com + remoteJwks: + uri: https://foo.com/jwt/public-key/jwks.json +--- +apiVersion: security.gateway.envoyproxy.io/v1alpha1 +kind: Authentication +metadata: + name: example2 +spec: + type: JWT + jwtProviders: + - name: example2 + issuer: https://www.example2.com + audiences: + - bar.com + remoteJwks: + uri: https://bar.com/jwt/public-key/jwks.json +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: HTTPRoute +metadata: + name: example1 +spec: + hostnames: + - www.example1.com + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: eg + rules: + - matches: + - path: + type: PathPrefix + value: /foo + filters: + - type: ExtensionRef + extensionRef: + group: security.gateway.envoyproxy.io + kind: Authentication + name: example1 + backendRefs: + - name: backend + port: 3000 +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: HTTPRoute +metadata: + name: example2 +spec: + hostnames: + - www.example2.com + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: eg + rules: + - matches: + - path: + type: PathPrefix + value: /bar + filters: + - type: ExtensionRef + extensionRef: + group: security.gateway.envoyproxy.io + kind: Authentication + name: example2 + backendRefs: + - name: backend2 + port: 3000 +``` + +The following xDS configuration is created from the above example resources: + +```yaml +configs: +... +dynamic_listeners: + - name: default-eg-http + ... + default_filter_chain: + filters: + - name: envoy.filters.network.http_connection_manager_1 + typed_config: + '@type': >- + type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: http + rds: + config_source: + ... + route_config_name: default-eg-http-1 + http_filters: + - name: envoy.filters.http.jwt_authn + typed_config: + '@type': >- + type.googleapis.com/envoy.config.filter.http.jwt_authn.v2alpha.JwtAuthentication + providers: + default-example1-example1: + issuer: https://www.example1.com + audiences: + - foo.com + remote_jwks: + http_uri: + uri: https://foo.com/jwt/public-key/jwks.json + cluster: default-example1-example1-jwt + rules: + - match: + exact: /foo + requires: + provider_name: default-example1-example1 + - name: envoy.filters.http.router + typed_config: + '@type': >- + type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + - name: envoy.filters.network.http_connection_manager_2 + typed_config: + '@type': >- + type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: http + rds: + config_source: + ... + route_config_name: default-eg-http-2 + http_filters: + - name: envoy.filters.http.jwt_authn + typed_config: + '@type': >- + type.googleapis.com/envoy.config.filter.http.jwt_authn.v2alpha.JwtAuthentication + providers: + default-example2-example2: + issuer: https://www.example2.com + audiences: + - bar.com + remote_jwks: + http_uri: + uri: https://bar.com/jwt/public-key/jwks.json + cluster: default-example2-example2-jwt + rules: + - match: + exact: /bar + requires: + provider_name: default-example2-example2 + - name: envoy.filters.http.router + typed_config: + '@type': >- + type.googleapis.com/envoy.extensions.filters.http.router.v3.Router +dynamic_route_configs: + - route_config: + '@type': type.googleapis.com/envoy.config.route.v3.RouteConfiguration + name: default-eg-http-1 + virtual_hosts: + - name: default-eg-http-1 + domains: + - '*' + routes: + - match: + prefix: /foo + headers: + - name: ':authority' + string_match: + exact: www.example1.com + route: + cluster: default-backend-rule-0-match-0-www.example1.com + - route_config: + '@type': type.googleapis.com/envoy.config.route.v3.RouteConfiguration + name: default-eg-http + virtual_hosts: + - name: default-eg-http-2 + domains: + - '*' + routes: + - match: + prefix: /bar + headers: + - name: ':authority' + string_match: + exact: www.example2.com + route: + cluster: default-backend2-rule-0-match-0-www.example2.com +dynamic_active_clusters: + - cluster: + name: default-backend-rule-0-match-0-www.example.com + ... + endpoints: + - locality: {} + lb_endpoints: + - endpoint: + address: + socket_address: + address: $BACKEND_SERVICE1_IP + port_value: 3000 + - cluster: + '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster + name: default-backend-rule-1-match-0-www.example.com + ... + endpoints: + - locality: {} + lb_endpoints: + - endpoint: + address: + socket_address: + address: $BACKEND_SERVICE2_IP + port_value: 3000 +... +``` + +__Note:__ The JWT provider cluster and route is omitted from the above example for brevity. + +### Implementation Outline + +* Create a `security` API group and add the Authentication API type to this group. +* Update the Kubernetes provider to get/watch Authentication resources that are referenced by managed HTTPRoutes. Add + the referenced Authentication object to the resource map and publish it. +* Update the resource translator to include the Authentication API in HTTPRoute processing. +* Update the xDS translator to translate an Authentication into xDS resources. The translator should perform the + following: + * Convert a list of JWT rules from the xds IR into an Envoy JWT filter config. + * Create a JWT authentication filter. + * Build the HTTP Connection Manager (HCM) HTTP filters. + * Build the HCM. + * When building the Listener, create an HCM for each filter-chain. + +## Adding Authentication Provider Types + +Additional authentication provider types can be added in the future through the `ProviderType` API. For example, to add +the `Foo` authentication provider type: + +Define the `FooProvider` type: + +```go +package v1alpha1 + +// FooProvider defines the "Foo" authentication provider type. +type FooProvider struct { + // TODO: Define fields of the Foo authentication provider type. +} +``` + +Add the `FooProvider` type to `AuthenticationSpec`: + +```go +package v1alpha1 + +type AuthenticationSpec struct { + ... + + // Foo defines the Foo authentication type. For additional + // details, see: + // + // + // + // +optional + Foo *FooAuthentication `json:"foo"` +} +``` + +Authentication should support additional authentication types in the future, for example: +- mutualTLS (client certificate) +- OAuth2 +- OIDC +- External authentication + +## Outstanding Questions + +- If Envoy Gateway owns the Authentication API, is an xDS IR equivalent needed? +- Should local JWKS be implemented before remote JWKS? +- How should Envoy obtain the trusted CA for a remote JWKS? +- Should HTTPS be the only supported scheme for remote JWKS? +- Should OR'ing JWT providers be supported? +- Should Authentication provide status? +- Are the API field validation rules acceptable? + +[Issue 336]: https://github.com/envoyproxy/gateway/issues/336 +[Gateway API]: https://gateway-api.sigs.k8s.io/ +[support levels]: https://gateway-api.sigs.k8s.io/concepts/conformance/?h=extended#2-support-levels +[JWT]: https://jwt.io/ +[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute/ +[extension points]: https://gateway-api.sigs.k8s.io/concepts/api-overview/?h=extension#extension-points +[HTTPRouteFilter]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteFilter +[JWKS]: https://www.rfc-editor.org/rfc/rfc7517 +[JWT authentication filter]: https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/jwt_authn_filter#config-http-filters-jwt-authn diff --git a/docs/latest/design/roadmap.md b/docs/latest/design/roadmap.md new file mode 100644 index 00000000000..ee219500bf8 --- /dev/null +++ b/docs/latest/design/roadmap.md @@ -0,0 +1,74 @@ +# Roadmap + +This document serves as a high-level reference for Envoy Gateway users and contributors to understand the direction of +the project. + +## Contributing to the Roadmap + +- To add a feature to the roadmap, create an [issue][issue] or join a [community meeting][meeting] to discuss your use + case. If your feature is accepted, a maintainer will assign your issue to a [release milestone][milestones] and update + this document accordingly. +- To help with an existing roadmap item, comment on or assign yourself to the associated issue. +- If a roadmap item doesn't have an issue, create one, assign yourself to the issue, and reference this document. A + maintainer will submit a [pull request][PR] to add the feature to the roadmap. __Note:__ The feature should be + discussed in an issue or a community meeting before implementing it. + +If you don't know where to start contributing, help is needed to reduce technical, automation, and documentation debt. +Look for issues with the `help wanted` label to get started. + +## Details + +Roadmap features and timelines may change based on feedback, community contributions, etc. If you depend on a specific +roadmap item, you're encouraged to attend a community meeting to discuss the details, or help us deliver the feature by +contributing to the project. + +`Last Updated: November 2022` + +### [v0.2.0][v0.2.0]: Establish a Solid Foundation + +- Complete the core Envoy Gateway implementation- [Issue #60][60]. +- Establish initial testing, e2e, integration, etc- [Issue #64][64]. +- Establish user and developer project documentation- [Issue #17][17]. +- Achieve Gateway API conformance (e.g. routing, LB, Header transformation, etc.)- [Issue #65][65]. +- Setup a CI/CD pipeline- [Issue #63][63]. + +### [v0.3.0][v0.3.0]: Drive Advanced Features through Extension Mechanisms + +- Support extended Gateway API fields [Issue #707][707]. +- Support experimental Gateway APIs such as TCPRoute [Issue #643][643], UDPRoute [Issue #641][641] and GRPCRoute [Issue #642][642]. +- Establish guidelines for leveragaing Gateway API extensions [Issue #675][675]. +- Rate Limiting [Issue #670][670]. +- Authentication [Issue #336][336]. + +### [v0.4.0][v0.4.0]: More Advanced Features through Extension Mechanisms + +- Allow users to configure xDS Resources [Issue #24][24]. + +### [v0.5.0][v0.5.0]: Manageability and Scale + +- Tooling for devs/infra admins to aid in managing/maintaining EG +- Support advanced provisioning use cases (e.g. multi-cluster, serverless, etc.) +- Perf testing (EG specifically) +- Support for Chaos engineering? + +[issue]: https://github.com/envoyproxy/gateway/issues +[meeting]: https://docs.google.com/document/d/1leqwsHX8N-XxNEyTflYjRur462ukFxd19Rnk3Uzy55I/edit?usp=sharing +[pr]: https://github.com/envoyproxy/gateway/compare +[milestones]: https://github.com/envoyproxy/gateway/milestones +[v0.2.0]: https://github.com/envoyproxy/gateway/milestone/1 +[v0.3.0]: https://github.com/envoyproxy/gateway/milestone/7 +[v0.4.0]: https://github.com/envoyproxy/gateway/milestone/12 +[v0.5.0]: https://github.com/envoyproxy/gateway/milestone/13 +[17]: https://github.com/envoyproxy/gateway/issues/17 +[24]: https://github.com/envoyproxy/gateway/issues/24 +[60]: https://github.com/envoyproxy/gateway/issues/60 +[63]: https://github.com/envoyproxy/gateway/issues/63 +[64]: https://github.com/envoyproxy/gateway/issues/64 +[65]: https://github.com/envoyproxy/gateway/issues/65 +[336]: https://github.com/envoyproxy/gateway/issues/336 +[641]: https://github.com/envoyproxy/gateway/issues/641 +[642]: https://github.com/envoyproxy/gateway/issues/642 +[643]: https://github.com/envoyproxy/gateway/issues/643 +[670]: https://github.com/envoyproxy/gateway/issues/670 +[675]: https://github.com/envoyproxy/gateway/issues/675 +[707]: https://github.com/envoyproxy/gateway/issues/707 diff --git a/docs/design/SYSTEM_DESIGN.md b/docs/latest/design/system-design.md similarity index 69% rename from docs/design/SYSTEM_DESIGN.md rename to docs/latest/design/system-design.md index 46bdf547236..731cb0925b0 100644 --- a/docs/design/SYSTEM_DESIGN.md +++ b/docs/latest/design/system-design.md @@ -1,36 +1,44 @@ -## System Design +# System Design + +## Goals -### Goals * Define the system components needed to satisfy the requirements of Envoy Gateway. -### Non-Goals +## Non-Goals + * Create a detailed design and interface specification for each system component. -### Terminology +## Terminology + * Control Plane- A collection of inter-related software components for providing application gateway and routing functionality. The control plane is implemented by Envoy Gateway and provides services for managing the data plane. These services are detailed in the [components](#components) section. * Data Plane- Provides intelligent application-level traffic routing and is implemented as one or more Envoy proxies. -### Architecture +## Architecture + ![Architecture](../images/architecture.png) -### Configuration +## Configuration + Envoy Gateway is configured statically at startup and the managed data plane is configured dynamically through Kubernetes resources, primarily [Gateway API][gw_api] objects. -#### Static Configuration +### Static Configuration + Static configuration is used to configure Envoy Gateway at startup, i.e. change the GatewayClass controllerName, configure a Provider, etc. Currently, Envoy Gateway only supports configuration through a configuration file. If the -configuration file is not provided, Envoy Gateway will start up with default configuration parameters. +configuration file is not provided, Envoy Gateway starts-up with default configuration parameters. + +### Dynamic Configuration -#### Dynamic Configuration Dynamic configuration is based on the concept of a declaring the desired state of the data plane and using reconciliation loops to drive the actual state toward the desired state. The desired state of the data plane is defined as Kubernetes resources that provide the following services: + * Infrastructure Management- Manage the data plane infrastructure, i.e. deploy, upgrade, etc. This configuration is - expressed through [GatewayClass][gc] and [Gateway][gw] resources. A TBD [Custom Resource][cr] can be referenced by - `gatewayclass.spec.parametersRef` to modify data plane infrastructure default parameters, + expressed through [GatewayClass][gc] and [Gateway][gw] resources. The `EnvoyProxy` [Custom Resource][cr] can be + referenced by `gatewayclass.spec.parametersRef` to modify data plane infrastructure default parameters, e.g. expose Envoy network endpoints using a NodePort service instead of a LoadBalancer service. * Traffic Routing- Define how to handle application-level requests to backend services. For example, route all HTTP requests for "www.example.com" to a backend service running a web server. This configuration is expressed through @@ -38,58 +46,70 @@ defined as Kubernetes resources that provide the following services: Although a backend can be any valid Kubernetes Group/Kind resource, Envoy Gateway only supports a [Service][svc] reference. -### Components +## Components Envoy Gateway is made up of several components that communicate in-process; how this communication happens is described -in [watching.md][]. +in the [Watching Components Design][wcd]. + +### Provider -#### Provider A Provider is an infrastructure component that Envoy Gateway calls to establish its runtime configuration, resolve -services, persist data, etc. Kubernetes and File are the only supported providers. However, other providers can be added -in the future as Envoy Gateway use cases are better understood. A provider is configured at start up through Envoy -Gateway's [static configuration](#static-configuration). +services, persist data, etc. As of v0.2, Kubernetes is the only implemented provider. A file provider is on the roadmap +via [Issue #37][]. Other providers can be added in the future as Envoy Gateway use cases are better understood. A +provider is configured at start up through Envoy Gateway's [static configuration](#static-configuration). + +#### Kubernetes Provider -##### Kubernetes Provider * Uses Kubernetes-style controllers to reconcile Kubernetes resources that comprise the [dynamic configuration](#dynamic-configuration). * Manages the data plane through Kubernetes API CRUD operations. * Uses Kubernetes for Service discovery. * Uses etcd (via Kubernetes API) to persist data. -##### File Provider +#### File Provider * Uses a file watcher to watch files in a directory that define the data plane configuration. * Manages the data plane by calling internal APIs, e.g. `CreateDataPlane()`. * Uses the host's DNS for Service discovery. * If needed, the local filesystem is used to persist data. -#### Resource Watcher +### Resource Watcher + The Resource Watcher watches resources used to establish and maintain Envoy Gateway's dynamic configuration. The mechanics for watching resources is provider-specific, e.g. informers, caches, etc. are used for the Kubernetes provider. The Resource Watcher uses the configured provider for input and provides resources to the Resource Translator as output. -#### Resource Translator +### Resource Translator + The Resource Translator translates external resources, e.g. GatewayClass, from the Resource Watcher to the Intermediate Representation (IR). It is responsible for: + * Translating infrastructure-specific resources/fields from the Resource Watcher to the Infra IR. * Translating proxy configuration resources/fields from the Resource Watcher to the xDS IR. -#### Intermediate Representation (IR) +__Note:__ The Resource Translator is implemented as the `Translator` API type in the `gatewayapi` package. + +### Intermediate Representation (IR) + The Intermediate Representation defines internal data models that external resources are translated into. This allows Envoy Gateway to be decoupled from the external resources used for dynamic configuration. The IR consists of an Infra IR used as input for the Infra Manager and an xDS IR used as input for the xDS Translator. + * Infra IR- Used as the internal definition of the managed data plane infrastructure. * xDS IR- Used as the internal definition of the managed data plane xDS configuration. -#### xDS Translator +### xDS Translator + The xDS Translator translates the xDS IR into xDS Resources that are consumed by the xDS server. -#### xDS Server -The xDS Server is a xDS gRPC Server based on [Go Control Plane][go_cp]. Go Control Plane implements the xDS Server +### xDS Server + +The xDS Server is a xDS gRPC Server based on [Go Control Plane][go_cp]. Go Control Plane implements the Delta xDS Server Protocol and is responsible for using xDS to configure the data plane. -#### Infra Manager +### Infra Manager + The Infra Manager is a provider-specific component responsible for managing the following infrastructure: * Data Plane - Manages all the infrastructure required to run the managed Envoy proxies. For example, CRUD Deployment, @@ -101,29 +121,32 @@ The Infra Manager is a provider-specific component responsible for managing the The Infra Manager consumes the Infra IR as input to manage the data plane infrastructure. -### Design Decisions -* Envoy Gateway will consume one [GatewayClass][gc] by comparing its configured controller name with +## Design Decisions + +* Envoy Gateway consumes one [GatewayClass][gc] by comparing its configured controller name with `spec.controllerName` of a GatewayClass. If multiple GatewayClasses exist with the same `spec.controllerName`, Envoy - Gateway will follow Gateway API [guidelines][gwapi_conflicts] to resolve the conflict. - `gatewayclass.spec.parametersRef` refers to a custom resource for configuring the managed proxy infrastructure. If - unspecified, default configuration parameters are used for the managed proxy infrastructure. -* Envoy Gateway will manage [Gateways][gw] that reference its GatewayClass. - * The first Gateway causes Envoy Gateway to provision the managed Envoy proxy infrastructure. - * Envoy Gateway will merge multiple Gateways that match its GatewayClass and will follow Gateway API - [guidelines][gwapi_conflicts] to resolve any conflicts. - * A Gateway `listener` corresponds to a proxy [Listener][listener]. -* An [HTTPRoute][hroute] resource corresponds to a proxy [Route][route]. - * Each [backendRef][be_ref] corresponds to a proxy [Cluster][cluster]. -* The goal is to make the Infra Manager & Translator components extensible in the future. For now, extensibility can be - achieved using xDS support that Envoy Gateway will provide. + Gateway follows Gateway API [guidelines][gwapi_conflicts] to resolve the conflict. + `gatewayclass.spec.parametersRef` refers to the `EnvoyProxy` custom resource for configuring the managed proxy + infrastructure. If unspecified, default configuration parameters are used for the managed proxy infrastructure. +* Envoy Gateway manages [Gateways][gw] that reference its GatewayClass. + * A Gateway resource causes Envoy Gateway to provision managed Envoy proxy infrastructure. + * Envoy Gateway groups Listeners by Port and collapses each group of Listeners into a single Listener if the Listeners + in the group are compatible. Envoy Gateway considers Listeners to be compatible if all the following conditions are + met: + * Either each Listener within the group specifies the “HTTP” Protocol or each Listener within the group specifies + either the “HTTPS” or “TLS” Protocol. + * Each Listener within the group specifies a unique "Hostname". + * As a special case, one Listener within a group may omit "Hostname", in which case this Listener matches when no + other Listener matches. + * Envoy Gateway does __not__ merge listeners across multiple Gateways. +* Envoy Gateway follows Gateway API [guidelines][gwapi_conflicts] to resolve any conflicts. + * A Gateway `listener` corresponds to an Envoy proxy [Listener][listener]. +* An [HTTPRoute][hroute] resource corresponds to an Envoy proxy [Route][route]. + * Each [backendRef][be_ref] corresponds to an Envoy proxy [Cluster][cluster]. +* The goal is to make Envoy Gateway components extensible in the future. See the [roadmap][] for additional details. The draft for this document is [here][draft_design]. -### Caveats -* The custom resource used to configure the data plane infrastructure is TBD. Track [issue 95][issue_95] for the latest - updates. -* Envoy Gateway's static configuration spec is currently undefined. Track [issue 95][issue_95] for the latest updates. - [gw_api]: https://gateway-api.sigs.k8s.io [gc]: https://gateway-api.sigs.k8s.io/concepts/api-overview/#gatewayclass [gw]: https://gateway-api.sigs.k8s.io/concepts/api-overview/#gateway @@ -136,12 +159,13 @@ The draft for this document is [here][draft_design]. [crf]: https://gateway-api.sigs.k8s.io/v1alpha2/api-types/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.proto#config-route-v3-routeconfiguration +[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 [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 [svc]: https://kubernetes.io/docs/concepts/services-networking/service/ -[issue_95]: https://github.com/envoyproxy/gateway/pull/95 -[watching.md]: ./watching.md +[ wcd ]: ./watching.md +[Issue #37]: https://github.com/envoyproxy/gateway/issues/37 +[roadmap]: roadmap.md diff --git a/docs/latest/design/tcp-udp-design.md b/docs/latest/design/tcp-udp-design.md new file mode 100644 index 00000000000..691838945af --- /dev/null +++ b/docs/latest/design/tcp-udp-design.md @@ -0,0 +1,47 @@ +# TCP and UDP Proxy Design + +Even though most of the use cases for Envoy Gateway are at Layer-7, Envoy Gateway can also work at Layer-4 to proxy TCP +and UDP traffic. This document will explore the options we have when operating Envoy Gateway at Layer-4 and explain the +design decision. + +Envoy can work as a non-transparent proxy or a transparent proxy for both [TCP](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_features/ip_transparency#arch-overview-ip-transparency-original-src-listener) + and [UDP](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/udp/udp_proxy/v3/udp_proxy.proto#envoy-v3-api-msg-extensions-filters-udp-udp-proxy-v3-udpproxyconfig) +, so ideally, Envoy Gateway should also be able to work in these two modes: + +## Non-transparent Proxy Mode +For TCP, Envoy terminates the downstream connection, connects the upstream with its own IP address, and proxies the +TCP traffic from the downstream to the upstream. + +For UDP, Envoy receives UDP packages from the downstream, and uses its own IP address as the sender IP address when +proxying the UDP packages to the upstream. + +In this mode, the upstream will see Envoy's IP address. + +## Transparent Proxy Mode +For TCP, Envoy terminates the downstream connection, connects the upstream with the downstream IP address, and proxies +the TCP traffic from the downstream to the upstream. + +For UDP, Envoy receives UDP packages from the downstream, and uses the downstream IP address as the sender IP address +when proxying the UDP packages to the upstream. + +In this mode, the upstream will see the original downstream IP address. + +Note: Even in transparent mode, the upstream can't see the port number of the downstream because Envoy doesn't forward +the port number. + +## The Implications of Transparent Proxy Mode + +### Escalated Privilege +Envoy needs to bind to the downstream IP when connecting to the upstream, which means Envoy requires escalated +CAP_NET_ADMIN privileges. This is often considered as a bad security practice and not allowed in some sensitive deployments. + +### Routing +The upstream can see the original source IP, but the original port number won't be passed, so the return +traffic from the upstream must be routed back to Envoy because only Envoy knows how to send the return traffic back +to the right port number of the downstream, which requires routing at the upstream side to be set up. +In a Kubernetes cluster, Envoy Gateway will have to carefully cooperate with CNI plugins to get the routing right. + +## The Design Decision (For Now) + +The implementation will only support proxying in non-transparent mode i.e. the backend will see the source IP and +port of the deployed Envoy instance instead of the client. diff --git a/docs/design/watching.md b/docs/latest/design/watching.md similarity index 80% rename from docs/design/watching.md rename to docs/latest/design/watching.md index 19cb785edfd..b8477a30e2d 100644 --- a/docs/design/watching.md +++ b/docs/latest/design/watching.md @@ -1,6 +1,6 @@ -## Watching the results of other components +# Watching Components Design -Envoy Gateway is made up of several components that communicate in-process. Some of them (namely providers) watch +Envoy Gateway is made up of several components that communicate in-process. Some of them (namely Providers) watch external resources, and "publish" what they see for other components to consume; others watch what another publishes and act on it (such as the resource translator watches what the providers publish, and then publishes its own results that are watched by another component). Some of these internally published results are consumed by multiple components. @@ -9,7 +9,7 @@ To facilitate this communication use the [watchable][] library. The `watchable. standard library's `sync.Map` type, but supports a `.Subscribe` (and `.SubscribeSubset`) method that promotes a pub/sub pattern. -### pub +## Pub Many of the things we communicate around are naturally named, either by a bare "name" string or by a "name"/"namespace" tuple. And because `watchable.Map` is typed, it makes sense to have one map for each type of thing (very similar to if @@ -34,7 +34,7 @@ method) the current value is a no-op; it won't trigger an event for subscribers. doesn't have as much state to keep track of; it doesn't need to know "did I already publish this thing", it can just `.Store` its data and `watchable` will do the right thing. -### sub +## Sub Meanwhile, the translator and other interested components subscribe to it with `table.Thing.Subscribe` (or `table.Thing.SubscribeSubset` if they only care about a few "Thing"s). So the translator goroutine might look like: @@ -90,14 +90,19 @@ handy way to know when to run, `.Load` and friends can be used without subscribi There can be any number of subscribers. For that matter, there can be any number of publishers `.Store`ing things, but it's probably wise to just have one publisher for each map. -The channel returned from `.Subscribe` is immediately readable with a snapshot of the map as it existed when -`.Subscribe` was called; after that initial read it becomes readable again whenever `.Store` or `.Delete` mutates the -map. If multiple mutations happen between reads, they are coalesced in to one snapshot to be read; the `snapshot.State` -is the most-recent full state, and `snapshot.Updates` is a listing of each of the mutations that cause this snapshot to -be different than the last-read one. This way subscribers don't need to worry about a backlog accumulating if they -can't keep up with the rate of changes from the publisher. +The channel returned from `.Subscribe` **is immediately readable** with a snapshot of the map as it existed when +`.Subscribe` was called; and becomes readable again whenever `.Store` or `.Delete` mutates the map. If multiple +mutations happen between reads (or if mutations happen between `.Subscribe` and the first read), they are coalesced in +to one snapshot to be read; the `snapshot.State` is the most-recent full state, and `snapshot.Updates` is a listing of +each of the mutations that cause this snapshot to be different than the last-read one. This way subscribers don't need +to worry about a backlog accumulating if they can't keep up with the rate of changes from the publisher. -### other notes +If the map contains anything before `.Subscribe` is called, that very first read won't include `snapshot.Updates` +entries for those pre-existing items; if you are working with `snapshot.Update` instead of `snapshot.State`, then you +must add special handling for your first read. We have a utility function `./internal/message.HandleSubscription` to +help with this. + +## Other Notes The common pattern will likely be that the entrypoint that launches the goroutines for each component instantiates the map, and passes them to the appropriate publishers and subscribers; same as if they were communicating via a dumb diff --git a/docs/latest/design_docs.rst b/docs/latest/design_docs.rst new file mode 100644 index 00000000000..82f87e7650b --- /dev/null +++ b/docs/latest/design_docs.rst @@ -0,0 +1,13 @@ +Design Docs +=========== + +Learn about the internal details of Envoy Gateway. + +.. toctree:: + :maxdepth: 2 + + design/system-design + design/gatewayapi-translator + design/watching + design/config-api + design/tcp-udp-design \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/docs/latest/dev/CODE_OF_CONDUCT.md similarity index 100% rename from CODE_OF_CONDUCT.md rename to docs/latest/dev/CODE_OF_CONDUCT.md diff --git a/CONTRIBUTING.md b/docs/latest/dev/CONTRIBUTING.md similarity index 100% rename from CONTRIBUTING.md rename to docs/latest/dev/CONTRIBUTING.md diff --git a/docs/latest/dev/DOCS.md b/docs/latest/dev/DOCS.md new file mode 100644 index 00000000000..fb49b9d55dd --- /dev/null +++ b/docs/latest/dev/DOCS.md @@ -0,0 +1,63 @@ +# Working on the Envoy Gateway Docs + +The documentation for the Envoy Gateway lives in the `docs/` directory. Any +individual document can be written using either [reStructuredText] or [Markdown], +you can choose the format that you're most comfortable with when working on the +documentation. + +## Documentation Structure + +We supported the versioned Docs now, the directory name under docs represents +the version of docs. The root of the latest site is in `docs/latest/index.rst`. +This is probably where to start if you're trying to understand how things fit together. + +Note that the new contents should be added to `docs/latest` and will be cut off at +the next release. The contents under `docs/v0.2.0` are auto-generated, +and usually do not need to make changes to them, unless if you find the current release pages have +some incorrect contents. If so, you should send a PR to update contents both of `docs/latest` +and `docs/v0.2.0`. + +It's important to note that a given document _must_ have a reference in some +`.. toctree::` section for the document to be reachable. Not everything needs +to be in `docs/index.rst`'s `toctree` though. + +You can access the website which represents the current release in default, +and you can access the website which contains the latest version changes in +[Here][latest-website] or at the footer of the pages. + +## Documentation Workflow + +To work with the docs, just edit reStructuredText or Markdown files in `docs`, +then run + +```bash +make docs +``` + +This will create `docs/html` with the built HTML pages. You can view the docs +either simply by pointing a web browser at the `file://` path to your +`docs/html`, or by firing up a static webserver from that directory, e.g. + +``` shell +make docs-serve +``` + +If you want to generate a new release version of the docs, like `v0.3.0`, then run + +```bash +make docs-release TAG=v0.3.0 +``` + +This will update the VERSION file at the project root, which records current release version, +and it will be used in the pages version context and binary version output. Also, this will generate +new dir `docs/v0.3.0`, which contains docs at v0.3.0 and updates artifact links to `v0.3.0` +in all files under `docs/v0.3.0/user`, like `quickstart.md`, `http-routing.md` and etc. + +## Publishing Docs + +Whenever docs are pushed to `main`, CI will publish the built docs to GitHub +Pages. For more details, see `.github/workflows/docs.yaml`. + +[reStructuredText]: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html +[Markdown]: https://daringfireball.net/projects/markdown/syntax +[latest-website]: https://gateway.envoyproxy.io/latest diff --git a/docs/latest/dev/README.md b/docs/latest/dev/README.md new file mode 100644 index 00000000000..f66661406d3 --- /dev/null +++ b/docs/latest/dev/README.md @@ -0,0 +1,139 @@ +# Developer Guide + +Envoy Gateway is built using a [make][]-based build system. Our CI is based on [Github Actions][] using [workflows][]. + +## Prerequisites + +### go + +* Version: 1.18.2 +* 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 the Envoy Gateway binary. __Note:__ The binary gets generated in the `bin/` directory + +### Testing + +* Run `make test` to run the golang tests. + +### Running Linters + +* Run `make lint` to make sure your code passes all the linter checks. + +### 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 `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. + +[Quickstart]: https://github.com/envoyproxy/gateway/blob/main/docs/user/quickstart.md +[make]: https://www.gnu.org/software/make/ +[Github Actions]: https://docs.github.com/en/actions +[workflows]: .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 diff --git a/docs/latest/dev/releasing.md b/docs/latest/dev/releasing.md new file mode 100644 index 00000000000..a6d965be92f --- /dev/null +++ b/docs/latest/dev/releasing.md @@ -0,0 +1,144 @@ +# Release Process + +This document guides maintainers through the process of creating an Envoy Gateway release. + +## Creating a Minor Release + +### Prerequisites + +- Permissions to push to the Envoy Gateway repository. + +### Set Environment Variables + +Set environment variables for use in subsequent steps: + +```shell +export MAJOR_VERSION=0 +export MINOR_VERSION=3 +export GITHUB_REMOTE=origin +``` + +1. Clone the repo, checkout the `main` branch, ensure it’s up-to-date, and your local branch is clean. +2. Create a topic branch to create the release notes and release docs. Reference previous [release notes][] for additional details. +3. Sign, commit, and push your changes to your fork and submit a [Pull Request][] to merge the changes listed below + into the `main` branch. Do not proceed until all your PRs have merged and the [Build and Test][build-and-test GitHub action] has completed for your final PR: + + 1. Add Release Announcement. + 2. Add Release Versioned Documents. + + ``` shell + make docs-release TAG=v${MAJOR_VERSION}.${MINOR_VERSION}.0 + ``` + +4. Create a new release branch from `main`. The release branch should be named + `release/v${MAJOR_VERSION}.${MINOR_VERSION}`, e.g. `release/v0.3`. + + ```shell + git checkout -b release/v${MAJOR_VERSION}.${MINOR_VERSION} + ``` + +5. Push the branch to the Envoy Gateway repo. + + ```shell + git push ${GITHUB_REMOTE} release/v${MAJOR_VERSION}.${MINOR_VERSION} + ``` + +6. Tag the head of your release branch with the release tag. For example: + + ```shell + git tag -a v${MAJOR_VERSION}.${MINOR_VERSION}.0 -m 'Envoy Gateway v${MAJOR_VERSION}.${MINOR_VERSION}.0 Release' + ``` + + __Note:__ The tag version differs from the release branch by including the `.0` patch version. + +7. Push the tag to the Envoy Gateway repository. + + ```shell + git push origin v${MAJOR_VERSION}.${MINOR_VERSION}.0 + ``` + +8. This will trigger the [release GitHub action][] that generates the release, release artifacts, etc. +9. Confirm that the [release workflow][] completed successfully. +10. Confirm that the Envoy Gateway [image][] with the correct release tag was published to Docker Hub. +11. Confirm that the [release][] was created. +12. Confirm that the steps in the [Quickstart Guide][] work as expected. +13. [Generate][] the GitHub changelog and include the following text at the beginning of the release page: + + ```console + # Release Announcement + + Check out the [v${MAJOR_VERSION}.${MINOR_VERSION} release announcement] + (https://gateway.envoyproxy.io/releases/v${MAJOR_VERSION}.${MINOR_VERSION}.html) to learn more about the release. + ``` + +If you find any bugs in this process, please create an issue. + +## Creating a Release Candidate + +### Prerequisites + +- Permissions to push to the Envoy Gateway repository. + +### Set Environment Variables + +```shell +export MAJOR_VERSION=0 +export MINOR_VERSION=3 +export RELEASE_CANDIDATE_NUMBER=1 +export GITHUB_REMOTE=origin +``` + +1. Clone the repo, checkout the `main` branch, ensure it’s up-to-date, and your local branch is clean. +2. Tag the head of the main branch with the release candidate number. + + ```shell + git tag -a v${MAJOR_VERSION}.${MINOR_VERSION}.0-rc.${RELEASE_CANDIDATE_NUMBER} -m 'Envoy Gateway v${MAJOR_VERSION}.${MINOR_VERSION}.0-rc.${RELEASE_CANDIDATE_NUMBER} Release Candidate' + ``` + +3. Push the tag to the Envoy Gateway repository. + + ```shell + git push v${MAJOR_VERSION}.${MINOR_VERSION}.0-rc.${RELEASE_CANDIDATE_NUMBER} + ``` + +4. This will trigger the [release GitHub action][] that generates the release, release artifacts, etc. +5. Confirm that the [release workflow][] completed successfully. +6. Confirm that the Envoy Gateway [image][] with the correct release tag was published to Docker Hub. +7. Confirm that the [release][] was created. +8. Note that the [Quickstart Guide][] references are __not__ updated for release candidates. However, test + the quickstart steps using the release candidate by manually updating the links. +9. [Generate][] the GitHub changelog. +10. Ensure you check the "This is a pre-release" checkbox when editing the GitHub release. +11. If you find any bugs in this process, please create an issue. + +## Announcing the Release + +It's important that the world knows about the release. Use the following steps to announce the release. + +1. Set the release information in the Envoy Gateway Slack channel. For example: + + ```shell + Envoy Gateway v${MAJOR_VERSION}.${MINOR_VERSION} has been released: https://github.com/envoyproxy/gateway/releases/tag/v${MAJOR_VERSION}.${MINOR_VERSION}.0 + ``` + +2. Send a message to the Envoy Gateway Slack channel. For example: + + ```shell + On behalf of the entire Envoy Gateway community, I am pleased to announce the release of Envoy Gateway + v${MAJOR_VERSION}.${MINOR_VERSION}. A big thank you to all the contributors that made this release possible. + Refer to the official v${MAJOR_VERSION}.${MINOR_VERSION} announcement for release details and the project docs + to start using Envoy Gateway. + ... + ``` + + Link to the GitHub release and release announcement page that highlights the release. + +[release notes]: https://github.com/envoyproxy/gateway/tree/main/release-notes +[Pull Request]: https://github.com/envoyproxy/gateway/pulls +[Quickstart Guide]: https://github.com/envoyproxy/gateway/blob/main/docs/user/quickstart.md +[build-and-test GitHub action]: https://github.com/envoyproxy/gateway/blob/main/.github/workflows/build_and_test.yaml +[release GitHub action]: https://github.com/envoyproxy/gateway/blob/main/.github/workflows/release.yaml +[release workflow]: https://github.com/envoyproxy/gateway/actions/workflows/release.yaml +[image]: https://hub.docker.com/r/envoyproxy/gateway/tags +[release]: https://github.com/envoyproxy/gateway/releases +[Generate]: https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes diff --git a/docs/latest/dev_docs.rst b/docs/latest/dev_docs.rst new file mode 100644 index 00000000000..88a0b367f01 --- /dev/null +++ b/docs/latest/dev_docs.rst @@ -0,0 +1,13 @@ +Developer Docs +============== + +Learn how to contribute to Envoy Gateway. + +.. toctree:: + :maxdepth: 2 + + dev/CODE_OF_CONDUCT + dev/CONTRIBUTING + dev/README + dev/DOCS + dev/releasing diff --git a/docs/get_involved.rst b/docs/latest/get_involved.rst similarity index 94% rename from docs/get_involved.rst rename to docs/latest/get_involved.rst index cd4a70b07ce..f17febd5651 100644 --- a/docs/get_involved.rst +++ b/docs/latest/get_involved.rst @@ -1,4 +1,4 @@ -Getting involved +Getting Involved ================ We welcome contributions from the community. Please carefully review the diff --git a/docs/images/architecture.png b/docs/latest/images/architecture.png similarity index 100% rename from docs/images/architecture.png rename to docs/latest/images/architecture.png diff --git a/docs/latest/index.rst b/docs/latest/index.rst new file mode 100644 index 00000000000..39f345fa125 --- /dev/null +++ b/docs/latest/index.rst @@ -0,0 +1,32 @@ +`Envoy Gateway `_ +============= + +Release: |version| + +.. image:: https://img.shields.io/badge/slack-join-orange.svg + :target: https://envoyproxy.slack.com/archives/C03E6NHLESV + :alt: Join the Envoy Slack + +Envoy Gateway is an open source project for managing `Envoy Proxy`_ as a standalone or Kubernetes-based application +gateway. `Gateway API`_ resources are used to dynamically provision and configure the managed Envoy Proxies. Whether +you are interested in using or contributing to Envoy Gateway, the following resources will help you get started: + +.. toctree:: + :maxdepth: 1 + + intro/compatibility + user_docs + design_docs + dev_docs + releases + roadmap + about_docs + get_involved + +.. note:: + + This project is under active development. Many, many features are not + complete. We would love for you to :doc:`get involved`. + +.. _Envoy Proxy: https://www.envoyproxy.io/ +.. _Gateway API: https://gateway-api.sigs.k8s.io/ diff --git a/docs/intro/compatibility.rst b/docs/latest/intro/compatibility.rst similarity index 100% rename from docs/intro/compatibility.rst rename to docs/latest/intro/compatibility.rst diff --git a/docs/latest/releases.rst b/docs/latest/releases.rst new file mode 100644 index 00000000000..090c6707fd2 --- /dev/null +++ b/docs/latest/releases.rst @@ -0,0 +1,10 @@ +Releases +======== + +Learn more about Envoy Gateway releases. + +.. toctree:: + :maxdepth: 1 + + releases/README + releases/v0.2 diff --git a/docs/latest/releases/README.md b/docs/latest/releases/README.md new file mode 100644 index 00000000000..93d3366efb0 --- /dev/null +++ b/docs/latest/releases/README.md @@ -0,0 +1,41 @@ +# Release Details + +This document provides details for Envoy Gateway releases. Envoy Gateway follows the Semantic Versioning [v2.0.0 spec][] +for release versioning. Since Envoy Gateway is a new project, minor releases are the only defined releases. Envoy +Gateway maintainers will establish additional release details, e.g. patch releases, at a future date. + +## Stable Releases + +Stable releases of Envoy Gateway include: + +* Minor Releases- A new release branch and corresponding tag are created from the `main` branch. A minor release + is supported for 6 months following the release date. As the project matures, Envoy Gateway maintainers will reassess + the support timeframe. + +Minor releases happen quarterly and follow the schedule below. + +## Release Management + +Minor releases are handled by a designated Envoy Gateway maintainer. This maintainer is considered the Release Manager +for the release. The details for creating a release are outlined in the [release guide][]. The Release Manager is +responsible for coordinating the overall release. This includes identifying issues to be fixed in the release, +communications with the Envoy Gateway community, and the mechanics of the release. + +| Quarter | Release Manager | +|:-------:|:--------------------------------------------------------------:| +| 2022 Q4 | Daneyon Hansen ([danehans](https://github.com/danehans)) | +| 2023 Q1 | TBD | + +## Release Schedule + +In order to align with the Envoy Proxy [release schedule][], Envoy Gateway releases are produced on a fixed schedule +(the 22nd day of each quarter), with an acceptable delay of up to 2 weeks, and a hard deadline of 3 weeks. + +| Version | Expected | Actual | Difference | End of Life | +|:-------:|:-----------:|:-----------:|:----------:|:-----------:| +| 0.2.0 | 2022/10/22 | 2022/10/20 | -2 day | 2023/4/20 | +| 0.3.0 | 2023/01/22 | | | | + +[v2.0.0 spec]: https://semver.org/spec/v2.0.0.html +[release guide]: ../dev/releasing.md +[release schedule]: https://github.com/envoyproxy/envoy/blob/main/RELEASES.md#major-release-schedule diff --git a/docs/latest/releases/v0.2.md b/docs/latest/releases/v0.2.md new file mode 100644 index 00000000000..a0dc0e885de --- /dev/null +++ b/docs/latest/releases/v0.2.md @@ -0,0 +1,50 @@ +--- +title: Announcing Envoy Gateway v0.2 +linktitle: v0.2 +subtitle: Major Update +description: Envoy Gateway v0.2 release announcement. +publishdate: 2022-10-20 +release: v0.2.0 +skip_list: true +aliases: +- /releases/v0.2 +- /releases/v0.2.0 +--- +# Envoy Gateway Release v0.2 + +We are pleased to announce the release of Envoy Gateway v0.2! + +This is the first functional release of Envoy Gateway. We would like to thank the entire Envoy Gateway community for +helping publish the release. + +| [Release Notes][] | [Docs][docs] | [Compatibility Matrix][matrix] | [Download][] | +|-------------------|--------------|--------------------------------|--------------| + +## What's New + +The release adds a ton of features and functionality. Here are some highlights: + +### Kubernetes Support + +Run Envoy Gateway in a Kubernetes cluster. Checkout the [quickstart guide][] to get started with Envoy Gateway in a few +simple steps. + +### Gateway API Support + +Envoy Gateway supports Gateway API resources for running and configuring a managed fleet of Envoy proxies. Envoy Gateway +passes Gateway API core [conformance tests][] and supports GatewayClass, Gateway, HTTPRoute, and TLSRoute resources. See +the [documentation][docs] for additional details on how to use Envoy Gateway for your edge proxy and API gateway needs. + +## Envoy Gateway at EnvoyCon NA + +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 +[matrix]: https://gateway.envoyproxy.io/intro/compatibility.html +[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]: https://gateway.envoyproxy.io/user/quickstart.html +[EnvoyCon NA]: https://events.linuxfoundation.org/envoycon-north-america/program/schedule/ +[our talk]: https://sched.co/1AO5S diff --git a/docs/latest/roadmap.rst b/docs/latest/roadmap.rst new file mode 100644 index 00000000000..711b6245503 --- /dev/null +++ b/docs/latest/roadmap.rst @@ -0,0 +1,9 @@ +Roadmap +======= + +Learn about the future direction of Envoy Gateway. + +.. toctree:: + :maxdepth: 2 + + design/roadmap diff --git a/docs/latest/user/http-redirect.md b/docs/latest/user/http-redirect.md new file mode 100644 index 00000000000..c11b346b592 --- /dev/null +++ b/docs/latest/user/http-redirect.md @@ -0,0 +1,123 @@ +# 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][]. + +Follow the steps from the [Secure Gateways](secure-gateways.md) to install Envoy Gateway and the example manifest. Do +not proceed until you can curl the example backend from the Quickstart guide 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"`: + +```shell +cat < 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. + +```shell +cat < 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. + +```shell +cat < 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 separate filters on the same HTTPRoute and they will all perform as expected + +```shell +cat < 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: + +```shell +cat < 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. + +```shell +cat < 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/references/spec/#gateway.networking.k8s.io/v1beta1.BackendRef diff --git a/docs/latest/user/quickstart.md b/docs/latest/user/quickstart.md new file mode 100644 index 00000000000..993003e2419 --- /dev/null +++ b/docs/latest/user/quickstart.md @@ -0,0 +1,81 @@ +# Quickstart + +This guide will help you get started with Envoy Gateway in a few simple steps. + +## Prerequisites + +A Kubernetes cluster. + +__Note:__ Envoy Gateway is tested against Kubernetes v1.24.0. + +## Installation + +Install the Gateway API CRDs and Envoy Gateway: + +```shell +kubectl apply -f https://github.com/envoyproxy/gateway/releases/download/latest/install.yaml +``` + +Install the GatewayClass, Gateway, HTTPRoute and example app: + +```shell +kubectl apply -f https://github.com/envoyproxy/gateway/releases/download/latest/quickstart.yaml +``` + +## 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:8080 & +``` + +Curl the example app through Envoy proxy: + +```shell +curl --verbose --header "Host: www.example.com" http://localhost:8888/get +``` + +### 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 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 example app through Envoy proxy: + +```shell +curl --verbose --header "Host: www.example.com" http://$GATEWAY_HOST:8080/get +``` + +## Clean-Up + +Use the steps in this section to uninstall everything from the quickstart guide. + +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 +kubectl delete -f https://github.com/envoyproxy/gateway/releases/download/latest/install.yaml --ignore-not-found=true +``` + +## Next Steps + +Checkout the [Developer Guide](../dev/README.md) to get involved in the project. diff --git a/docs/latest/user/secure-gateways.md b/docs/latest/user/secure-gateways.md new file mode 100644 index 00000000000..9fec46c59bf --- /dev/null +++ b/docs/latest/user/secure-gateways.md @@ -0,0 +1,260 @@ +# Secure Gateways + +This guide will help you get started using secure Gateways. The guide uses a self-signed CA, so it should be used for +testing and demonstration purposes only. + +## Prerequisites + +- A Kubernetes cluster with `kubectl` context configured for the cluster. +- OpenSSL to generate TLS assets. + +__Note:__ Envoy Gateway is tested against Kubernetes v1.24. + +## Installation + +Follow the steps from the [Quickstart Guide](quickstart.md) 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 guide to include an HTTPS listener that listens on port `8443` 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": 8443, + "tls": { + "mode": "Terminate", + "certificateRefs": [{ + "kind": "Secret", + "group": "", + "name": "example-cert", + }], + }, + }, +}]' +``` + +Verify the Gateway status: + +```shell +kubectl get gateway/eg -o yaml +``` + +## Testing + +### Clusters without External 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} 8043:8443 & +``` + +Query the example app through Envoy proxy: + +```shell +curl -v -HHost:www.example.com --resolve "www.example.com:8043:127.0.0.1" \ +--cacert example.com.crt https://www.example.com:8043/get +``` + +### Clusters 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:8443:${GATEWAY_HOST}" \ +--cacert example.com.crt https://www.example.com:8443/get +``` + +## 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": 8443, + "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: + +```console +$ cat < /etc/envoy-gateway/config.yaml +apiVersion: config.gateway.envoyproxy.io/v1alpha1 +kind: EnvoyGateway +provider: + type: Kubernetes + kubernetes: {} +EOF +``` + +This configuration will cause Envoy Gateway to use the Kubernetes provider with default configuration parameters. + +The Kubernetes provider can be configured using the `provider` field. For example, the `foo` field can be set to "bar": + +```yaml +$ cat << EOF > /etc/envoy-gateway/config.yaml +apiVersion: config.gateway.envoyproxy.io/v1alpha1 +kind: EnvoyGateway +provider: + type: Kubernetes + kubernetes: + foo: bar +EOF +``` + +__Note:__ The Provider API from the Kubernetes package is currently undefined and `foo: bar` is provided for +illustration purposes only. + +The same API structure is followed for each supported provider. The following example causes Envoy Gateway to use the +File provider: + +```yaml +$ cat << EOF > /etc/envoy-gateway/config.yaml +apiVersion: config.gateway.envoyproxy.io/v1alpha1 +kind: EnvoyGateway +provider: + type: File + file: + foo: bar +EOF +``` + +__Note:__ The Provider API from the File package is currently undefined and `foo: bar` is provided for illustration +purposes only. + +Gateway API-related configuration is expressed through the `gateway` field. If unspecified, Envoy Gateway will use +default configuration parameters for `gateway`. The following example causes the [GatewayClass][gc] controller to +manage GatewayClasses with controllerName `foo` instead of the default `gateway.envoyproxy.io/gatewayclass-controller`: + +```yaml +$ cat << EOF > /etc/envoy-gateway/config.yaml +apiVersion: config.gateway.envoyproxy.io/v1alpha1 +kind: EnvoyGateway +gateway: + controllerName: foo +``` + +With any of the above configuration examples, Envoy Gateway can be started without any additional arguments: + +```shell +$ ./envoy-gateway +``` + +## Data Plane API + +The data plane is configured dynamically through Kubernetes resources, primarily [Gateway API][gw_api] objects. +Optionally, the data plane infrastructure can be configured by referencing a [custom resource (CR)][cr] through +`spec.parametersRef` of the managed GatewayClass. The `EnvoyProxy` API defines the data plane infrastructure +configuration and is represented as the CR referenced by the managed GatewayClass. Key points of this API are: + +* If unreferenced by `gatewayclass.spec.parametersRef`, default parameters will be used to configure the data plane + infrastructure, e.g. expose Envoy network endpoints using a LoadBalancer service. +* Envoy Gateway will follow Gateway API [recommendations][gc] regarding updates to the EnvoyProxy CR: + > It is recommended that this resource be used as a template for Gateways. This means that a Gateway is based on the + > state of the GatewayClass at the time it was created and changes to the GatewayClass or associated parameters are + > not propagated down to existing Gateways. + +The initial `EnvoyProxy` API: + +```go +// gateway/api/config/v1alpha1/envoyproxy.go + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EnvoyProxy is the Schema for the envoyproxies API. +type EnvoyProxy struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec EnvoyProxySpec `json:"spec,omitempty"` + Status EnvoyProxyStatus `json:"status,omitempty"` +} + +// EnvoyProxySpec defines the desired state of Envoy Proxy infrastructure +// configuration. +type EnvoyProxySpec struct { + // Undefined by this design spec. +} + +// EnvoyProxyStatus defines the observed state of EnvoyProxy. +type EnvoyProxyStatus struct { + // Undefined by this design spec. +} +``` + +The EnvoyProxySpec and EnvoyProxyStatus fields will be defined in the future as proxy infrastructure configuration use +cases are better understood. + +### Data Plane Configuration + +GatewayClass and Gateway resources define the data plane infrastructure. Note that all examples assume Envoy Gateway is +running with the Kubernetes provider. + +```yaml +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: GatewayClass +metadata: + name: example-class +spec: + controllerName: gateway.envoyproxy.io/gatewayclass-controller +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: Gateway +metadata: + name: example-gateway +spec: + gatewayClassName: example-class + listeners: + - name: http + protocol: HTTP + port: 80 +``` + +Since the GatewayClass does not define `spec.parametersRef`, the data plane is provisioned using default configuration +parameters. The Envoy proxies will be configured with a http listener and a Kubernetes LoadBalancer service listening +on port 80. + +The following example will configure the data plane to use a ClusterIP service instead of the default LoadBalancer +service: + +```yaml +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: GatewayClass +metadata: + name: example-class +spec: + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parametersRef: + name: example-config + group: config.gateway.envoyproxy.io + kind: EnvoyProxy +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: Gateway +metadata: + name: example-gateway +spec: + gatewayClassName: example-class + listeners: + - name: http + protocol: HTTP + port: 80 +--- +apiVersion: config.gateway.envoyproxy.io/v1alpha1 +kind: EnvoyProxy +metadata: + name: example-config +spec: + networkPublishing: + type: ClusterIPService +``` + +__Note:__ The NetworkPublishing API is currently undefined and is provided here for illustration purposes only. + +[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 +[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/docs/v0.2.0/design/gatewayapi-translator.md b/docs/v0.2.0/design/gatewayapi-translator.md new file mode 100644 index 00000000000..1480c5f4257 --- /dev/null +++ b/docs/v0.2.0/design/gatewayapi-translator.md @@ -0,0 +1,250 @@ +# Gateway API Translator Design + +The Gateway API translates external resources, e.g. GatewayClass, from the configured Provider to the Intermediate +Representation (IR). + +## Assumptions + +Initially target core conformance features only, to be followed by extended conformance features. + +## Inputs and Outputs + +The main inputs to the Gateway API translator are: + +- GatewayClass, Gateway, HTTPRoute, TLSRoute, Service, ReferenceGrant, Namespace, and Secret resources. + +__Note:__ ReferenceGrant is not fully implemented as of v0.2. + +The outputs of the Gateway API translator are: + +- Xds and Infra Internal Representations (IRs). +- Status updates for GatewayClass, Gateways, HTTPRoutes + +## Listener Compatibility + +Envoy Gateway follows Gateway API listener compatibility spec: +> Each listener in a Gateway must have a unique combination of Hostname, Port, and Protocol. An implementation MAY group +> Listeners by Port and then collapse each group of Listeners into a single Listener if the implementation determines +> that the Listeners in the group are “compatible”. + +__Note:__ Envoy Gateway does not collapse listeners across multiple Gateways. + +### Listener Compatibility Examples + +#### Example 1: Gateway with compatible Listeners (same port & protocol, different hostnames) + +```yaml +kind: Gateway +apiVersion: gateway.networking.k8s.io/v1beta1 +metadata: + name: gateway-1 + namespace: envoy-gateway +spec: + gatewayClassName: envoy-gateway + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All + hostname: *.envoygateway.io + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All + hostname: whales.envoygateway.io +``` + +#### Example 2: Gateway with compatible Listeners (same port & protocol, one hostname specified, one not) + +```yaml +kind: Gateway +apiVersion: gateway.networking.k8s.io/v1beta1 +metadata: + name: gateway-1 + namespace: envoy-gateway +spec: + gatewayClassName: envoy-gateway + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All + hostname: *.envoygateway.io + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All +``` + +#### Example 3: Gateway with incompatible Listeners (same port, protocol and hostname) + +```yaml +kind: Gateway +apiVersion: gateway.networking.k8s.io/v1beta1 +metadata: + name: gateway-1 + namespace: envoy-gateway +spec: + gatewayClassName: envoy-gateway + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All + hostname: whales.envoygateway.io + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All + hostname: whales.envoygateway.io +``` + +#### Example 4: Gateway with incompatible Listeners (neither specify a hostname) + +```yaml +kind: Gateway +apiVersion: gateway.networking.k8s.io/v1beta1 +metadata: + name: gateway-1 + namespace: envoy-gateway +spec: + gatewayClassName: envoy-gateway + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All +``` + +## Computing Status + +Gateway API specifies a rich set of status fields & conditions for each resource. To achieve conformance, Envoy Gateway +must compute the appropriate status fields and conditions for managed resources. + +Status is computed and set for: + +- The managed GatewayClass (`gatewayclass.status.conditions`). +- Each managed Gateway, based on its Listeners' status (`gateway.status.conditions`). For the Kubernetes provider, the + Envoy Deployment and Service status are also included to calculate Gateway status. +- Listeners for each Gateway (`gateway.status.listeners`). +- The ParentRef for each Route (`route.status.parents`). + +The Gateway API translator is responsible for calculating status conditions while translating Gateway API resources to +the IR and publishing status over the [message bus][]. The Status Manager subscribes to these status messages and +updates the resource status using the configured provider. For example, the Status Manager uses a Kubernetes client to +update resource status on the Kubernetes API server. + +## Outline + +The following roughly outlines the translation process. Each step may produce (1) IR; and (2) status updates on Gateway +API resources. + +1. Process Gateway Listeners + - Validate unique hostnames, ports, and protocols. + - Validate and compute supported kinds. + - Validate allowed namespaces (validate selector if specified). + - Validate TLS fields if specified, including resolving referenced Secrets. + +2. Process HTTPRoutes + - foreach route rule: + - compute matches + - [core] path exact, path prefix + - [core] header exact + - [extended] query param exact + - [extended] HTTP method + - compute filters + - [core] request header modifier (set/add/remove) + - [core] request redirect (hostname, statuscode) + - [extended] request mirror + - compute backends + - [core] Kubernetes services + - foreach route parent ref: + - get matching listeners (check Gateway, section name, listener validation status, listener allowed routes, hostname intersection) + - foreach matching listener: + - foreach hostname intersection with route: + - add each computed route rule to host + +## Context Structs + +To help store, access and manipulate information as it's processed during the translation process, a set of context +structs are used. These structs wrap a given Gateway API type, and add additional fields and methods to support +processing. + +`GatewayContext` wraps a Gateway and provides helper methods for setting conditions, accessing Listeners, etc. + +```go +type GatewayContext struct { + // The managed Gateway + *v1beta1.Gateway + + // A list of Gateway ListenerContexts. + listeners []*ListenerContext +} +``` + +`ListenerContext` wraps a Listener and provides helper methods for setting conditions and other status information on +the associated Gateway. + +```go +type ListenerContext struct { + // The Gateway listener. + *v1beta1.Listener + + // The Gateway this Listener belongs to. + gateway *v1beta1.Gateway + + // An index used for managing this listener in the list of Gateway listeners. + listenerStatusIdx int + + // Only Routes in namespaces selected by the selector may be attached + // to the Gateway this listener belongs to. + namespaceSelector labels.Selector + + // The TLS Secret for this Listener, if applicable. + tlsSecret *v1.Secret +} +``` + +`RouteContext` represents a generic Route object (HTTPRoute, TLSRoute, etc.) that can reference Gateway objects. + +```go +type RouteContext interface { + client.Object + + // GetRouteType returns the Kind of the Route object, HTTPRoute, + // TLSRoute, TCPRoute, UDPRoute etc. + GetRouteType() string + + // GetHostnames returns the hosts targeted by the Route object. + GetHostnames() []string + + // GetParentReferences returns the ParentReference of the Route object. + GetParentReferences() []v1beta1.ParentReference + + // GetRouteParentContext returns RouteParentContext by using the Route + // objects' ParentReference. + GetRouteParentContext(forParentRef v1beta1.ParentReference) *RouteParentContext +} +``` + +[message bus]: watching.md diff --git a/docs/design/ROADMAP.md b/docs/v0.2.0/design/roadmap.md similarity index 96% rename from docs/design/ROADMAP.md rename to docs/v0.2.0/design/roadmap.md index 1d5d942d83e..d6ec649e4a2 100644 --- a/docs/design/ROADMAP.md +++ b/docs/v0.2.0/design/roadmap.md @@ -1,4 +1,4 @@ -## Introduction +# Roadmap This document serves as a high-level reference for Envoy Gateway users and contributors to understand the direction of the project. @@ -16,7 +16,8 @@ the project. If you don't know where to start contributing, help is needed to reduce technical, automation, and documentation debt. Look for issues with the `help wanted` label to get started. -## Roadmap +## Details + Roadmap features and timelines may change based on feedback, community contributions, etc. If you depend on a specific roadmap item, you're encouraged to attend a community meeting to discuss the details, or help us deliver the feature by contributing to the project. @@ -24,6 +25,7 @@ contributing to the project. `Last Updated: October 2022` ### [v0.2.0][v0.2.0]: Establish a Solid Foundation + - Complete the core Envoy Gateway implementation- [Issue #60][60]. - Establish initial testing, e2e, integration, etc- [Issue #64][64]. - Establish user and developer project documentation- [Issue #17][17]. @@ -31,17 +33,18 @@ contributing to the project. - Setup a CI/CD pipeline- [Issue #63][63]. ### [v0.3.0][v0.3.0]: Drive Advanced Features through Extension Mechanisms + - Global Rate Limiting - AuthN/AuthZ- [Issue #336][336]. - Lets Encrypt Integration ### [v0.4.0][v0.4.0]: Manageability and Scale + - Tooling for devs/infra admins to aid in managing/maintaining EG - Support advanced provisioning use cases (e.g. multi-cluster, serverless, etc.) - Perf testing (EG specifically) - Support for Chaos engineering? -[eg_board]: https://github.com/orgs/envoyproxy/projects/1/views/1?layout=board [issue]: https://github.com/envoyproxy/gateway/issues [meeting]: https://docs.google.com/document/d/1leqwsHX8N-XxNEyTflYjRur462ukFxd19Rnk3Uzy55I/edit?usp=sharing [pr]: https://github.com/envoyproxy/gateway/compare diff --git a/docs/v0.2.0/design/system-design.md b/docs/v0.2.0/design/system-design.md new file mode 100644 index 00000000000..731cb0925b0 --- /dev/null +++ b/docs/v0.2.0/design/system-design.md @@ -0,0 +1,171 @@ +# System Design + +## Goals + +* Define the system components needed to satisfy the requirements of Envoy Gateway. + +## Non-Goals + +* Create a detailed design and interface specification for each system component. + +## Terminology + +* Control Plane- A collection of inter-related software components for providing application gateway and routing + functionality. The control plane is implemented by Envoy Gateway and provides services for managing the data plane. + These services are detailed in the [components](#components) section. +* Data Plane- Provides intelligent application-level traffic routing and is implemented as one or more Envoy proxies. + +## Architecture + +![Architecture](../images/architecture.png) + +## Configuration + +Envoy Gateway is configured statically at startup and the managed data plane is configured dynamically through +Kubernetes resources, primarily [Gateway API][gw_api] objects. + +### Static Configuration + +Static configuration is used to configure Envoy Gateway at startup, i.e. change the GatewayClass controllerName, +configure a Provider, etc. Currently, Envoy Gateway only supports configuration through a configuration file. If the +configuration file is not provided, Envoy Gateway starts-up with default configuration parameters. + +### Dynamic Configuration + +Dynamic configuration is based on the concept of a declaring the desired state of the data plane and using +reconciliation loops to drive the actual state toward the desired state. The desired state of the data plane is +defined as Kubernetes resources that provide the following services: + +* Infrastructure Management- Manage the data plane infrastructure, i.e. deploy, upgrade, etc. This configuration is + expressed through [GatewayClass][gc] and [Gateway][gw] resources. The `EnvoyProxy` [Custom Resource][cr] can be + referenced by `gatewayclass.spec.parametersRef` to modify data plane infrastructure default parameters, + e.g. expose Envoy network endpoints using a NodePort service instead of a LoadBalancer service. +* Traffic Routing- Define how to handle application-level requests to backend services. For example, route all HTTP + requests for "www.example.com" to a backend service running a web server. This configuration is expressed through + [HTTPRoute][hroute] and [TLSRoute][troute] resources that match, filter, and route traffic to a [backend][be]. + Although a backend can be any valid Kubernetes Group/Kind resource, Envoy Gateway only supports a [Service][svc] + reference. + +## Components + +Envoy Gateway is made up of several components that communicate in-process; how this communication happens is described +in the [Watching Components Design][wcd]. + +### Provider + +A Provider is an infrastructure component that Envoy Gateway calls to establish its runtime configuration, resolve +services, persist data, etc. As of v0.2, Kubernetes is the only implemented provider. A file provider is on the roadmap +via [Issue #37][]. Other providers can be added in the future as Envoy Gateway use cases are better understood. A +provider is configured at start up through Envoy Gateway's [static configuration](#static-configuration). + +#### Kubernetes Provider + +* Uses Kubernetes-style controllers to reconcile Kubernetes resources that comprise the + [dynamic configuration](#dynamic-configuration). +* Manages the data plane through Kubernetes API CRUD operations. +* Uses Kubernetes for Service discovery. +* Uses etcd (via Kubernetes API) to persist data. + +#### File Provider + +* Uses a file watcher to watch files in a directory that define the data plane configuration. +* Manages the data plane by calling internal APIs, e.g. `CreateDataPlane()`. +* Uses the host's DNS for Service discovery. +* If needed, the local filesystem is used to persist data. + +### Resource Watcher + +The Resource Watcher watches resources used to establish and maintain Envoy Gateway's dynamic configuration. The +mechanics for watching resources is provider-specific, e.g. informers, caches, etc. are used for the Kubernetes +provider. The Resource Watcher uses the configured provider for input and provides resources to the Resource Translator +as output. + +### Resource Translator + +The Resource Translator translates external resources, e.g. GatewayClass, from the Resource Watcher to the Intermediate +Representation (IR). It is responsible for: + +* Translating infrastructure-specific resources/fields from the Resource Watcher to the Infra IR. +* Translating proxy configuration resources/fields from the Resource Watcher to the xDS IR. + +__Note:__ The Resource Translator is implemented as the `Translator` API type in the `gatewayapi` package. + +### Intermediate Representation (IR) + +The Intermediate Representation defines internal data models that external resources are translated into. This allows +Envoy Gateway to be decoupled from the external resources used for dynamic configuration. The IR consists of an Infra IR +used as input for the Infra Manager and an xDS IR used as input for the xDS Translator. + +* Infra IR- Used as the internal definition of the managed data plane infrastructure. +* xDS IR- Used as the internal definition of the managed data plane xDS configuration. + +### xDS Translator + +The xDS Translator translates the xDS IR into xDS Resources that are consumed by the xDS server. + +### xDS Server + +The xDS Server is a xDS gRPC Server based on [Go Control Plane][go_cp]. Go Control Plane implements the Delta xDS Server +Protocol and is responsible for using xDS to configure the data plane. + +### Infra Manager + +The Infra Manager is a provider-specific component responsible for managing the following infrastructure: + +* Data Plane - Manages all the infrastructure required to run the managed Envoy proxies. For example, CRUD Deployment, + Service, etc. resources to run Envoy in a Kubernetes cluster. +* Auxiliary Control Planes - Optional infrastructure needed to implement application Gateway features that require + external integrations with the managed Envoy proxies. For example, [Global Rate Limiting][grl] requires provisioning + and configuring the [Envoy Rate Limit Service][rls] and the [Rate Limit filter][rlf]. Such features are exposed to + users through the [Custom Route Filters][crf] extension. + +The Infra Manager consumes the Infra IR as input to manage the data plane infrastructure. + +## Design Decisions + +* Envoy Gateway consumes one [GatewayClass][gc] by comparing its configured controller name with + `spec.controllerName` of a GatewayClass. If multiple GatewayClasses exist with the same `spec.controllerName`, Envoy + Gateway follows Gateway API [guidelines][gwapi_conflicts] to resolve the conflict. + `gatewayclass.spec.parametersRef` refers to the `EnvoyProxy` custom resource for configuring the managed proxy + infrastructure. If unspecified, default configuration parameters are used for the managed proxy infrastructure. +* Envoy Gateway manages [Gateways][gw] that reference its GatewayClass. + * A Gateway resource causes Envoy Gateway to provision managed Envoy proxy infrastructure. + * Envoy Gateway groups Listeners by Port and collapses each group of Listeners into a single Listener if the Listeners + in the group are compatible. Envoy Gateway considers Listeners to be compatible if all the following conditions are + met: + * Either each Listener within the group specifies the “HTTP” Protocol or each Listener within the group specifies + either the “HTTPS” or “TLS” Protocol. + * Each Listener within the group specifies a unique "Hostname". + * As a special case, one Listener within a group may omit "Hostname", in which case this Listener matches when no + other Listener matches. + * Envoy Gateway does __not__ merge listeners across multiple Gateways. +* Envoy Gateway follows Gateway API [guidelines][gwapi_conflicts] to resolve any conflicts. + * A Gateway `listener` corresponds to an Envoy proxy [Listener][listener]. +* An [HTTPRoute][hroute] resource corresponds to an Envoy proxy [Route][route]. + * Each [backendRef][be_ref] corresponds to an Envoy proxy [Cluster][cluster]. +* The goal is to make Envoy Gateway components extensible in the future. See the [roadmap][] for additional details. + +The draft for this document is [here][draft_design]. + +[gw_api]: https://gateway-api.sigs.k8s.io +[gc]: https://gateway-api.sigs.k8s.io/concepts/api-overview/#gatewayclass +[gw]: https://gateway-api.sigs.k8s.io/concepts/api-overview/#gateway +[hroute]: https://gateway-api.sigs.k8s.io/concepts/api-overview/#httproute +[troute]: https://gateway-api.sigs.k8s.io/concepts/api-overview/#tlsroute +[go_cp]: https://github.com/envoyproxy/go-control-plane +[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 +[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 +[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 +[svc]: https://kubernetes.io/docs/concepts/services-networking/service/ +[ wcd ]: ./watching.md +[Issue #37]: https://github.com/envoyproxy/gateway/issues/37 +[roadmap]: roadmap.md diff --git a/docs/v0.2.0/design/watching.md b/docs/v0.2.0/design/watching.md new file mode 100644 index 00000000000..b8477a30e2d --- /dev/null +++ b/docs/v0.2.0/design/watching.md @@ -0,0 +1,117 @@ +# Watching Components Design + +Envoy Gateway is made up of several components that communicate in-process. Some of them (namely Providers) watch +external resources, and "publish" what they see for other components to consume; others watch what another publishes and +act on it (such as the resource translator watches what the providers publish, and then publishes its own results that +are watched by another component). Some of these internally published results are consumed by multiple components. + +To facilitate this communication use the [watchable][] library. The `watchable.Map` type is very similar to the +standard library's `sync.Map` type, but supports a `.Subscribe` (and `.SubscribeSubset`) method that promotes a pub/sub +pattern. + +## Pub + +Many of the things we communicate around are naturally named, either by a bare "name" string or by a "name"/"namespace" +tuple. And because `watchable.Map` is typed, it makes sense to have one map for each type of thing (very similar to if +we were using native Go `map`s). For example, a struct that might be written to by the Kubernetes provider, and read by +the IR translator: + + ```go + type ResourceTable struct { + // gateway classes are cluster-scoped; no namespace + GatewayClasses watchable.Map[string, *gwapiv1b1.GatewayClass] + + // gateways are namespace-scoped, so use a k8s.io/apimachinery/pkg/types.NamespacedName as the map key. + Gateways watchable.Map[types.NamespacedName, *gwapiv1b1.Gateway] + + HTTPRoutes watchable.Map[types.NamespacedName, *gwapiv1b1.HTTPRoute] + } + ``` + +The Kubernetes provider updates the table by calling `table.Thing.Store(name, val)` and `table.Thing.Delete(name)`; +updating a map key with a value that is deep-equal (usually `reflect.DeepEqual`, but you can implement your own `.Equal` +method) the current value is a no-op; it won't trigger an event for subscribers. This is handy so that the publisher +doesn't have as much state to keep track of; it doesn't need to know "did I already publish this thing", it can just +`.Store` its data and `watchable` will do the right thing. + +## Sub + +Meanwhile, the translator and other interested components subscribe to it with `table.Thing.Subscribe` (or +`table.Thing.SubscribeSubset` if they only care about a few "Thing"s). So the translator goroutine might look like: + + ```go + func(ctx context.Context) error { + for snapshot := range k8sTable.HTTPRoutes.Subscribe(ctx) { + fullState := irInput{ + GatewayClasses: k8sTable.GatewayClasses.LoadAll(), + Gateways: k8sTable.Gateways.LoadAll(), + HTTPRoutes: snapshot.State, + } + translate(irInput) + } + } + ``` + +Or, to watch multiple maps in the same loop: + + ```go + func worker(ctx context.Context) error { + classCh := k8sTable.GatewayClasses.Subscribe(ctx) + gwCh := k8sTable.Gateways.Subscribe(ctx) + routeCh := k8sTable.HTTPRoutes.Subscribe(ctx) + for ctx.Err() == nil { + var arg irInput + select { + case snapshot := <-classCh: + arg.GatewayClasses = snapshot.State + case snapshot := <-gwCh: + arg.Gateways = snapshot.State + case snapshot := <-routeCh: + arg.Routes = snapshot.State + } + if arg.GateWayClasses == nil { + arg.GatewayClasses = k8sTable.GateWayClasses.LoadAll() + } + if arg.GateWays == nil { + arg.Gateways = k8sTable.GateWays.LoadAll() + } + if arg.HTTPRoutes == nil { + arg.HTTPRoutes = k8sTable.HTTPRoutes.LoadAll() + } + translate(irInput) + } + } + ``` + +From the updates it gets from `.Subscribe`, it can get a full view of the map being subscribed to via `snapshot.State`; +but it must read the other maps explicitly. Like `sync.Map`, `watchable.Map`s are thread-safe; while `.Subscribe` is a +handy way to know when to run, `.Load` and friends can be used without subscribing. + +There can be any number of subscribers. For that matter, there can be any number of publishers `.Store`ing things, but +it's probably wise to just have one publisher for each map. + +The channel returned from `.Subscribe` **is immediately readable** with a snapshot of the map as it existed when +`.Subscribe` was called; and becomes readable again whenever `.Store` or `.Delete` mutates the map. If multiple +mutations happen between reads (or if mutations happen between `.Subscribe` and the first read), they are coalesced in +to one snapshot to be read; the `snapshot.State` is the most-recent full state, and `snapshot.Updates` is a listing of +each of the mutations that cause this snapshot to be different than the last-read one. This way subscribers don't need +to worry about a backlog accumulating if they can't keep up with the rate of changes from the publisher. + +If the map contains anything before `.Subscribe` is called, that very first read won't include `snapshot.Updates` +entries for those pre-existing items; if you are working with `snapshot.Update` instead of `snapshot.State`, then you +must add special handling for your first read. We have a utility function `./internal/message.HandleSubscription` to +help with this. + +## Other Notes + +The common pattern will likely be that the entrypoint that launches the goroutines for each component instantiates the +map, and passes them to the appropriate publishers and subscribers; same as if they were communicating via a dumb +`chan`. + +A limitation of `watchable.Map` is that in order to ensure safety between goroutines, it does require that value types +be deep-copiable; either by having a `DeepCopy` method, being a `proto.Message`, or by containing no reference types and +so can be deep-copied by naive assignment. Fortunately, we're using `controller-gen` anyway, and `controller-gen` can +generate `DeepCopy` methods for us: just stick a `// +k8s:deepcopy-gen=true` on the types that you want it to generate +methods for. + +[watchable]: https://pkg.go.dev/github.com/telepresenceio/watchable diff --git a/docs/v0.2.0/design_docs.rst b/docs/v0.2.0/design_docs.rst new file mode 100644 index 00000000000..4e95a518d1e --- /dev/null +++ b/docs/v0.2.0/design_docs.rst @@ -0,0 +1,12 @@ +Design Docs +=========== + +Learn about the internal details of Envoy Gateway. + +.. toctree:: + :maxdepth: 2 + + design/system-design + design/gatewayapi-translator + design/watching + design/config-api diff --git a/docs/v0.2.0/dev/CODE_OF_CONDUCT.md b/docs/v0.2.0/dev/CODE_OF_CONDUCT.md new file mode 100644 index 00000000000..5071043e94a --- /dev/null +++ b/docs/v0.2.0/dev/CODE_OF_CONDUCT.md @@ -0,0 +1,3 @@ +## Community Code of Conduct + +Gateway follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md). diff --git a/docs/v0.2.0/dev/CONTRIBUTING.md b/docs/v0.2.0/dev/CONTRIBUTING.md new file mode 100644 index 00000000000..eb4ad7e5a67 --- /dev/null +++ b/docs/v0.2.0/dev/CONTRIBUTING.md @@ -0,0 +1,183 @@ +# Contributing + +We welcome contributions from the community. Please carefully review the [project goals](GOALS.md) +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.md) 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 [docs](docs) as + well as the [changelog](./changelogs). +* 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 a subsystem name followed by a + colon. Examples: + * "docs: fix grammar error" + * "translator: add new feature" +* 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.md) 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/docs/v0.2.0/dev/DOCS.md b/docs/v0.2.0/dev/DOCS.md new file mode 100644 index 00000000000..fb49b9d55dd --- /dev/null +++ b/docs/v0.2.0/dev/DOCS.md @@ -0,0 +1,63 @@ +# Working on the Envoy Gateway Docs + +The documentation for the Envoy Gateway lives in the `docs/` directory. Any +individual document can be written using either [reStructuredText] or [Markdown], +you can choose the format that you're most comfortable with when working on the +documentation. + +## Documentation Structure + +We supported the versioned Docs now, the directory name under docs represents +the version of docs. The root of the latest site is in `docs/latest/index.rst`. +This is probably where to start if you're trying to understand how things fit together. + +Note that the new contents should be added to `docs/latest` and will be cut off at +the next release. The contents under `docs/v0.2.0` are auto-generated, +and usually do not need to make changes to them, unless if you find the current release pages have +some incorrect contents. If so, you should send a PR to update contents both of `docs/latest` +and `docs/v0.2.0`. + +It's important to note that a given document _must_ have a reference in some +`.. toctree::` section for the document to be reachable. Not everything needs +to be in `docs/index.rst`'s `toctree` though. + +You can access the website which represents the current release in default, +and you can access the website which contains the latest version changes in +[Here][latest-website] or at the footer of the pages. + +## Documentation Workflow + +To work with the docs, just edit reStructuredText or Markdown files in `docs`, +then run + +```bash +make docs +``` + +This will create `docs/html` with the built HTML pages. You can view the docs +either simply by pointing a web browser at the `file://` path to your +`docs/html`, or by firing up a static webserver from that directory, e.g. + +``` shell +make docs-serve +``` + +If you want to generate a new release version of the docs, like `v0.3.0`, then run + +```bash +make docs-release TAG=v0.3.0 +``` + +This will update the VERSION file at the project root, which records current release version, +and it will be used in the pages version context and binary version output. Also, this will generate +new dir `docs/v0.3.0`, which contains docs at v0.3.0 and updates artifact links to `v0.3.0` +in all files under `docs/v0.3.0/user`, like `quickstart.md`, `http-routing.md` and etc. + +## Publishing Docs + +Whenever docs are pushed to `main`, CI will publish the built docs to GitHub +Pages. For more details, see `.github/workflows/docs.yaml`. + +[reStructuredText]: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html +[Markdown]: https://daringfireball.net/projects/markdown/syntax +[latest-website]: https://gateway.envoyproxy.io/latest diff --git a/docs/v0.2.0/dev/README.md b/docs/v0.2.0/dev/README.md new file mode 100644 index 00000000000..f66661406d3 --- /dev/null +++ b/docs/v0.2.0/dev/README.md @@ -0,0 +1,139 @@ +# Developer Guide + +Envoy Gateway is built using a [make][]-based build system. Our CI is based on [Github Actions][] using [workflows][]. + +## Prerequisites + +### go + +* Version: 1.18.2 +* 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 the Envoy Gateway binary. __Note:__ The binary gets generated in the `bin/` directory + +### Testing + +* Run `make test` to run the golang tests. + +### Running Linters + +* Run `make lint` to make sure your code passes all the linter checks. + +### 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 `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. + +[Quickstart]: https://github.com/envoyproxy/gateway/blob/main/docs/user/quickstart.md +[make]: https://www.gnu.org/software/make/ +[Github Actions]: https://docs.github.com/en/actions +[workflows]: .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 diff --git a/docs/v0.2.0/dev/releasing.md b/docs/v0.2.0/dev/releasing.md new file mode 100644 index 00000000000..a6d965be92f --- /dev/null +++ b/docs/v0.2.0/dev/releasing.md @@ -0,0 +1,144 @@ +# Release Process + +This document guides maintainers through the process of creating an Envoy Gateway release. + +## Creating a Minor Release + +### Prerequisites + +- Permissions to push to the Envoy Gateway repository. + +### Set Environment Variables + +Set environment variables for use in subsequent steps: + +```shell +export MAJOR_VERSION=0 +export MINOR_VERSION=3 +export GITHUB_REMOTE=origin +``` + +1. Clone the repo, checkout the `main` branch, ensure it’s up-to-date, and your local branch is clean. +2. Create a topic branch to create the release notes and release docs. Reference previous [release notes][] for additional details. +3. Sign, commit, and push your changes to your fork and submit a [Pull Request][] to merge the changes listed below + into the `main` branch. Do not proceed until all your PRs have merged and the [Build and Test][build-and-test GitHub action] has completed for your final PR: + + 1. Add Release Announcement. + 2. Add Release Versioned Documents. + + ``` shell + make docs-release TAG=v${MAJOR_VERSION}.${MINOR_VERSION}.0 + ``` + +4. Create a new release branch from `main`. The release branch should be named + `release/v${MAJOR_VERSION}.${MINOR_VERSION}`, e.g. `release/v0.3`. + + ```shell + git checkout -b release/v${MAJOR_VERSION}.${MINOR_VERSION} + ``` + +5. Push the branch to the Envoy Gateway repo. + + ```shell + git push ${GITHUB_REMOTE} release/v${MAJOR_VERSION}.${MINOR_VERSION} + ``` + +6. Tag the head of your release branch with the release tag. For example: + + ```shell + git tag -a v${MAJOR_VERSION}.${MINOR_VERSION}.0 -m 'Envoy Gateway v${MAJOR_VERSION}.${MINOR_VERSION}.0 Release' + ``` + + __Note:__ The tag version differs from the release branch by including the `.0` patch version. + +7. Push the tag to the Envoy Gateway repository. + + ```shell + git push origin v${MAJOR_VERSION}.${MINOR_VERSION}.0 + ``` + +8. This will trigger the [release GitHub action][] that generates the release, release artifacts, etc. +9. Confirm that the [release workflow][] completed successfully. +10. Confirm that the Envoy Gateway [image][] with the correct release tag was published to Docker Hub. +11. Confirm that the [release][] was created. +12. Confirm that the steps in the [Quickstart Guide][] work as expected. +13. [Generate][] the GitHub changelog and include the following text at the beginning of the release page: + + ```console + # Release Announcement + + Check out the [v${MAJOR_VERSION}.${MINOR_VERSION} release announcement] + (https://gateway.envoyproxy.io/releases/v${MAJOR_VERSION}.${MINOR_VERSION}.html) to learn more about the release. + ``` + +If you find any bugs in this process, please create an issue. + +## Creating a Release Candidate + +### Prerequisites + +- Permissions to push to the Envoy Gateway repository. + +### Set Environment Variables + +```shell +export MAJOR_VERSION=0 +export MINOR_VERSION=3 +export RELEASE_CANDIDATE_NUMBER=1 +export GITHUB_REMOTE=origin +``` + +1. Clone the repo, checkout the `main` branch, ensure it’s up-to-date, and your local branch is clean. +2. Tag the head of the main branch with the release candidate number. + + ```shell + git tag -a v${MAJOR_VERSION}.${MINOR_VERSION}.0-rc.${RELEASE_CANDIDATE_NUMBER} -m 'Envoy Gateway v${MAJOR_VERSION}.${MINOR_VERSION}.0-rc.${RELEASE_CANDIDATE_NUMBER} Release Candidate' + ``` + +3. Push the tag to the Envoy Gateway repository. + + ```shell + git push v${MAJOR_VERSION}.${MINOR_VERSION}.0-rc.${RELEASE_CANDIDATE_NUMBER} + ``` + +4. This will trigger the [release GitHub action][] that generates the release, release artifacts, etc. +5. Confirm that the [release workflow][] completed successfully. +6. Confirm that the Envoy Gateway [image][] with the correct release tag was published to Docker Hub. +7. Confirm that the [release][] was created. +8. Note that the [Quickstart Guide][] references are __not__ updated for release candidates. However, test + the quickstart steps using the release candidate by manually updating the links. +9. [Generate][] the GitHub changelog. +10. Ensure you check the "This is a pre-release" checkbox when editing the GitHub release. +11. If you find any bugs in this process, please create an issue. + +## Announcing the Release + +It's important that the world knows about the release. Use the following steps to announce the release. + +1. Set the release information in the Envoy Gateway Slack channel. For example: + + ```shell + Envoy Gateway v${MAJOR_VERSION}.${MINOR_VERSION} has been released: https://github.com/envoyproxy/gateway/releases/tag/v${MAJOR_VERSION}.${MINOR_VERSION}.0 + ``` + +2. Send a message to the Envoy Gateway Slack channel. For example: + + ```shell + On behalf of the entire Envoy Gateway community, I am pleased to announce the release of Envoy Gateway + v${MAJOR_VERSION}.${MINOR_VERSION}. A big thank you to all the contributors that made this release possible. + Refer to the official v${MAJOR_VERSION}.${MINOR_VERSION} announcement for release details and the project docs + to start using Envoy Gateway. + ... + ``` + + Link to the GitHub release and release announcement page that highlights the release. + +[release notes]: https://github.com/envoyproxy/gateway/tree/main/release-notes +[Pull Request]: https://github.com/envoyproxy/gateway/pulls +[Quickstart Guide]: https://github.com/envoyproxy/gateway/blob/main/docs/user/quickstart.md +[build-and-test GitHub action]: https://github.com/envoyproxy/gateway/blob/main/.github/workflows/build_and_test.yaml +[release GitHub action]: https://github.com/envoyproxy/gateway/blob/main/.github/workflows/release.yaml +[release workflow]: https://github.com/envoyproxy/gateway/actions/workflows/release.yaml +[image]: https://hub.docker.com/r/envoyproxy/gateway/tags +[release]: https://github.com/envoyproxy/gateway/releases +[Generate]: https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes diff --git a/docs/v0.2.0/dev_docs.rst b/docs/v0.2.0/dev_docs.rst new file mode 100644 index 00000000000..88a0b367f01 --- /dev/null +++ b/docs/v0.2.0/dev_docs.rst @@ -0,0 +1,13 @@ +Developer Docs +============== + +Learn how to contribute to Envoy Gateway. + +.. toctree:: + :maxdepth: 2 + + dev/CODE_OF_CONDUCT + dev/CONTRIBUTING + dev/README + dev/DOCS + dev/releasing diff --git a/docs/v0.2.0/get_involved.rst b/docs/v0.2.0/get_involved.rst new file mode 100644 index 00000000000..f17febd5651 --- /dev/null +++ b/docs/v0.2.0/get_involved.rst @@ -0,0 +1,9 @@ +Getting Involved +================ + +We welcome contributions from the community. Please carefully review the +`project goals `_ +and the +`code of conduct `_ +before diving in. + diff --git a/docs/v0.2.0/images/architecture.png b/docs/v0.2.0/images/architecture.png new file mode 100644 index 00000000000..1d4131fbea7 Binary files /dev/null and b/docs/v0.2.0/images/architecture.png differ diff --git a/docs/v0.2.0/index.rst b/docs/v0.2.0/index.rst new file mode 100644 index 00000000000..39f345fa125 --- /dev/null +++ b/docs/v0.2.0/index.rst @@ -0,0 +1,32 @@ +`Envoy Gateway `_ +============= + +Release: |version| + +.. image:: https://img.shields.io/badge/slack-join-orange.svg + :target: https://envoyproxy.slack.com/archives/C03E6NHLESV + :alt: Join the Envoy Slack + +Envoy Gateway is an open source project for managing `Envoy Proxy`_ as a standalone or Kubernetes-based application +gateway. `Gateway API`_ resources are used to dynamically provision and configure the managed Envoy Proxies. Whether +you are interested in using or contributing to Envoy Gateway, the following resources will help you get started: + +.. toctree:: + :maxdepth: 1 + + intro/compatibility + user_docs + design_docs + dev_docs + releases + roadmap + about_docs + get_involved + +.. note:: + + This project is under active development. Many, many features are not + complete. We would love for you to :doc:`get involved`. + +.. _Envoy Proxy: https://www.envoyproxy.io/ +.. _Gateway API: https://gateway-api.sigs.k8s.io/ diff --git a/docs/v0.2.0/intro/compatibility.rst b/docs/v0.2.0/intro/compatibility.rst new file mode 100644 index 00000000000..4dc06f769e5 --- /dev/null +++ b/docs/v0.2.0/intro/compatibility.rst @@ -0,0 +1,19 @@ +Compatibility Matrix +==================== + +Envoy Gateway relies on the Envoy Proxy and the Gateway API, and runs +within a Kubernetes cluster. Not all versions of each of these products +can function together for Envoy Gateway. Supported version combinations +are listed below; **bold** type indicates the versions of the Envoy Proxy +and the Gateway API actually compiled into each Envoy Gateway release. + ++--------------------------+---------------------+---------------------+----------------------------+ +| Envoy Gateway version | Envoy Proxy version | Gateway API version | Kubernetes minimum version | ++--------------------------+---------------------+---------------------+----------------------------+ +| v0.2.0 | **v1.23-latest** | **v0.5.1** | v1.24 | ++--------------------------+---------------------+---------------------+----------------------------+ + +.. note:: + + This project is under active development. Many, many features are not + complete. We would love for you to :doc:`get involved<../get_involved>`. diff --git a/docs/v0.2.0/releases.rst b/docs/v0.2.0/releases.rst new file mode 100644 index 00000000000..090c6707fd2 --- /dev/null +++ b/docs/v0.2.0/releases.rst @@ -0,0 +1,10 @@ +Releases +======== + +Learn more about Envoy Gateway releases. + +.. toctree:: + :maxdepth: 1 + + releases/README + releases/v0.2 diff --git a/docs/v0.2.0/releases/README.md b/docs/v0.2.0/releases/README.md new file mode 100644 index 00000000000..93d3366efb0 --- /dev/null +++ b/docs/v0.2.0/releases/README.md @@ -0,0 +1,41 @@ +# Release Details + +This document provides details for Envoy Gateway releases. Envoy Gateway follows the Semantic Versioning [v2.0.0 spec][] +for release versioning. Since Envoy Gateway is a new project, minor releases are the only defined releases. Envoy +Gateway maintainers will establish additional release details, e.g. patch releases, at a future date. + +## Stable Releases + +Stable releases of Envoy Gateway include: + +* Minor Releases- A new release branch and corresponding tag are created from the `main` branch. A minor release + is supported for 6 months following the release date. As the project matures, Envoy Gateway maintainers will reassess + the support timeframe. + +Minor releases happen quarterly and follow the schedule below. + +## Release Management + +Minor releases are handled by a designated Envoy Gateway maintainer. This maintainer is considered the Release Manager +for the release. The details for creating a release are outlined in the [release guide][]. The Release Manager is +responsible for coordinating the overall release. This includes identifying issues to be fixed in the release, +communications with the Envoy Gateway community, and the mechanics of the release. + +| Quarter | Release Manager | +|:-------:|:--------------------------------------------------------------:| +| 2022 Q4 | Daneyon Hansen ([danehans](https://github.com/danehans)) | +| 2023 Q1 | TBD | + +## Release Schedule + +In order to align with the Envoy Proxy [release schedule][], Envoy Gateway releases are produced on a fixed schedule +(the 22nd day of each quarter), with an acceptable delay of up to 2 weeks, and a hard deadline of 3 weeks. + +| Version | Expected | Actual | Difference | End of Life | +|:-------:|:-----------:|:-----------:|:----------:|:-----------:| +| 0.2.0 | 2022/10/22 | 2022/10/20 | -2 day | 2023/4/20 | +| 0.3.0 | 2023/01/22 | | | | + +[v2.0.0 spec]: https://semver.org/spec/v2.0.0.html +[release guide]: ../dev/releasing.md +[release schedule]: https://github.com/envoyproxy/envoy/blob/main/RELEASES.md#major-release-schedule diff --git a/docs/v0.2.0/releases/v0.2.md b/docs/v0.2.0/releases/v0.2.md new file mode 100644 index 00000000000..a0dc0e885de --- /dev/null +++ b/docs/v0.2.0/releases/v0.2.md @@ -0,0 +1,50 @@ +--- +title: Announcing Envoy Gateway v0.2 +linktitle: v0.2 +subtitle: Major Update +description: Envoy Gateway v0.2 release announcement. +publishdate: 2022-10-20 +release: v0.2.0 +skip_list: true +aliases: +- /releases/v0.2 +- /releases/v0.2.0 +--- +# Envoy Gateway Release v0.2 + +We are pleased to announce the release of Envoy Gateway v0.2! + +This is the first functional release of Envoy Gateway. We would like to thank the entire Envoy Gateway community for +helping publish the release. + +| [Release Notes][] | [Docs][docs] | [Compatibility Matrix][matrix] | [Download][] | +|-------------------|--------------|--------------------------------|--------------| + +## What's New + +The release adds a ton of features and functionality. Here are some highlights: + +### Kubernetes Support + +Run Envoy Gateway in a Kubernetes cluster. Checkout the [quickstart guide][] to get started with Envoy Gateway in a few +simple steps. + +### Gateway API Support + +Envoy Gateway supports Gateway API resources for running and configuring a managed fleet of Envoy proxies. Envoy Gateway +passes Gateway API core [conformance tests][] and supports GatewayClass, Gateway, HTTPRoute, and TLSRoute resources. See +the [documentation][docs] for additional details on how to use Envoy Gateway for your edge proxy and API gateway needs. + +## Envoy Gateway at EnvoyCon NA + +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 +[matrix]: https://gateway.envoyproxy.io/intro/compatibility.html +[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]: https://gateway.envoyproxy.io/user/quickstart.html +[EnvoyCon NA]: https://events.linuxfoundation.org/envoycon-north-america/program/schedule/ +[our talk]: https://sched.co/1AO5S diff --git a/docs/v0.2.0/roadmap.rst b/docs/v0.2.0/roadmap.rst new file mode 100644 index 00000000000..711b6245503 --- /dev/null +++ b/docs/v0.2.0/roadmap.rst @@ -0,0 +1,9 @@ +Roadmap +======= + +Learn about the future direction of Envoy Gateway. + +.. toctree:: + :maxdepth: 2 + + design/roadmap diff --git a/docs/v0.2.0/user/http-redirect.md b/docs/v0.2.0/user/http-redirect.md new file mode 100644 index 00000000000..c11b346b592 --- /dev/null +++ b/docs/v0.2.0/user/http-redirect.md @@ -0,0 +1,123 @@ +# 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][]. + +Follow the steps from the [Secure Gateways](secure-gateways.md) to install Envoy Gateway and the example manifest. Do +not proceed until you can curl the example backend from the Quickstart guide 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"`: + +```shell +cat < 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. + +```shell +cat < 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. + +```shell +cat < 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 separate filters on the same HTTPRoute and they will all perform as expected + +```shell +cat < 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: + +```shell +cat < 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. + +```shell +cat < 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/references/spec/#gateway.networking.k8s.io/v1beta1.BackendRef diff --git a/docs/v0.2.0/user/quickstart.md b/docs/v0.2.0/user/quickstart.md new file mode 100644 index 00000000000..46da189d2ec --- /dev/null +++ b/docs/v0.2.0/user/quickstart.md @@ -0,0 +1,81 @@ +# Quickstart + +This guide will help you get started with Envoy Gateway in a few simple steps. + +## Prerequisites + +A Kubernetes cluster. + +__Note:__ Envoy Gateway is tested against Kubernetes v1.24.0. + +## Installation + +Install the Gateway API CRDs and Envoy Gateway: + +```shell +kubectl apply -f https://github.com/envoyproxy/gateway/releases/download/v0.2.0/install.yaml +``` + +Install the GatewayClass, Gateway, HTTPRoute and example app: + +```shell +kubectl apply -f https://github.com/envoyproxy/gateway/releases/download/v0.2.0/quickstart.yaml +``` + +## 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:8080 & +``` + +Curl the example app through Envoy proxy: + +```shell +curl --verbose --header "Host: www.example.com" http://localhost:8888/get +``` + +### 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 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 example app through Envoy proxy: + +```shell +curl --verbose --header "Host: www.example.com" http://$GATEWAY_HOST:8080/get +``` + +## Clean-Up + +Use the steps in this section to uninstall everything from the quickstart guide. + +Delete the GatewayClass, Gateway, HTTPRoute and Example App: + +```shell +kubectl delete -f https://github.com/envoyproxy/gateway/releases/download/v0.2.0/quickstart.yaml --ignore-not-found=true +``` + +Delete the Gateway API CRDs and Envoy Gateway: + +```shell +kubectl delete -f https://github.com/envoyproxy/gateway/releases/download/v0.2.0/install.yaml --ignore-not-found=true +``` + +## Next Steps + +Checkout the [Developer Guide](../dev/README.md) to get involved in the project. diff --git a/docs/v0.2.0/user/secure-gateways.md b/docs/v0.2.0/user/secure-gateways.md new file mode 100644 index 00000000000..9fec46c59bf --- /dev/null +++ b/docs/v0.2.0/user/secure-gateways.md @@ -0,0 +1,260 @@ +# Secure Gateways + +This guide will help you get started using secure Gateways. The guide uses a self-signed CA, so it should be used for +testing and demonstration purposes only. + +## Prerequisites + +- A Kubernetes cluster with `kubectl` context configured for the cluster. +- OpenSSL to generate TLS assets. + +__Note:__ Envoy Gateway is tested against Kubernetes v1.24. + +## Installation + +Follow the steps from the [Quickstart Guide](quickstart.md) 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 guide to include an HTTPS listener that listens on port `8443` 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": 8443, + "tls": { + "mode": "Terminate", + "certificateRefs": [{ + "kind": "Secret", + "group": "", + "name": "example-cert", + }], + }, + }, +}]' +``` + +Verify the Gateway status: + +```shell +kubectl get gateway/eg -o yaml +``` + +## Testing + +### Clusters without External 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} 8043:8443 & +``` + +Query the example app through Envoy proxy: + +```shell +curl -v -HHost:www.example.com --resolve "www.example.com:8043:127.0.0.1" \ +--cacert example.com.crt https://www.example.com:8043/get +``` + +### Clusters 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:8443:${GATEWAY_HOST}" \ +--cacert example.com.crt https://www.example.com:8443/get +``` + +## 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": 8443, + "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: + +```console +$ cat < -1 { - r.route.Status.Parents[r.routeParentStatusIdx].Conditions[idx] = cond - } else { - r.route.Status.Parents[r.routeParentStatusIdx].Conditions = append(r.route.Status.Parents[r.routeParentStatusIdx].Conditions, cond) + if idx > -1 { + r.httpRoute.Status.Parents[r.routeParentStatusIdx].Conditions[idx] = cond + } else { + r.httpRoute.Status.Parents[r.routeParentStatusIdx].Conditions = append(r.httpRoute.Status.Parents[r.routeParentStatusIdx].Conditions, cond) + } + case KindTLSRoute: + for i, existing := range r.tlsRoute.Status.Parents[r.routeParentStatusIdx].Conditions { + if existing.Type == cond.Type { + // return early if the condition is unchanged + if existing.Status == cond.Status && + existing.Reason == cond.Reason && + existing.Message == cond.Message { + return + } + idx = i + break + } + } + + if idx > -1 { + r.tlsRoute.Status.Parents[r.routeParentStatusIdx].Conditions[idx] = cond + } else { + r.tlsRoute.Status.Parents[r.routeParentStatusIdx].Conditions = append(r.tlsRoute.Status.Parents[r.routeParentStatusIdx].Conditions, cond) + } } } -func (r *RouteParentContext) ResetConditions() { - r.route.Status.Parents[r.routeParentStatusIdx].Conditions = make([]metav1.Condition, 0) +func (r *RouteParentContext) ResetConditions(route RouteContext) { + switch route.GetRouteType() { + case KindHTTPRoute: + r.httpRoute.Status.Parents[r.routeParentStatusIdx].Conditions = make([]metav1.Condition, 0) + case KindTLSRoute: + r.tlsRoute.Status.Parents[r.routeParentStatusIdx].Conditions = make([]metav1.Condition, 0) + } } -func (r *RouteParentContext) IsAccepted() bool { - for _, cond := range r.route.Status.Parents[r.routeParentStatusIdx].Conditions { +func (r *RouteParentContext) IsAccepted(route RouteContext) bool { + var conditions []metav1.Condition + switch route.GetRouteType() { + case KindHTTPRoute: + conditions = r.httpRoute.Status.Parents[r.routeParentStatusIdx].Conditions + case KindTLSRoute: + conditions = r.tlsRoute.Status.Parents[r.routeParentStatusIdx].Conditions + } + for _, cond := range conditions { if cond.Type == string(v1beta1.RouteConditionAccepted) && cond.Status == metav1.ConditionTrue { return true } diff --git a/internal/gatewayapi/contexts_test.go b/internal/gatewayapi/contexts_test.go index 5b84fdf379e..b584df2629d 100644 --- a/internal/gatewayapi/contexts_test.go +++ b/internal/gatewayapi/contexts_test.go @@ -1,3 +1,8 @@ +// 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 gatewayapi import ( diff --git a/internal/gatewayapi/helpers.go b/internal/gatewayapi/helpers.go index f4313c93b29..0cf58152e1e 100644 --- a/internal/gatewayapi/helpers.go +++ b/internal/gatewayapi/helpers.go @@ -1,3 +1,8 @@ +// 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 gatewayapi import ( @@ -27,6 +32,15 @@ func FromNamespacesPtr(fromNamespaces v1beta1.FromNamespaces) *v1beta1.FromNames return &fromNamespaces } +func SectionNamePtr(name string) *v1beta1.SectionName { + sectionName := v1beta1.SectionName(name) + return §ionName +} + +func TLSModeTypePtr(mode v1beta1.TLSModeType) *v1beta1.TLSModeType { + return &mode +} + func StringPtr(val string) *string { return &val } @@ -67,6 +81,14 @@ func HeaderMatchTypeDerefOr(matchType *v1beta1.HeaderMatchType, defaultType v1be return defaultType } +func QueryParamMatchTypeDerefOr(matchType *v1beta1.QueryParamMatchType, + defaultType v1beta1.QueryParamMatchType) v1beta1.QueryParamMatchType { + if matchType != nil { + return *matchType + } + return defaultType +} + func NamespaceDerefOr(namespace *v1beta1.Namespace, defaultNamespace string) string { if namespace != nil && *namespace != "" { return string(*namespace) @@ -117,8 +139,8 @@ func GetReferencedListeners(parentRef v1beta1.ParentReference, gateways []*Gatew selectsGateway = true // The parentRef may be to the entire Gateway, or to a specific listener. - for listenerName, listener := range gateway.listeners { - if parentRef.SectionName == nil || *parentRef.SectionName == listenerName { + for _, listener := range gateway.listeners { + if parentRef.SectionName == nil || *parentRef.SectionName == listener.Name { referencedListeners = append(referencedListeners, listener) } } @@ -138,9 +160,9 @@ func HasReadyListener(listeners []*ListenerContext) bool { return false } -// ComputeHosts returns a list of the intersecting hostnames between the route +// computeHosts returns a list of the intersecting hostnames between the route // and the listener. -func ComputeHosts(routeHostnames []v1beta1.Hostname, listenerHostname *v1beta1.Hostname) []string { +func computeHosts(routeHostnames []string, listenerHostname *v1beta1.Hostname) []string { var listenerHostnameVal string if listenerHostname != nil { listenerHostnameVal = string(*listenerHostname) @@ -159,7 +181,7 @@ func ComputeHosts(routeHostnames []v1beta1.Hostname, listenerHostname *v1beta1.H var hostnames []string for i := range routeHostnames { - routeHostname := string(routeHostnames[i]) + routeHostname := routeHostnames[i] // TODO ensure routeHostname is a valid hostname diff --git a/internal/gatewayapi/helpers_v1alpha2.go b/internal/gatewayapi/helpers_v1alpha2.go new file mode 100644 index 00000000000..1dc934a27f0 --- /dev/null +++ b/internal/gatewayapi/helpers_v1alpha2.go @@ -0,0 +1,145 @@ +// 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. + +// This file contains code derived from Contour, +// https://github.com/projectcontour/contour +// and is provided here subject to the following: +// Copyright Project Contour Authors +// SPDX-License-Identifier: Apache-2.0 + +package gatewayapi + +import ( + "sigs.k8s.io/gateway-api/apis/v1alpha2" + "sigs.k8s.io/gateway-api/apis/v1beta1" +) + +// TODO: [v1alpha2-v1beta1] +// This file can be removed once TLSRoute graduates to v1beta1. + +func GroupPtrV1Alpha2(group string) *v1alpha2.Group { + gwGroup := v1alpha2.Group(group) + return &gwGroup +} + +func KindPtrV1Alpha2(kind string) *v1alpha2.Kind { + gwKind := v1alpha2.Kind(kind) + return &gwKind +} + +func NamespacePtrV1Alpha2(namespace string) *v1alpha2.Namespace { + gwNamespace := v1alpha2.Namespace(namespace) + return &gwNamespace +} + +func SectionNamePtrV1Alpha2(sectionName string) *v1alpha2.SectionName { + gwSectionName := v1alpha2.SectionName(sectionName) + return &gwSectionName +} + +func PortNumPtrV1Alpha2(port int) *v1alpha2.PortNumber { + pn := v1alpha2.PortNumber(port) + return &pn +} + +func UpgradeParentReferences(old []v1alpha2.ParentReference) []v1beta1.ParentReference { + newParentReferences := make([]v1beta1.ParentReference, len(old)) + for i, o := range old { + newParentReferences[i] = UpgradeParentReference(o) + } + return newParentReferences +} + +// UpgradeParentReference converts v1alpha2.ParentReference to v1beta1.ParentReference +func UpgradeParentReference(old v1alpha2.ParentReference) v1beta1.ParentReference { + upgraded := v1beta1.ParentReference{} + + if old.Group != nil { + upgraded.Group = GroupPtr(string(*old.Group)) + } + + if old.Kind != nil { + upgraded.Kind = KindPtr(string(*old.Kind)) + } + + if old.Namespace != nil { + upgraded.Namespace = NamespacePtr(string(*old.Namespace)) + } + + upgraded.Name = v1beta1.ObjectName(old.Name) + + if old.SectionName != nil { + upgraded.SectionName = SectionNamePtr(string(*old.SectionName)) + } + + if old.Port != nil { + upgraded.Port = PortNumPtr(int32(*old.Port)) + } + + return upgraded +} + +func DowngradeParentReference(old v1beta1.ParentReference) v1alpha2.ParentReference { + downgraded := v1alpha2.ParentReference{} + + if old.Group != nil { + downgraded.Group = GroupPtrV1Alpha2(string(*old.Group)) + } + + if old.Kind != nil { + downgraded.Kind = KindPtrV1Alpha2(string(*old.Kind)) + } + + if old.Namespace != nil { + downgraded.Namespace = NamespacePtrV1Alpha2(string(*old.Namespace)) + } + + downgraded.Name = v1alpha2.ObjectName(old.Name) + + if old.SectionName != nil { + downgraded.SectionName = SectionNamePtrV1Alpha2(string(*old.SectionName)) + } + + if old.Port != nil { + downgraded.Port = PortNumPtrV1Alpha2(int(*old.Port)) + } + + return downgraded +} + +func UpgradeRouteParentStatuses(routeParentStatuses []v1alpha2.RouteParentStatus) []v1beta1.RouteParentStatus { + var res []v1beta1.RouteParentStatus + + for _, rps := range routeParentStatuses { + res = append(res, v1beta1.RouteParentStatus{ + ParentRef: UpgradeParentReference(rps.ParentRef), + ControllerName: v1beta1.GatewayController(rps.ControllerName), + Conditions: rps.Conditions, + }) + } + + return res +} + +func DowngradeRouteParentStatuses(routeParentStatuses []v1beta1.RouteParentStatus) []v1alpha2.RouteParentStatus { + var res []v1alpha2.RouteParentStatus + + for _, rps := range routeParentStatuses { + res = append(res, v1alpha2.RouteParentStatus{ + ParentRef: DowngradeParentReference(rps.ParentRef), + ControllerName: v1alpha2.GatewayController(rps.ControllerName), + Conditions: rps.Conditions, + }) + } + + return res +} + +func NamespaceDerefOrAlpha(namespace *v1alpha2.Namespace, defaultNamespace string) string { + if namespace != nil && *namespace != "" { + return string(*namespace) + } + return defaultNamespace +} diff --git a/internal/gatewayapi/runner/runner.go b/internal/gatewayapi/runner/runner.go index 05c5929a7aa..091b92bf6b5 100644 --- a/internal/gatewayapi/runner/runner.go +++ b/internal/gatewayapi/runner/runner.go @@ -1,3 +1,8 @@ +// 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 ( @@ -43,7 +48,10 @@ func (r *Runner) subscribeAndTranslate(ctx context.Context) { // Subscribe to resources gatewayClassesCh := r.ProviderResources.GatewayClasses.Subscribe(ctx) gatewaysCh := r.ProviderResources.Gateways.Subscribe(ctx) + secretsCh := r.ProviderResources.Secrets.Subscribe(ctx) + refGrantsCh := r.ProviderResources.ReferenceGrants.Subscribe(ctx) httpRoutesCh := r.ProviderResources.HTTPRoutes.Subscribe(ctx) + tlsRoutesCh := r.ProviderResources.TLSRoutes.Subscribe(ctx) servicesCh := r.ProviderResources.Services.Subscribe(ctx) namespacesCh := r.ProviderResources.Namespaces.Subscribe(ctx) @@ -53,14 +61,20 @@ func (r *Runner) subscribeAndTranslate(ctx context.Context) { select { case <-gatewayClassesCh: case <-gatewaysCh: + case <-secretsCh: + case <-refGrantsCh: case <-httpRoutesCh: + case <-tlsRoutesCh: case <-servicesCh: case <-namespacesCh: } r.Logger.Info("received a notification") // Load all resources required for translation in.Gateways = r.ProviderResources.GetGateways() + in.Secrets = r.ProviderResources.GetSecrets() + in.ReferenceGrants = r.ProviderResources.GetReferenceGrants() in.HTTPRoutes = r.ProviderResources.GetHTTPRoutes() + in.TLSRoutes = r.ProviderResources.GetTLSRoutes() in.Services = r.ProviderResources.GetServices() in.Namespaces = r.ProviderResources.GetNamespaces() gatewayClasses := r.ProviderResources.GetGatewayClasses() @@ -99,6 +113,7 @@ func (r *Runner) subscribeAndTranslate(ctx context.Context) { newKeys = append(newKeys, key) } } + for key, val := range result.XdsIR { if err := val.Validate(); err != nil { r.Logger.Error(err, "unable to validate xds ir, skipped sending it") @@ -124,6 +139,10 @@ func (r *Runner) subscribeAndTranslate(ctx context.Context) { key := utils.NamespacedName(httpRoute) r.ProviderResources.HTTPRouteStatuses.Store(key, httpRoute) } + for _, tlsRoute := range result.TLSRoutes { + key := utils.NamespacedName(tlsRoute) + r.ProviderResources.TLSRouteStatuses.Store(key, tlsRoute) + } } } r.Logger.Info("shutting down") diff --git a/internal/gatewayapi/runner/runner_test.go b/internal/gatewayapi/runner/runner_test.go index 77cdfffa61e..c6a84eb5fa6 100644 --- a/internal/gatewayapi/runner/runner_test.go +++ b/internal/gatewayapi/runner/runner_test.go @@ -1,3 +1,8 @@ +// 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 ( diff --git a/internal/gatewayapi/sort.go b/internal/gatewayapi/sort.go index 035d2f6093b..6a7d1cc2a24 100644 --- a/internal/gatewayapi/sort.go +++ b/internal/gatewayapi/sort.go @@ -1,3 +1,8 @@ +// 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 gatewayapi import ( @@ -44,8 +49,6 @@ func (x XdsIRRoutes) Less(i, j int) bool { // https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteRule func sortXdsIRMap(xdsIR XdsIRMap) { for _, ir := range xdsIR { - ir := ir - sort.SliceStable(ir.HTTP, func(i, j int) bool { return ir.HTTP[i].Name < ir.HTTP[j].Name }) for _, http := range ir.HTTP { // descending order sort.Sort(sort.Reverse(XdsIRRoutes(http.Routes))) diff --git a/internal/gatewayapi/testdata/gateway-allows-same-namespace-with-allowed-httproute.out.yaml b/internal/gatewayapi/testdata/gateway-allows-same-namespace-with-allowed-httproute.out.yaml index 4ec6b5e9f85..7dd8df241ee 100644 --- a/internal/gatewayapi/testdata/gateway-allows-same-namespace-with-allowed-httproute.out.yaml +++ b/internal/gatewayapi/testdata/gateway-allows-same-namespace-with-allowed-httproute.out.yaml @@ -77,7 +77,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/gateway-allows-same-namespace-with-disallowed-httproute.out.yaml b/internal/gatewayapi/testdata/gateway-allows-same-namespace-with-disallowed-httproute.out.yaml index ef0063b5b3a..564ce5e3385 100644 --- a/internal/gatewayapi/testdata/gateway-allows-same-namespace-with-disallowed-httproute.out.yaml +++ b/internal/gatewayapi/testdata/gateway-allows-same-namespace-with-disallowed-httproute.out.yaml @@ -69,7 +69,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-namespaces-selector.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-namespaces-selector.out.yaml index c9df1469eac..786aa790a33 100644 --- a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-namespaces-selector.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-namespaces-selector.out.yaml @@ -69,6 +69,6 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-routes-group.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-routes-group.out.yaml index 520a94d69cb..b7dbf3cca34 100644 --- a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-routes-group.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-routes-group.out.yaml @@ -67,6 +67,6 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-routes-kind.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-routes-kind.out.yaml index 140cb3dc8b9..24da45d907c 100644 --- a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-routes-kind.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-routes-kind.out.yaml @@ -67,6 +67,6 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-tls-route-kind.in.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-tls-route-kind.in.yaml new file mode 100644 index 00000000000..c5762afbaff --- /dev/null +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-tls-route-kind.in.yaml @@ -0,0 +1,35 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls + hostname: foo.com + protocol: TLS + port: 80 + tls: + mode: Passthrough + allowedRoutes: + namespaces: + from: All + kinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute +tlsRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-1 + port: 8080 diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-tls-route-kind.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-tls-route-kind.out.yaml new file mode 100644 index 00000000000..e4c95db8bb5 --- /dev/null +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-tls-route-kind.out.yaml @@ -0,0 +1,72 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls + protocol: TLS + hostname: foo.com + port: 80 + tls: + mode: Passthrough + allowedRoutes: + namespaces: + from: All + kinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + status: + listeners: + - name: tls + attachedRoutes: 0 + conditions: + - type: ResolvedRefs + status: "False" + reason: InvalidRouteKinds + message: "Kind is not supported, kind must be TLSRoute" + - type: Ready + status: "False" + reason: Invalid + message: Listener is invalid, see other Conditions for details. +tlsRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-1 + port: 8080 + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "False" + reason: NoReadyListeners + message: There are no ready listeners for this parent ref +xdsIR: + envoy-gateway-gateway-1: {} +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:translator-tests + listeners: + - address: "" diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-invalid-mode.in.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-invalid-mode.in.yaml index c17a2ef492e..a4793ddeff2 100644 --- a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-invalid-mode.in.yaml +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-invalid-mode.in.yaml @@ -9,6 +9,7 @@ gateways: listeners: - name: tls protocol: HTTPS + hostname: foo.com port: 443 allowedRoutes: namespaces: diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-invalid-mode.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-invalid-mode.out.yaml index 40a696060d1..02d5aead369 100644 --- a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-invalid-mode.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-invalid-mode.out.yaml @@ -9,6 +9,7 @@ gateways: listeners: - name: tls protocol: HTTPS + hostname: foo.com port: 443 allowedRoutes: namespaces: @@ -67,6 +68,6 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-no-certificate-refs.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-no-certificate-refs.out.yaml index 7f9685d1c08..29923e8a403 100644 --- a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-no-certificate-refs.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-no-certificate-refs.out.yaml @@ -65,6 +65,6 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-does-not-exist.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-does-not-exist.out.yaml index 3219789b684..014573079c1 100644 --- a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-does-not-exist.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-does-not-exist.out.yaml @@ -71,6 +71,6 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-in-other-namespace.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-in-other-namespace.out.yaml index 4dd99085ff9..af0ec8df8d2 100644 --- a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-in-other-namespace.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-in-other-namespace.out.yaml @@ -28,7 +28,7 @@ gateways: conditions: - type: ResolvedRefs status: "False" - reason: InvalidCertificateRef + reason: RefNotPermitted message: Certificate ref to secret default/tls-secret-1 not permitted by any ReferenceGrant - type: Ready status: "False" @@ -72,6 +72,6 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-is-not-valid.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-is-not-valid.out.yaml index 6c1dc8b1550..23868e2b77d 100644 --- a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-is-not-valid.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-is-not-valid.out.yaml @@ -71,6 +71,6 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-missing-allowed-namespaces-selector.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-missing-allowed-namespaces-selector.out.yaml index 54e12a52725..edbca3ecb79 100644 --- a/internal/gatewayapi/testdata/gateway-with-listener-with-missing-allowed-namespaces-selector.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-missing-allowed-namespaces-selector.out.yaml @@ -63,6 +63,6 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-tls-secret-in-other-namespace-allowed-by-refgrant.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-tls-secret-in-other-namespace-allowed-by-refgrant.out.yaml index 910d29210c6..90b8679b1c9 100644 --- a/internal/gatewayapi/testdata/gateway-with-listener-with-tls-secret-in-other-namespace-allowed-by-refgrant.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-tls-secret-in-other-namespace-allowed-by-refgrant.out.yaml @@ -85,7 +85,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-tls-terminate-and-passthrough.in.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-tls-terminate-and-passthrough.in.yaml new file mode 100644 index 00000000000..bf254c658b1 --- /dev/null +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-tls-terminate-and-passthrough.in.yaml @@ -0,0 +1,70 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls-passthrough + protocol: TLS + port: 90 + hostname: foo.com + tls: + mode: Passthrough + allowedRoutes: + namespaces: + from: All + - name: tls-terminate + protocol: HTTPS + port: 443 + hostname: foo.com + tls: + mode: Terminate + certificateRefs: + - name: tls-secret-1 + allowedRoutes: + namespaces: + from: All +tlsRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-2 + port: 8080 +httpRoutes: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 +secrets: + - apiVersion: v1 + kind: Secret + metadata: + namespace: envoy-gateway + name: tls-secret-1 + type: kubernetes.io/tls + data: + tls.crt: Zm9vCg== + tls.key: YmFyCg== diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-tls-terminate-and-passthrough.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-tls-terminate-and-passthrough.out.yaml new file mode 100644 index 00000000000..53a3a73bd94 --- /dev/null +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-tls-terminate-and-passthrough.out.yaml @@ -0,0 +1,154 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls-passthrough + protocol: TLS + port: 90 + hostname: foo.com + tls: + mode: Passthrough + allowedRoutes: + namespaces: + from: All + - name: tls-terminate + protocol: HTTPS + port: 443 + hostname: foo.com + tls: + mode: Terminate + certificateRefs: + - name: tls-secret-1 + allowedRoutes: + namespaces: + from: All + status: + listeners: + - name: tls-passthrough + supportedKinds: + - group: gateway.networking.k8s.io + kind: TLSRoute + attachedRoutes: 1 + conditions: + - type: Ready + status: "True" + reason: Ready + message: Listener is ready + - name: tls-terminate + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + attachedRoutes: 1 + conditions: + - type: Ready + status: "True" + reason: Ready + message: Listener is ready +tlsRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-2 + port: 8080 + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "True" + reason: Accepted + message: Route is accepted +httpRoutes: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "True" + reason: Accepted + message: Route is accepted +xdsIR: + envoy-gateway-gateway-1: + http: + - name: envoy-gateway-gateway-1-tls-terminate + address: 0.0.0.0 + port: 10443 + hostnames: + - "foo.com" + tls: + serverCertificate: Zm9vCg== + privateKey: YmFyCg== + routes: + - name: default-httproute-1-rule-0-match-0-foo.com + pathMatch: + prefix: "/" + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 + tcp: + - name: envoy-gateway-gateway-1-tls-passthrough-tlsroute-1 + address: 0.0.0.0 + port: 10090 + tls: + snis: + - "foo.com" + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:translator-tests + listeners: + - address: "" + ports: + - name: tls-passthrough + protocol: "TLS" + servicePort: 90 + containerPort: 10090 + - name: tls-terminate + protocol: "HTTPS" + servicePort: 443 + containerPort: 10443 diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-unsupported-protocol.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-unsupported-protocol.out.yaml index a5a2dff4287..2ffb00f9481 100644 --- a/internal/gatewayapi/testdata/gateway-with-listener-with-unsupported-protocol.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-unsupported-protocol.out.yaml @@ -64,6 +64,6 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-valid-tls-configuration.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-valid-tls-configuration.out.yaml index c13cd35b074..f51684e2a0e 100644 --- a/internal/gatewayapi/testdata/gateway-with-listener-with-valid-tls-configuration.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-valid-tls-configuration.out.yaml @@ -81,10 +81,10 @@ infraIR: proxy: metadata: labels: - gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-http-and-tlsroute-same-hostname-and-port.in.yaml b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-http-and-tlsroute-same-hostname-and-port.in.yaml new file mode 100644 index 00000000000..e3bfa502356 --- /dev/null +++ b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-http-and-tlsroute-same-hostname-and-port.in.yaml @@ -0,0 +1,56 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http-1 + protocol: HTTP + port: 80 + hostname: foo.com + allowedRoutes: + namespaces: + from: All + - name: tls-1 + protocol: TLS + tls: + mode: Passthrough + port: 80 + hostname: foo.com + allowedRoutes: + namespaces: + from: All +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 +tlsRoutes: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-1 + port: 8080 diff --git a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-http-and-tlsroute-same-hostname-and-port.out.yaml b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-http-and-tlsroute-same-hostname-and-port.out.yaml new file mode 100644 index 00000000000..975d8f585bc --- /dev/null +++ b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-http-and-tlsroute-same-hostname-and-port.out.yaml @@ -0,0 +1,120 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http-1 + protocol: HTTP + port: 80 + hostname: foo.com + allowedRoutes: + namespaces: + from: All + - name: tls-1 + protocol: TLS + tls: + mode: Passthrough + port: 80 + hostname: foo.com + allowedRoutes: + namespaces: + from: All + status: + listeners: + - name: http-1 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + conditions: + - type: Conflicted + status: "True" + reason: HostnameConflict + message: All listeners for a given port must use a unique hostname + - type: Ready + status: "False" + reason: Invalid + message: Listener is invalid, see other Conditions for details. + - name: tls-1 + supportedKinds: + - group: gateway.networking.k8s.io + kind: TLSRoute + conditions: + - type: Conflicted + status: "True" + reason: HostnameConflict + message: All listeners for a given port must use a unique hostname + - type: Ready + status: "False" + reason: Invalid + message: Listener is invalid, see other Conditions for details. +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "False" + reason: NoReadyListeners + message: There are no ready listeners for this parent ref +tlsRoutes: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-1 + port: 8080 + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "False" + reason: NoReadyListeners + message: There are no ready listeners for this parent ref + +xdsIR: + envoy-gateway-gateway-1: {} +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:translator-tests + listeners: + - address: "" diff --git a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-hostname.out.yaml b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-hostname.out.yaml index b2d6d8a773c..b8319813128 100644 --- a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-hostname.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-hostname.out.yaml @@ -87,6 +87,6 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" diff --git a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-incompatible-protocol.out.yaml b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-incompatible-protocol.out.yaml index 66d91bfb60f..3ab2adbf4c3 100644 --- a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-incompatible-protocol.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-incompatible-protocol.out.yaml @@ -87,6 +87,6 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" diff --git a/internal/gatewayapi/testdata/httproute-attaching-to-gateway-with-more-different-listeners.in.yaml b/internal/gatewayapi/testdata/httproute-attaching-to-gateway-with-more-different-listeners.in.yaml new file mode 100644 index 00000000000..7ec5ee38330 --- /dev/null +++ b/internal/gatewayapi/testdata/httproute-attaching-to-gateway-with-more-different-listeners.in.yaml @@ -0,0 +1,82 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http-1 + protocol: HTTP + port: 81 + hostname: foo.com + allowedRoutes: + namespaces: + from: All + - name: http-2 + protocol: HTTP + port: 82 + hostname: bar.com + allowedRoutes: + namespaces: + from: All + - name: http-3 + protocol: HTTP + port: 83 + hostname: foo1.com + allowedRoutes: + namespaces: + from: All + - name: http-4 + protocol: HTTP + port: 84 + hostname: bar1.com + allowedRoutes: + namespaces: + from: All + - name: http-5 + protocol: HTTP + port: 85 + hostname: foo2.com + allowedRoutes: + namespaces: + from: All + - name: http-6 + protocol: HTTP + port: 86 + hostname: bar2.com + allowedRoutes: + namespaces: + from: All + - name: http-7 + protocol: HTTP + port: 87 + hostname: foo3.com + allowedRoutes: + namespaces: + from: All + - name: http-8 + protocol: HTTP + port: 88 + hostname: bar3.com + allowedRoutes: + namespaces: + from: All +httpRoutes: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 diff --git a/internal/gatewayapi/testdata/httproute-attaching-to-gateway-with-more-different-listeners.out.yaml b/internal/gatewayapi/testdata/httproute-attaching-to-gateway-with-more-different-listeners.out.yaml new file mode 100644 index 00000000000..acd287a9516 --- /dev/null +++ b/internal/gatewayapi/testdata/httproute-attaching-to-gateway-with-more-different-listeners.out.yaml @@ -0,0 +1,326 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http-1 + protocol: HTTP + port: 81 + hostname: foo.com + allowedRoutes: + namespaces: + from: All + - name: http-2 + protocol: HTTP + port: 82 + hostname: bar.com + allowedRoutes: + namespaces: + from: All + - name: http-3 + protocol: HTTP + port: 83 + hostname: foo1.com + allowedRoutes: + namespaces: + from: All + - name: http-4 + protocol: HTTP + port: 84 + hostname: bar1.com + allowedRoutes: + namespaces: + from: All + - name: http-5 + protocol: HTTP + port: 85 + hostname: foo2.com + allowedRoutes: + namespaces: + from: All + - name: http-6 + protocol: HTTP + port: 86 + hostname: bar2.com + allowedRoutes: + namespaces: + from: All + - name: http-7 + protocol: HTTP + port: 87 + hostname: foo3.com + allowedRoutes: + namespaces: + from: All + - name: http-8 + protocol: HTTP + port: 88 + hostname: bar3.com + allowedRoutes: + namespaces: + from: All + status: + listeners: + - name: http-1 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + attachedRoutes: 1 + conditions: + - type: Ready + status: "True" + reason: Ready + message: Listener is ready + - name: http-2 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + attachedRoutes: 1 + conditions: + - type: Ready + status: "True" + reason: Ready + message: Listener is ready + - name: http-3 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + attachedRoutes: 1 + conditions: + - type: Ready + status: "True" + reason: Ready + message: Listener is ready + - name: http-4 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + attachedRoutes: 1 + conditions: + - type: Ready + status: "True" + reason: Ready + message: Listener is ready + - name: http-5 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + attachedRoutes: 1 + conditions: + - type: Ready + status: "True" + reason: Ready + message: Listener is ready + - name: http-6 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + attachedRoutes: 1 + conditions: + - type: Ready + status: "True" + reason: Ready + message: Listener is ready + - name: http-7 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + attachedRoutes: 1 + conditions: + - type: Ready + status: "True" + reason: Ready + message: Listener is ready + - name: http-8 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + attachedRoutes: 1 + conditions: + - type: Ready + status: "True" + reason: Ready + message: Listener is ready +httpRoutes: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "True" + reason: Accepted + message: Route is accepted +xdsIR: + envoy-gateway-gateway-1: + http: + - name: envoy-gateway-gateway-1-http-1 + address: 0.0.0.0 + port: 10081 + hostnames: + - foo.com + routes: + - name: default-httproute-1-rule-0-match-0-foo.com + pathMatch: + prefix: "/" + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 + - name: envoy-gateway-gateway-1-http-2 + address: 0.0.0.0 + port: 10082 + hostnames: + - bar.com + routes: + - name: default-httproute-1-rule-0-match-0-bar.com + pathMatch: + prefix: "/" + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 + - name: envoy-gateway-gateway-1-http-3 + address: 0.0.0.0 + port: 10083 + hostnames: + - foo1.com + routes: + - name: default-httproute-1-rule-0-match-0-foo1.com + pathMatch: + prefix: "/" + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 + - name: envoy-gateway-gateway-1-http-4 + address: 0.0.0.0 + port: 10084 + hostnames: + - bar1.com + routes: + - name: default-httproute-1-rule-0-match-0-bar1.com + pathMatch: + prefix: "/" + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 + - name: envoy-gateway-gateway-1-http-5 + address: 0.0.0.0 + port: 10085 + hostnames: + - foo2.com + routes: + - name: default-httproute-1-rule-0-match-0-foo2.com + pathMatch: + prefix: "/" + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 + - name: envoy-gateway-gateway-1-http-6 + address: 0.0.0.0 + port: 10086 + hostnames: + - bar2.com + routes: + - name: default-httproute-1-rule-0-match-0-bar2.com + pathMatch: + prefix: "/" + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 + - name: envoy-gateway-gateway-1-http-7 + address: 0.0.0.0 + port: 10087 + hostnames: + - foo3.com + routes: + - name: default-httproute-1-rule-0-match-0-foo3.com + pathMatch: + prefix: "/" + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 + - name: envoy-gateway-gateway-1-http-8 + address: 0.0.0.0 + port: 10088 + hostnames: + - bar3.com + routes: + - name: default-httproute-1-rule-0-match-0-bar3.com + pathMatch: + prefix: "/" + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:translator-tests + listeners: + - address: "" + ports: + - name: http-1 + protocol: "HTTP" + servicePort: 81 + containerPort: 10081 + - name: http-2 + protocol: "HTTP" + servicePort: 82 + containerPort: 10082 + - name: http-3 + protocol: "HTTP" + servicePort: 83 + containerPort: 10083 + - name: http-4 + protocol: "HTTP" + servicePort: 84 + containerPort: 10084 + - name: http-5 + protocol: "HTTP" + servicePort: 85 + containerPort: 10085 + - name: http-6 + protocol: "HTTP" + servicePort: 86 + containerPort: 10086 + - name: http-7 + protocol: "HTTP" + servicePort: 87 + containerPort: 10087 + - name: http-8 + protocol: "HTTP" + servicePort: 88 + containerPort: 10088 diff --git a/internal/gatewayapi/testdata/httproute-attaching-to-gateway-with-more-listeners.in.yaml b/internal/gatewayapi/testdata/httproute-attaching-to-gateway-with-more-listeners.in.yaml new file mode 100644 index 00000000000..6d6d34395df --- /dev/null +++ b/internal/gatewayapi/testdata/httproute-attaching-to-gateway-with-more-listeners.in.yaml @@ -0,0 +1,82 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http-1 + protocol: HTTP + port: 80 + hostname: foo.com + allowedRoutes: + namespaces: + from: All + - name: http-2 + protocol: HTTP + port: 80 + hostname: bar.com + allowedRoutes: + namespaces: + from: All + - name: http-3 + protocol: HTTP + port: 80 + hostname: foo1.com + allowedRoutes: + namespaces: + from: All + - name: http-4 + protocol: HTTP + port: 80 + hostname: bar1.com + allowedRoutes: + namespaces: + from: All + - name: http-5 + protocol: HTTP + port: 80 + hostname: foo2.com + allowedRoutes: + namespaces: + from: All + - name: http-6 + protocol: HTTP + port: 80 + hostname: bar2.com + allowedRoutes: + namespaces: + from: All + - name: http-7 + protocol: HTTP + port: 80 + hostname: foo3.com + allowedRoutes: + namespaces: + from: All + - name: http-8 + protocol: HTTP + port: 80 + hostname: bar3.com + allowedRoutes: + namespaces: + from: All +httpRoutes: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 diff --git a/internal/gatewayapi/testdata/httproute-attaching-to-gateway-with-more-listeners.out.yaml b/internal/gatewayapi/testdata/httproute-attaching-to-gateway-with-more-listeners.out.yaml new file mode 100644 index 00000000000..6fea241c693 --- /dev/null +++ b/internal/gatewayapi/testdata/httproute-attaching-to-gateway-with-more-listeners.out.yaml @@ -0,0 +1,298 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http-1 + protocol: HTTP + port: 80 + hostname: foo.com + allowedRoutes: + namespaces: + from: All + - name: http-2 + protocol: HTTP + port: 80 + hostname: bar.com + allowedRoutes: + namespaces: + from: All + - name: http-3 + protocol: HTTP + port: 80 + hostname: foo1.com + allowedRoutes: + namespaces: + from: All + - name: http-4 + protocol: HTTP + port: 80 + hostname: bar1.com + allowedRoutes: + namespaces: + from: All + - name: http-5 + protocol: HTTP + port: 80 + hostname: foo2.com + allowedRoutes: + namespaces: + from: All + - name: http-6 + protocol: HTTP + port: 80 + hostname: bar2.com + allowedRoutes: + namespaces: + from: All + - name: http-7 + protocol: HTTP + port: 80 + hostname: foo3.com + allowedRoutes: + namespaces: + from: All + - name: http-8 + protocol: HTTP + port: 80 + hostname: bar3.com + allowedRoutes: + namespaces: + from: All + status: + listeners: + - name: http-1 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + attachedRoutes: 1 + conditions: + - type: Ready + status: "True" + reason: Ready + message: Listener is ready + - name: http-2 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + attachedRoutes: 1 + conditions: + - type: Ready + status: "True" + reason: Ready + message: Listener is ready + - name: http-3 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + attachedRoutes: 1 + conditions: + - type: Ready + status: "True" + reason: Ready + message: Listener is ready + - name: http-4 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + attachedRoutes: 1 + conditions: + - type: Ready + status: "True" + reason: Ready + message: Listener is ready + - name: http-5 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + attachedRoutes: 1 + conditions: + - type: Ready + status: "True" + reason: Ready + message: Listener is ready + - name: http-6 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + attachedRoutes: 1 + conditions: + - type: Ready + status: "True" + reason: Ready + message: Listener is ready + - name: http-7 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + attachedRoutes: 1 + conditions: + - type: Ready + status: "True" + reason: Ready + message: Listener is ready + - name: http-8 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + attachedRoutes: 1 + conditions: + - type: Ready + status: "True" + reason: Ready + message: Listener is ready +httpRoutes: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "True" + reason: Accepted + message: Route is accepted +xdsIR: + envoy-gateway-gateway-1: + http: + - name: envoy-gateway-gateway-1-http-1 + address: 0.0.0.0 + port: 10080 + hostnames: + - foo.com + routes: + - name: default-httproute-1-rule-0-match-0-foo.com + pathMatch: + prefix: "/" + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 + - name: envoy-gateway-gateway-1-http-2 + address: 0.0.0.0 + port: 10080 + hostnames: + - bar.com + routes: + - name: default-httproute-1-rule-0-match-0-bar.com + pathMatch: + prefix: "/" + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 + - name: envoy-gateway-gateway-1-http-3 + address: 0.0.0.0 + port: 10080 + hostnames: + - foo1.com + routes: + - name: default-httproute-1-rule-0-match-0-foo1.com + pathMatch: + prefix: "/" + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 + - name: envoy-gateway-gateway-1-http-4 + address: 0.0.0.0 + port: 10080 + hostnames: + - bar1.com + routes: + - name: default-httproute-1-rule-0-match-0-bar1.com + pathMatch: + prefix: "/" + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 + - name: envoy-gateway-gateway-1-http-5 + address: 0.0.0.0 + port: 10080 + hostnames: + - foo2.com + routes: + - name: default-httproute-1-rule-0-match-0-foo2.com + pathMatch: + prefix: "/" + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 + - name: envoy-gateway-gateway-1-http-6 + address: 0.0.0.0 + port: 10080 + hostnames: + - bar2.com + routes: + - name: default-httproute-1-rule-0-match-0-bar2.com + pathMatch: + prefix: "/" + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 + - name: envoy-gateway-gateway-1-http-7 + address: 0.0.0.0 + port: 10080 + hostnames: + - foo3.com + routes: + - name: default-httproute-1-rule-0-match-0-foo3.com + pathMatch: + prefix: "/" + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 + - name: envoy-gateway-gateway-1-http-8 + address: 0.0.0.0 + port: 10080 + hostnames: + - bar3.com + routes: + - name: default-httproute-1-rule-0-match-0-bar3.com + pathMatch: + prefix: "/" + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:translator-tests + listeners: + - address: "" + ports: + - name: http-1 + protocol: "HTTP" + servicePort: 80 + containerPort: 10080 diff --git a/internal/gatewayapi/testdata/httproute-attaching-to-gateway-with-two-listeners-with-different-ports.out.yaml b/internal/gatewayapi/testdata/httproute-attaching-to-gateway-with-two-listeners-with-different-ports.out.yaml index 8e1de469390..b641ae2d49a 100644 --- a/internal/gatewayapi/testdata/httproute-attaching-to-gateway-with-two-listeners-with-different-ports.out.yaml +++ b/internal/gatewayapi/testdata/httproute-attaching-to-gateway-with-two-listeners-with-different-ports.out.yaml @@ -113,7 +113,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-attaching-to-gateway-with-two-listeners.out.yaml b/internal/gatewayapi/testdata/httproute-attaching-to-gateway-with-two-listeners.out.yaml index 32bcf4d8409..ca7d3c917a0 100644 --- a/internal/gatewayapi/testdata/httproute-attaching-to-gateway-with-two-listeners.out.yaml +++ b/internal/gatewayapi/testdata/httproute-attaching-to-gateway-with-two-listeners.out.yaml @@ -108,7 +108,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-attaching-to-gateway.out.yaml b/internal/gatewayapi/testdata/httproute-attaching-to-gateway.out.yaml index 03294c5496f..2e521d61675 100644 --- a/internal/gatewayapi/testdata/httproute-attaching-to-gateway.out.yaml +++ b/internal/gatewayapi/testdata/httproute-attaching-to-gateway.out.yaml @@ -77,7 +77,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-attaching-to-listener-on-gateway-with-two-listeners.out.yaml b/internal/gatewayapi/testdata/httproute-attaching-to-listener-on-gateway-with-two-listeners.out.yaml index 29157f1eeb4..9d0ebd76c9b 100644 --- a/internal/gatewayapi/testdata/httproute-attaching-to-listener-on-gateway-with-two-listeners.out.yaml +++ b/internal/gatewayapi/testdata/httproute-attaching-to-listener-on-gateway-with-two-listeners.out.yaml @@ -102,7 +102,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-attaching-to-listener.out.yaml b/internal/gatewayapi/testdata/httproute-attaching-to-listener.out.yaml index 6e384d6ba51..071675942cc 100644 --- a/internal/gatewayapi/testdata/httproute-attaching-to-listener.out.yaml +++ b/internal/gatewayapi/testdata/httproute-attaching-to-listener.out.yaml @@ -79,7 +79,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-rule-with-multiple-backends-and-no-weights.out.yaml b/internal/gatewayapi/testdata/httproute-rule-with-multiple-backends-and-no-weights.out.yaml index 907102e5e29..be8b2b1bb16 100644 --- a/internal/gatewayapi/testdata/httproute-rule-with-multiple-backends-and-no-weights.out.yaml +++ b/internal/gatewayapi/testdata/httproute-rule-with-multiple-backends-and-no-weights.out.yaml @@ -87,7 +87,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-rule-with-multiple-backends-and-weights.out.yaml b/internal/gatewayapi/testdata/httproute-rule-with-multiple-backends-and-weights.out.yaml index 98233570d75..dd2c8c35d14 100644 --- a/internal/gatewayapi/testdata/httproute-rule-with-multiple-backends-and-weights.out.yaml +++ b/internal/gatewayapi/testdata/httproute-rule-with-multiple-backends-and-weights.out.yaml @@ -90,7 +90,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-with-backendref-in-other-namespace-allowed-by-refgrant.out.yaml b/internal/gatewayapi/testdata/httproute-with-backendref-in-other-namespace-allowed-by-refgrant.out.yaml index 6f2809ff6e8..379aa0c1aa2 100644 --- a/internal/gatewayapi/testdata/httproute-with-backendref-in-other-namespace-allowed-by-refgrant.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-backendref-in-other-namespace-allowed-by-refgrant.out.yaml @@ -79,7 +79,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-with-header-filter-duplicate-add-multiple-filters.out.yaml b/internal/gatewayapi/testdata/httproute-with-header-filter-duplicate-add-multiple-filters.out.yaml index 1408cc0eae4..f7162af9ab4 100644 --- a/internal/gatewayapi/testdata/httproute-with-header-filter-duplicate-add-multiple-filters.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-header-filter-duplicate-add-multiple-filters.out.yaml @@ -73,10 +73,6 @@ httpRoutes: status: "True" reason: Accepted message: Route is accepted - - type: ResolvedRefs - status: "False" - reason: UnsupportedValue - message: "RequestHeaderModifier Filter already configures request header: add-header-1 to be added, ignoring second entry" xdsIR: envoy-gateway-gateway-1: http: @@ -114,7 +110,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-with-header-filter-duplicate-adds.out.yaml b/internal/gatewayapi/testdata/httproute-with-header-filter-duplicate-adds.out.yaml index 011724c4560..54ab4403f29 100644 --- a/internal/gatewayapi/testdata/httproute-with-header-filter-duplicate-adds.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-header-filter-duplicate-adds.out.yaml @@ -83,11 +83,6 @@ httpRoutes: status: "True" reason: Accepted message: Route is accepted - - type: ResolvedRefs - status: "False" - reason: UnsupportedValue - # Currently only one invalid value status will be set. If there are multiple, then only the latest is displayed until that issue is resolved. - message: "RequestHeaderModifier Filter already configures request header: set-header-4 to be added/set, ignoring second entry" xdsIR: envoy-gateway-gateway-1: http: @@ -131,7 +126,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-with-header-filter-duplicate-remove-multiple-filters.out.yaml b/internal/gatewayapi/testdata/httproute-with-header-filter-duplicate-remove-multiple-filters.out.yaml index 003288773a2..14906d98cf2 100644 --- a/internal/gatewayapi/testdata/httproute-with-header-filter-duplicate-remove-multiple-filters.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-header-filter-duplicate-remove-multiple-filters.out.yaml @@ -69,10 +69,6 @@ httpRoutes: status: "True" reason: Accepted message: Route is accepted - - type: ResolvedRefs - status: "False" - reason: UnsupportedValue - message: "RequestHeaderModifier Filter already configures request header: rem-header-1 to be removed, ignoring second entry" xdsIR: envoy-gateway-gateway-1: http: @@ -104,7 +100,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-with-header-filter-duplicate-removes.out.yaml b/internal/gatewayapi/testdata/httproute-with-header-filter-duplicate-removes.out.yaml index 7123200aff5..6e551f461b5 100644 --- a/internal/gatewayapi/testdata/httproute-with-header-filter-duplicate-removes.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-header-filter-duplicate-removes.out.yaml @@ -64,10 +64,6 @@ httpRoutes: status: "True" reason: Accepted message: Route is accepted - - type: ResolvedRefs - status: "False" - reason: UnsupportedValue - message: "RequestHeaderModifier Filter already configures request header: some-header-1 to be removed, ignoring second entry" xdsIR: envoy-gateway-gateway-1: http: @@ -97,7 +93,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-with-header-filter-empty-header-values.out.yaml b/internal/gatewayapi/testdata/httproute-with-header-filter-empty-header-values.out.yaml index aa8c5e0ceff..372e90ebf7a 100644 --- a/internal/gatewayapi/testdata/httproute-with-header-filter-empty-header-values.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-header-filter-empty-header-values.out.yaml @@ -101,7 +101,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-with-header-filter-empty-headers.out.yaml b/internal/gatewayapi/testdata/httproute-with-header-filter-empty-headers.out.yaml index 238545928dd..b35d829de07 100644 --- a/internal/gatewayapi/testdata/httproute-with-header-filter-empty-headers.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-header-filter-empty-headers.out.yaml @@ -66,13 +66,9 @@ httpRoutes: controllerName: gateway.envoyproxy.io/gatewayclass-controller conditions: - type: Accepted - status: "True" - reason: Accepted - message: Route is accepted - - type: ResolvedRefs status: "False" reason: UnsupportedValue - message: "RequestHeaderModifier Filter cannot set a header with an empty name" + message: RequestHeaderModifier Filter cannot set a header with an empty name xdsIR: envoy-gateway-gateway-1: http: @@ -104,7 +100,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-with-header-filter-invalid-headers.out.yaml b/internal/gatewayapi/testdata/httproute-with-header-filter-invalid-headers.out.yaml index e8d0ff09547..0551d6c7076 100644 --- a/internal/gatewayapi/testdata/httproute-with-header-filter-invalid-headers.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-header-filter-invalid-headers.out.yaml @@ -66,10 +66,6 @@ httpRoutes: controllerName: gateway.envoyproxy.io/gatewayclass-controller conditions: - type: Accepted - status: "True" - reason: Accepted - message: Route is accepted - - type: ResolvedRefs status: "False" reason: UnsupportedValue message: "RequestHeaderModifier Filter cannot set headers with a '/' or ':' character in them. Header: 'example:1'" @@ -104,7 +100,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-with-header-filter-no-headers.out.yaml b/internal/gatewayapi/testdata/httproute-with-header-filter-no-headers.out.yaml index d7e95be2b3e..daace524950 100644 --- a/internal/gatewayapi/testdata/httproute-with-header-filter-no-headers.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-header-filter-no-headers.out.yaml @@ -91,7 +91,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-with-header-filter-no-valid-headers.out.yaml b/internal/gatewayapi/testdata/httproute-with-header-filter-no-valid-headers.out.yaml index a63a937bb2b..31ef9cc2688 100644 --- a/internal/gatewayapi/testdata/httproute-with-header-filter-no-valid-headers.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-header-filter-no-valid-headers.out.yaml @@ -61,10 +61,6 @@ httpRoutes: controllerName: gateway.envoyproxy.io/gatewayclass-controller conditions: - type: Accepted - status: "True" - reason: Accepted - message: Route is accepted - - type: ResolvedRefs status: "False" reason: UnsupportedValue message: "RequestHeaderModifier Filter did not provide valid configuration to add/set/remove any headers" @@ -95,7 +91,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-with-header-filter-remove.out.yaml b/internal/gatewayapi/testdata/httproute-with-header-filter-remove.out.yaml index 40d0556f5ef..ca5123dd3cd 100644 --- a/internal/gatewayapi/testdata/httproute-with-header-filter-remove.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-header-filter-remove.out.yaml @@ -96,7 +96,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-bad-port.out.yaml b/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-bad-port.out.yaml index 18fc9ff2c89..3c277285a9e 100644 --- a/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-bad-port.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-bad-port.out.yaml @@ -79,10 +79,10 @@ infraIR: proxy: metadata: labels: - gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-invalid-group.out.yaml b/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-invalid-group.out.yaml index 92d17de3237..5b09850fa10 100644 --- a/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-invalid-group.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-invalid-group.out.yaml @@ -84,7 +84,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-invalid-kind.out.yaml b/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-invalid-kind.out.yaml index a0b76319001..548e238ba82 100644 --- a/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-invalid-kind.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-invalid-kind.out.yaml @@ -80,10 +80,10 @@ infraIR: proxy: metadata: labels: - gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-no-port.out.yaml b/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-no-port.out.yaml index 634ed1b1bcb..a9cf51c095b 100644 --- a/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-no-port.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-no-port.out.yaml @@ -78,10 +78,10 @@ infraIR: proxy: metadata: labels: - gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-no-service.out.yaml b/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-no-service.out.yaml index 4dfdd8dd667..8aabd05f3a1 100644 --- a/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-no-service.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-no-service.out.yaml @@ -79,10 +79,10 @@ infraIR: proxy: metadata: labels: - gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-with-invalid-backendref-in-other-namespace.out.yaml b/internal/gatewayapi/testdata/httproute-with-invalid-backendref-in-other-namespace.out.yaml index 1ffa12050dd..ed769a16a6f 100644 --- a/internal/gatewayapi/testdata/httproute-with-invalid-backendref-in-other-namespace.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-invalid-backendref-in-other-namespace.out.yaml @@ -83,7 +83,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-with-non-matching-specific-hostname-attaching-to-gateway-with-wildcard-hostname.out.yaml b/internal/gatewayapi/testdata/httproute-with-non-matching-specific-hostname-attaching-to-gateway-with-wildcard-hostname.out.yaml index 3bb183f1475..b9baf17ed99 100644 --- a/internal/gatewayapi/testdata/httproute-with-non-matching-specific-hostname-attaching-to-gateway-with-wildcard-hostname.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-non-matching-specific-hostname-attaching-to-gateway-with-wildcard-hostname.out.yaml @@ -72,7 +72,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-with-redirect-filter-full-path-replace-https.out.yaml b/internal/gatewayapi/testdata/httproute-with-redirect-filter-full-path-replace-https.out.yaml index 3d42bf63d9e..a8f3819cce4 100644 --- a/internal/gatewayapi/testdata/httproute-with-redirect-filter-full-path-replace-https.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-redirect-filter-full-path-replace-https.out.yaml @@ -94,7 +94,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-with-redirect-filter-hostname.out.yaml b/internal/gatewayapi/testdata/httproute-with-redirect-filter-hostname.out.yaml index 79ffd78cdd0..e516810982c 100644 --- a/internal/gatewayapi/testdata/httproute-with-redirect-filter-hostname.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-redirect-filter-hostname.out.yaml @@ -91,7 +91,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-with-redirect-filter-invalid-filter-type.in.yaml b/internal/gatewayapi/testdata/httproute-with-redirect-filter-invalid-filter-type.in.yaml index 32d8717c5fc..e40076f5b56 100644 --- a/internal/gatewayapi/testdata/httproute-with-redirect-filter-invalid-filter-type.in.yaml +++ b/internal/gatewayapi/testdata/httproute-with-redirect-filter-invalid-filter-type.in.yaml @@ -35,8 +35,9 @@ httpRoutes: - name: service-1 port: 8080 filters: - - type: UnsupportedType - requestRedirect: - scheme: https - statusCode: 301 + - type: ExtensionRef + extensionRef: + group: unsupported.group.io + kind: UnsupportedKind + name: unsupported diff --git a/internal/gatewayapi/testdata/httproute-with-redirect-filter-invalid-filter-type.out.yaml b/internal/gatewayapi/testdata/httproute-with-redirect-filter-invalid-filter-type.out.yaml index 7a210ae376e..b9cea0da772 100644 --- a/internal/gatewayapi/testdata/httproute-with-redirect-filter-invalid-filter-type.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-redirect-filter-invalid-filter-type.out.yaml @@ -47,10 +47,11 @@ httpRoutes: - name: service-1 port: 8080 filters: - - type: UnsupportedType - requestRedirect: - scheme: https - statusCode: 301 + - type: ExtensionRef + extensionRef: + group: unsupported.group.io + kind: UnsupportedKind + name: unsupported status: parents: - parentRef: @@ -60,13 +61,9 @@ httpRoutes: controllerName: gateway.envoyproxy.io/gatewayclass-controller conditions: - type: Accepted - status: "True" - reason: Accepted - message: Route is accepted - - type: ResolvedRefs status: "False" reason: UnsupportedValue - message: "Unknown custom filter type: UnsupportedType" + message: "Unknown custom filter type: ExtensionRef" xdsIR: envoy-gateway-gateway-1: http: @@ -85,7 +82,7 @@ xdsIR: # I believe the correct way to handle an invalid filter should be to allow the HTTPRoute to function # normally but leave out the filter config and set the status, but this behaviour can be changed. directResponse: - body: "Unknown custom filter type: UnsupportedType" + body: "Unknown custom filter type: ExtensionRef" statusCode: 500 infraIR: envoy-gateway-gateway-1: @@ -95,7 +92,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-with-redirect-filter-invalid-scheme.out.yaml b/internal/gatewayapi/testdata/httproute-with-redirect-filter-invalid-scheme.out.yaml index a663120def3..9ca16051d6b 100644 --- a/internal/gatewayapi/testdata/httproute-with-redirect-filter-invalid-scheme.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-redirect-filter-invalid-scheme.out.yaml @@ -60,10 +60,6 @@ httpRoutes: controllerName: gateway.envoyproxy.io/gatewayclass-controller conditions: - type: Accepted - status: "True" - reason: Accepted - message: Route is accepted - - type: ResolvedRefs status: "False" reason: UnsupportedValue message: "Scheme: unknown is unsupported, only 'https' and 'http' are supported" @@ -96,7 +92,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-with-redirect-filter-invalid-status.out.yaml b/internal/gatewayapi/testdata/httproute-with-redirect-filter-invalid-status.out.yaml index 998d188aa3a..e9a96d4294c 100644 --- a/internal/gatewayapi/testdata/httproute-with-redirect-filter-invalid-status.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-redirect-filter-invalid-status.out.yaml @@ -60,10 +60,6 @@ httpRoutes: controllerName: gateway.envoyproxy.io/gatewayclass-controller conditions: - type: Accepted - status: "True" - reason: Accepted - message: Route is accepted - - type: ResolvedRefs status: "False" reason: UnsupportedValue message: "Status code 666 is invalid, only 302 and 301 are supported" @@ -96,7 +92,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-with-redirect-filter-prefix-replace-with-port-http.out.yaml b/internal/gatewayapi/testdata/httproute-with-redirect-filter-prefix-replace-with-port-http.out.yaml index 59361b9ede0..4b8f4d89afb 100644 --- a/internal/gatewayapi/testdata/httproute-with-redirect-filter-prefix-replace-with-port-http.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-redirect-filter-prefix-replace-with-port-http.out.yaml @@ -96,7 +96,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-with-single-rule-with-exact-path-match.out.yaml b/internal/gatewayapi/testdata/httproute-with-single-rule-with-exact-path-match.out.yaml index 0e074c278b5..cb0cde346dc 100644 --- a/internal/gatewayapi/testdata/httproute-with-single-rule-with-exact-path-match.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-single-rule-with-exact-path-match.out.yaml @@ -78,7 +78,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-with-single-rule-with-http-method-match.in.yaml b/internal/gatewayapi/testdata/httproute-with-single-rule-with-http-method-match.in.yaml new file mode 100644 index 00000000000..3d372e82c8d --- /dev/null +++ b/internal/gatewayapi/testdata/httproute-with-single-rule-with-http-method-match.in.yaml @@ -0,0 +1,31 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + 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 +httpRoutes: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - matches: + - method: POST + backendRefs: + - name: service-1 + port: 8080 diff --git a/internal/gatewayapi/testdata/httproute-with-single-rule-with-http-method-match.out.yaml b/internal/gatewayapi/testdata/httproute-with-single-rule-with-http-method-match.out.yaml new file mode 100644 index 00000000000..d98b7037756 --- /dev/null +++ b/internal/gatewayapi/testdata/httproute-with-single-rule-with-http-method-match.out.yaml @@ -0,0 +1,87 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + 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 + status: + listeners: + - name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + attachedRoutes: 1 + conditions: + - type: Ready + status: "True" + reason: Ready + message: Listener is ready +httpRoutes: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - matches: + - method: POST + backendRefs: + - name: service-1 + port: 8080 + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "True" + reason: Accepted + message: Route is accepted +xdsIR: + envoy-gateway-gateway-1: + http: + - name: envoy-gateway-gateway-1-http + address: 0.0.0.0 + port: 10080 + hostnames: + - "*" + routes: + - name: default-httproute-1-rule-0-match-0-* + headerMatches: + - name: ":method" + exact: POST + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:translator-tests + listeners: + - address: "" + ports: + - name: http + protocol: "HTTP" + servicePort: 80 + containerPort: 10080 diff --git a/internal/gatewayapi/testdata/httproute-with-single-rule-with-multiple-rules.in.yaml b/internal/gatewayapi/testdata/httproute-with-single-rule-with-multiple-rules.in.yaml new file mode 100644 index 00000000000..9fd68520b16 --- /dev/null +++ b/internal/gatewayapi/testdata/httproute-with-single-rule-with-multiple-rules.in.yaml @@ -0,0 +1,63 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + 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 +httpRoutes: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - matches: + - path: + type: Exact + value: "/exact" + headers: + - name: Header-1 + type: Exact + value: "exact" + queryParams: + - name: QueryParam-1 + type: Exact + value: "exact" + backendRefs: + - name: service-1 + port: 8080 + - matches: + - path: + type: PathPrefix + value: "/prefix" + backendRefs: + - name: service-2 + port: 8080 + - matches: + - path: + type: RegularExpression + value: "*regex*" + headers: + - name: Header-1 + type: RegularExpression + value: "*regex*" + queryParams: + - name: QueryParam-1 + type: RegularExpression + value: "*regex*" + backendRefs: + - name: service-3 + port: 8080 diff --git a/internal/gatewayapi/testdata/httproute-with-single-rule-with-multiple-rules.out.yaml b/internal/gatewayapi/testdata/httproute-with-single-rule-with-multiple-rules.out.yaml new file mode 100644 index 00000000000..67e4a9b5034 --- /dev/null +++ b/internal/gatewayapi/testdata/httproute-with-single-rule-with-multiple-rules.out.yaml @@ -0,0 +1,144 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + 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 + status: + listeners: + - name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + attachedRoutes: 1 + conditions: + - type: Ready + status: "True" + reason: Ready + message: Listener is ready +httpRoutes: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - matches: + - path: + type: Exact + value: "/exact" + headers: + - name: Header-1 + type: Exact + value: "exact" + queryParams: + - name: QueryParam-1 + type: Exact + value: "exact" + backendRefs: + - name: service-1 + port: 8080 + - matches: + - path: + type: PathPrefix + value: "/prefix" + backendRefs: + - name: service-2 + port: 8080 + - matches: + - path: + type: RegularExpression + value: "*regex*" + headers: + - name: Header-1 + type: RegularExpression + value: "*regex*" + queryParams: + - name: QueryParam-1 + type: RegularExpression + value: "*regex*" + backendRefs: + - name: service-3 + port: 8080 + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "True" + reason: Accepted + message: Route is accepted +xdsIR: + envoy-gateway-gateway-1: + http: + - name: envoy-gateway-gateway-1-http + address: 0.0.0.0 + port: 10080 + hostnames: + - "*" + routes: + - name: default-httproute-1-rule-2-match-0-* + pathMatch: + safeRegex: "*regex*" + headerMatches: + - name: "Header-1" + safeRegex: "*regex*" + queryParamMatches: + - name: "QueryParam-1" + safeRegex: "*regex*" + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 + - name: default-httproute-1-rule-1-match-0-* + pathMatch: + prefix: "/prefix" + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 + - name: default-httproute-1-rule-0-match-0-* + pathMatch: + exact: "/exact" + headerMatches: + - name: "Header-1" + exact: "exact" + queryParamMatches: + - name: "QueryParam-1" + exact: "exact" + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:translator-tests + listeners: + - address: "" + ports: + - name: http + protocol: "HTTP" + servicePort: 80 + containerPort: 10080 diff --git a/internal/gatewayapi/testdata/httproute-with-single-rule-with-path-prefix-and-exact-header-matches.out.yaml b/internal/gatewayapi/testdata/httproute-with-single-rule-with-path-prefix-and-exact-header-matches.out.yaml index cc0b4812552..851b5c7802e 100644 --- a/internal/gatewayapi/testdata/httproute-with-single-rule-with-path-prefix-and-exact-header-matches.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-single-rule-with-path-prefix-and-exact-header-matches.out.yaml @@ -87,7 +87,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-with-some-invalid-backend-refs-no-service.out.yaml b/internal/gatewayapi/testdata/httproute-with-some-invalid-backend-refs-no-service.out.yaml index 0a657f4ffd7..c1e7d30a17b 100644 --- a/internal/gatewayapi/testdata/httproute-with-some-invalid-backend-refs-no-service.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-some-invalid-backend-refs-no-service.out.yaml @@ -89,7 +89,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-with-specific-hostname-attaching-to-gateway-with-wildcard-hostname.out.yaml b/internal/gatewayapi/testdata/httproute-with-specific-hostname-attaching-to-gateway-with-wildcard-hostname.out.yaml index feb6375de05..2cb39d4f047 100644 --- a/internal/gatewayapi/testdata/httproute-with-specific-hostname-attaching-to-gateway-with-wildcard-hostname.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-specific-hostname-attaching-to-gateway-with-wildcard-hostname.out.yaml @@ -83,7 +83,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-with-two-specific-hostnames-attaching-to-gateway-with-wildcard-hostname.out.yaml b/internal/gatewayapi/testdata/httproute-with-two-specific-hostnames-attaching-to-gateway-with-wildcard-hostname.out.yaml index f632488ce31..2a203e13f4b 100644 --- a/internal/gatewayapi/testdata/httproute-with-two-specific-hostnames-attaching-to-gateway-with-wildcard-hostname.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-two-specific-hostnames-attaching-to-gateway-with-wildcard-hostname.out.yaml @@ -94,7 +94,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-filter-type.in.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-filter-type.in.yaml new file mode 100644 index 00000000000..17a0293da6b --- /dev/null +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-filter-type.in.yaml @@ -0,0 +1,41 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + hostname: "*.envoyproxy.io" + allowedRoutes: + namespaces: + from: All +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 + filters: + - type: URLRewrite + urlRewrite: + hostname: urlrewrite.envoyproxy.io + diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-filter-type.out.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-filter-type.out.yaml new file mode 100644 index 00000000000..6e6ac4f447d --- /dev/null +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-filter-type.out.yaml @@ -0,0 +1,100 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + hostname: "*.envoyproxy.io" + allowedRoutes: + namespaces: + from: All + status: + listeners: + - name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + attachedRoutes: 1 + conditions: + - type: Ready + status: "True" + reason: Ready + message: Listener is ready +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 + filters: + - type: URLRewrite + urlRewrite: + hostname: urlrewrite.envoyproxy.io + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + sectionName: http + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "False" + reason: UnsupportedValue + message: "Unsupported filter type: URLRewrite" +xdsIR: + envoy-gateway-gateway-1: + http: + - name: envoy-gateway-gateway-1-http + address: 0.0.0.0 + port: 10080 + hostnames: + - "*.envoyproxy.io" + routes: + - name: default-httproute-1-rule-0-match-0-gateway.envoyproxy.io + pathMatch: + prefix: "/" + headerMatches: + - name: ":authority" + exact: gateway.envoyproxy.io + # I believe the correct way to handle an invalid filter should be to allow the HTTPRoute to function + # normally but leave out the filter config and set the status, but this behaviour can be changed. + directResponse: + body: "Unsupported filter type: URLRewrite" + statusCode: 500 +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:translator-tests + listeners: + - address: "" + ports: + - name: http + protocol: "HTTP" + containerPort: 10080 + servicePort: 80 diff --git a/internal/gatewayapi/testdata/httproutes-with-multiple-matches.out.yaml b/internal/gatewayapi/testdata/httproutes-with-multiple-matches.out.yaml index 60ad2e5736f..1c300b1ad5a 100644 --- a/internal/gatewayapi/testdata/httproutes-with-multiple-matches.out.yaml +++ b/internal/gatewayapi/testdata/httproutes-with-multiple-matches.out.yaml @@ -187,10 +187,9 @@ xdsIR: - name: envoy-gateway-httproute-2-rule-0-match-0-example.com pathMatch: prefix: "/v1/example" - # Remove comments once https://github.com/envoyproxy/gateway/issues/512 is fixed - # queryParamMatches: - # - name: "debug" - # exact: "yes" + queryParamMatches: + - name: "debug" + exact: "yes" headerMatches: - name: :authority exact: example.com @@ -245,7 +244,7 @@ infraIR: gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 name: envoy-gateway-gateway-1 - image: envoyproxy/envoy:v1.23-latest + image: envoyproxy/envoy:translator-tests listeners: - address: "" ports: diff --git a/internal/gatewayapi/testdata/tlsroute-attaching-to-gateway.in.yaml b/internal/gatewayapi/testdata/tlsroute-attaching-to-gateway.in.yaml new file mode 100644 index 00000000000..71db6c0ece6 --- /dev/null +++ b/internal/gatewayapi/testdata/tlsroute-attaching-to-gateway.in.yaml @@ -0,0 +1,32 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls + protocol: TLS + hostname: foo.com + port: 90 + tls: + mode: Passthrough + allowedRoutes: + namespaces: + from: All +tlsRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-1 + port: 8080 diff --git a/internal/gatewayapi/testdata/tlsroute-attaching-to-gateway.out.yaml b/internal/gatewayapi/testdata/tlsroute-attaching-to-gateway.out.yaml new file mode 100644 index 00000000000..a6d52c33719 --- /dev/null +++ b/internal/gatewayapi/testdata/tlsroute-attaching-to-gateway.out.yaml @@ -0,0 +1,84 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls + protocol: TLS + hostname: foo.com + port: 90 + tls: + mode: Passthrough + allowedRoutes: + namespaces: + from: All + status: + listeners: + - name: tls + supportedKinds: + - group: gateway.networking.k8s.io + kind: TLSRoute + attachedRoutes: 1 + conditions: + - type: Ready + status: "True" + reason: Ready + message: Listener is ready +tlsRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-1 + port: 8080 + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "True" + reason: Accepted + message: Route is accepted +xdsIR: + envoy-gateway-gateway-1: + tcp: + - name: envoy-gateway-gateway-1-tls-tlsroute-1 + address: 0.0.0.0 + port: 10090 + tls: + snis: + - foo.com + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:translator-tests + listeners: + - address: "" + ports: + - name: tls + protocol: "TLS" + servicePort: 90 + containerPort: 10090 diff --git a/internal/gatewayapi/testdata/tlsroute-multiple.in.yaml b/internal/gatewayapi/testdata/tlsroute-multiple.in.yaml new file mode 100644 index 00000000000..f7c756f0cca --- /dev/null +++ b/internal/gatewayapi/testdata/tlsroute-multiple.in.yaml @@ -0,0 +1,58 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls + protocol: TLS + port: 91 + tls: + mode: Passthrough + allowedRoutes: + namespaces: + from: All +tlsRoutes: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + hostnames: + - "foo.com" + rules: + - backendRefs: + - name: service-1 + port: 8080 +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-2 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + hostnames: + - "bar.com" + rules: + - backendRefs: + - name: service-1 + port: 8080 +services: +- apiVersion: v1 + kind: Service + metadata: + namespace: default + name: service-1 + spec: + clusterIP: 7.7.7.7 + ports: + - port: 8080 diff --git a/internal/gatewayapi/testdata/tlsroute-multiple.out.yaml b/internal/gatewayapi/testdata/tlsroute-multiple.out.yaml new file mode 100644 index 00000000000..6eba7c09502 --- /dev/null +++ b/internal/gatewayapi/testdata/tlsroute-multiple.out.yaml @@ -0,0 +1,121 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls + protocol: TLS + port: 91 + tls: + mode: Passthrough + allowedRoutes: + namespaces: + from: All + status: + listeners: + - name: tls + supportedKinds: + - group: gateway.networking.k8s.io + kind: TLSRoute + attachedRoutes: 2 + conditions: + - type: Ready + status: "True" + reason: Ready + message: Listener is ready +tlsRoutes: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + hostnames: + - foo.com + rules: + - backendRefs: + - name: service-1 + port: 8080 + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "True" + reason: Accepted + message: Route is accepted +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-2 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + hostnames: + - bar.com + rules: + - backendRefs: + - name: service-1 + port: 8080 + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "True" + reason: Accepted + message: Route is accepted +xdsIR: + envoy-gateway-gateway-1: + tcp: + - name: envoy-gateway-gateway-1-tls-tlsroute-1 + address: 0.0.0.0 + port: 10091 + tls: + snis: + - foo.com + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 + - name: envoy-gateway-gateway-1-tls-tlsroute-2 + address: 0.0.0.0 + port: 10091 + tls: + snis: + - bar.com + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:translator-tests + listeners: + - address: "" + ports: + - name: tls + protocol: "TLS" + servicePort: 91 + containerPort: 10091 diff --git a/internal/gatewayapi/testdata/tlsroute-not-attaching-to-gateway-with-incorrect-mode.in.yaml b/internal/gatewayapi/testdata/tlsroute-not-attaching-to-gateway-with-incorrect-mode.in.yaml new file mode 100644 index 00000000000..d8a62c2adf3 --- /dev/null +++ b/internal/gatewayapi/testdata/tlsroute-not-attaching-to-gateway-with-incorrect-mode.in.yaml @@ -0,0 +1,31 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls + protocol: TLS + tls: + mode: Terminate + port: 90 + allowedRoutes: + namespaces: + from: All +tlsRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-1 + port: 8080 diff --git a/internal/gatewayapi/testdata/tlsroute-not-attaching-to-gateway-with-incorrect-mode.out.yaml b/internal/gatewayapi/testdata/tlsroute-not-attaching-to-gateway-with-incorrect-mode.out.yaml new file mode 100644 index 00000000000..59d04cad1af --- /dev/null +++ b/internal/gatewayapi/testdata/tlsroute-not-attaching-to-gateway-with-incorrect-mode.out.yaml @@ -0,0 +1,67 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls + protocol: TLS + tls: + mode: Terminate + port: 90 + allowedRoutes: + namespaces: + from: All + status: + listeners: + - name: tls + supportedKinds: + - group: gateway.networking.k8s.io + kind: TLSRoute + attachedRoutes: 0 + conditions: + - type: Ready + status: "False" + reason: UnsupportedTLSMode + message: TLS Terminate mode is not supported, TLS mode must be Passthrough. +tlsRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-1 + port: 8080 + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "False" + reason: NoReadyListeners + message: There are no ready listeners for this parent ref +xdsIR: + envoy-gateway-gateway-1: {} +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:translator-tests + listeners: + - address: "" diff --git a/internal/gatewayapi/testdata/tlsroute-not-attaching-to-gateway-with-no-mode.in.yaml b/internal/gatewayapi/testdata/tlsroute-not-attaching-to-gateway-with-no-mode.in.yaml new file mode 100644 index 00000000000..38395404cd8 --- /dev/null +++ b/internal/gatewayapi/testdata/tlsroute-not-attaching-to-gateway-with-no-mode.in.yaml @@ -0,0 +1,29 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls + protocol: TLS + port: 90 + allowedRoutes: + namespaces: + from: All +tlsRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-1 + port: 8080 diff --git a/internal/gatewayapi/testdata/tlsroute-not-attaching-to-gateway-with-no-mode.out.yaml b/internal/gatewayapi/testdata/tlsroute-not-attaching-to-gateway-with-no-mode.out.yaml new file mode 100644 index 00000000000..a055531eac7 --- /dev/null +++ b/internal/gatewayapi/testdata/tlsroute-not-attaching-to-gateway-with-no-mode.out.yaml @@ -0,0 +1,65 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls + protocol: TLS + port: 90 + allowedRoutes: + namespaces: + from: All + status: + listeners: + - name: tls + supportedKinds: + - group: gateway.networking.k8s.io + kind: TLSRoute + attachedRoutes: 0 + conditions: + - type: Ready + status: "False" + reason: Invalid + message: Listener must have TLS set when protocol is TLS. +tlsRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-1 + port: 8080 + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "False" + reason: NoReadyListeners + message: There are no ready listeners for this parent ref +xdsIR: + envoy-gateway-gateway-1: {} +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:translator-tests + listeners: + - address: "" diff --git a/internal/gatewayapi/testdata/tlsroute-with-backendref-in-other-namespace-allowed-by-refgrant.in.yaml b/internal/gatewayapi/testdata/tlsroute-with-backendref-in-other-namespace-allowed-by-refgrant.in.yaml new file mode 100644 index 00000000000..d18aca10b82 --- /dev/null +++ b/internal/gatewayapi/testdata/tlsroute-with-backendref-in-other-namespace-allowed-by-refgrant.in.yaml @@ -0,0 +1,57 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls + protocol: TLS + hostname: foo.com + port: 90 + tls: + mode: Passthrough + allowedRoutes: + namespaces: + from: All +tlsRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-1 + namespace: test-service-namespace + port: 8080 +services: + - apiVersion: v1 + kind: Service + metadata: + namespace: test-service-namespace + name: service-1 + spec: + clusterIP: 7.7.7.7 + ports: + - port: 8080 +referenceGrants: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: ReferenceGrant + metadata: + namespace: test-service-namespace + name: referencegrant-1 + spec: + from: + - group: gateway.networking.k8s.io + kind: TLSRoute + namespace: default + to: + - group: "" + kind: Service diff --git a/internal/gatewayapi/testdata/tlsroute-with-backendref-in-other-namespace-allowed-by-refgrant.out.yaml b/internal/gatewayapi/testdata/tlsroute-with-backendref-in-other-namespace-allowed-by-refgrant.out.yaml new file mode 100644 index 00000000000..16acfdeaffd --- /dev/null +++ b/internal/gatewayapi/testdata/tlsroute-with-backendref-in-other-namespace-allowed-by-refgrant.out.yaml @@ -0,0 +1,85 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls + protocol: TLS + hostname: foo.com + port: 90 + tls: + mode: Passthrough + allowedRoutes: + namespaces: + from: All + status: + listeners: + - name: tls + supportedKinds: + - group: gateway.networking.k8s.io + kind: TLSRoute + attachedRoutes: 1 + conditions: + - type: Ready + status: "True" + reason: Ready + message: Listener is ready +tlsRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-1 + namespace: test-service-namespace + port: 8080 + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "True" + reason: Accepted + message: Route is accepted +xdsIR: + envoy-gateway-gateway-1: + tcp: + - name: envoy-gateway-gateway-1-tls-tlsroute-1 + address: 0.0.0.0 + port: 10090 + tls: + snis: + - foo.com + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:translator-tests + listeners: + - address: "" + ports: + - name: tls + protocol: "TLS" + servicePort: 90 + containerPort: 10090 diff --git a/internal/gatewayapi/testdata/tlsroute-with-empty-hostname.in.yaml b/internal/gatewayapi/testdata/tlsroute-with-empty-hostname.in.yaml new file mode 100644 index 00000000000..071fa048f72 --- /dev/null +++ b/internal/gatewayapi/testdata/tlsroute-with-empty-hostname.in.yaml @@ -0,0 +1,41 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls + protocol: TLS + port: 91 + tls: + mode: Passthrough + allowedRoutes: + namespaces: + from: All +tlsRoutes: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-1 + port: 8080 +services: +- apiVersion: v1 + kind: Service + metadata: + namespace: default + name: service-1 + spec: + clusterIP: 7.7.7.7 + ports: + - port: 8080 diff --git a/internal/gatewayapi/testdata/tlsroute-with-empty-hostname.out.yaml b/internal/gatewayapi/testdata/tlsroute-with-empty-hostname.out.yaml new file mode 100644 index 00000000000..68cea3fb8d4 --- /dev/null +++ b/internal/gatewayapi/testdata/tlsroute-with-empty-hostname.out.yaml @@ -0,0 +1,83 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls + protocol: TLS + port: 91 + tls: + mode: Passthrough + allowedRoutes: + namespaces: + from: All + status: + listeners: + - name: tls + supportedKinds: + - group: gateway.networking.k8s.io + kind: TLSRoute + attachedRoutes: 1 + conditions: + - type: Ready + status: "True" + reason: Ready + message: Listener is ready +tlsRoutes: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-1 + port: 8080 + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "True" + reason: Accepted + message: Route is accepted +xdsIR: + envoy-gateway-gateway-1: + tcp: + - name: envoy-gateway-gateway-1-tls-tlsroute-1 + address: 0.0.0.0 + port: 10091 + tls: + snis: + - "*" + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:translator-tests + listeners: + - address: "" + ports: + - name: tls + protocol: "TLS" + servicePort: 91 + containerPort: 10091 diff --git a/internal/gatewayapi/testdata/tlsroute-with-empty-listener-hostname.in.yaml b/internal/gatewayapi/testdata/tlsroute-with-empty-listener-hostname.in.yaml new file mode 100644 index 00000000000..f5c8e1349a6 --- /dev/null +++ b/internal/gatewayapi/testdata/tlsroute-with-empty-listener-hostname.in.yaml @@ -0,0 +1,43 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls + protocol: TLS + port: 91 + tls: + mode: Passthrough + allowedRoutes: + namespaces: + from: All +tlsRoutes: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + hostnames: + - "foo.com" + rules: + - backendRefs: + - name: service-1 + port: 8080 +services: +- apiVersion: v1 + kind: Service + metadata: + namespace: default + name: service-1 + spec: + clusterIP: 7.7.7.7 + ports: + - port: 8080 diff --git a/internal/gatewayapi/testdata/tlsroute-with-empty-listener-hostname.out.yaml b/internal/gatewayapi/testdata/tlsroute-with-empty-listener-hostname.out.yaml new file mode 100644 index 00000000000..3c086a72820 --- /dev/null +++ b/internal/gatewayapi/testdata/tlsroute-with-empty-listener-hostname.out.yaml @@ -0,0 +1,85 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls + protocol: TLS + port: 91 + tls: + mode: Passthrough + allowedRoutes: + namespaces: + from: All + status: + listeners: + - name: tls + supportedKinds: + - group: gateway.networking.k8s.io + kind: TLSRoute + attachedRoutes: 1 + conditions: + - type: Ready + status: "True" + reason: Ready + message: Listener is ready +tlsRoutes: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + hostnames: + - foo.com + rules: + - backendRefs: + - name: service-1 + port: 8080 + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "True" + reason: Accepted + message: Route is accepted +xdsIR: + envoy-gateway-gateway-1: + tcp: + - name: envoy-gateway-gateway-1-tls-tlsroute-1 + address: 0.0.0.0 + port: 10091 + tls: + snis: + - foo.com + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:translator-tests + listeners: + - address: "" + ports: + - name: tls + protocol: "TLS" + servicePort: 91 + containerPort: 10091 diff --git a/internal/gatewayapi/testdata/tlsroute-with-listener-both-passthrough-and-cert-data.in.yaml b/internal/gatewayapi/testdata/tlsroute-with-listener-both-passthrough-and-cert-data.in.yaml new file mode 100644 index 00000000000..81afa2331b1 --- /dev/null +++ b/internal/gatewayapi/testdata/tlsroute-with-listener-both-passthrough-and-cert-data.in.yaml @@ -0,0 +1,44 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls + protocol: TLS + hostname: foo.com + tls: + mode: Passthrough + certificateRefs: + - name: tls-secret-1 + port: 90 + allowedRoutes: + namespaces: + from: All +tlsRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-1 + port: 8080 +secrets: + - apiVersion: v1 + kind: Secret + metadata: + namespace: envoy-gateway + name: tls-secret-1 + type: kubernetes.io/tls + data: + tls.crt: Zm9vCg== + tls.key: YmFyCg== diff --git a/internal/gatewayapi/testdata/tlsroute-with-listener-both-passthrough-and-cert-data.out.yaml b/internal/gatewayapi/testdata/tlsroute-with-listener-both-passthrough-and-cert-data.out.yaml new file mode 100644 index 00000000000..34c53240043 --- /dev/null +++ b/internal/gatewayapi/testdata/tlsroute-with-listener-both-passthrough-and-cert-data.out.yaml @@ -0,0 +1,70 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls + protocol: TLS + hostname: foo.com + tls: + mode: Passthrough + certificateRefs: + - name: tls-secret-1 + port: 90 + allowedRoutes: + namespaces: + from: All + status: + listeners: + - name: tls + supportedKinds: + - group: gateway.networking.k8s.io + kind: TLSRoute + attachedRoutes: 0 + conditions: + - type: Ready + status: "False" + reason: Invalid + message: Listener must not have TLS certificate refs set for TLS mode Passthrough +tlsRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-1 + port: 8080 + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "False" + reason: NoReadyListeners + message: There are no ready listeners for this parent ref +xdsIR: + envoy-gateway-gateway-1: {} +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:translator-tests + listeners: + - address: "" diff --git a/internal/gatewayapi/translator.go b/internal/gatewayapi/translator.go index edf0bd3dc5d..0d60adc35e9 100644 --- a/internal/gatewayapi/translator.go +++ b/internal/gatewayapi/translator.go @@ -1,3 +1,8 @@ +// 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 gatewayapi import ( @@ -19,6 +24,7 @@ import ( const ( KindGateway = "Gateway" KindHTTPRoute = "HTTPRoute" + KindTLSRoute = "TLSRoute" KindService = "Service" KindSecret = "Secret" @@ -45,6 +51,7 @@ type InfraIRMap map[string]*ir.Infra type Resources struct { Gateways []*v1beta1.Gateway HTTPRoutes []*v1beta1.HTTPRoute + TLSRoutes []*v1alpha2.TLSRoute ReferenceGrants []*v1alpha2.ReferenceGrant Namespaces []*v1.Namespace Services []*v1.Service @@ -84,17 +91,27 @@ func (r *Resources) GetSecret(namespace, name string) *v1.Secret { // Translator translates Gateway API resources to IRs and computes status // for Gateway API resources. type Translator struct { + // GatewayClassName is the name of the GatewayClass + // to process Gateways for. GatewayClassName v1beta1.ObjectName + + // ProxyImage is the optional proxy image to use in + // the Infra IR. If unspecified, the default proxy + // image will be used. + ProxyImage string } type TranslateResult struct { Gateways []*v1beta1.Gateway HTTPRoutes []*v1beta1.HTTPRoute + TLSRoutes []*v1alpha2.TLSRoute XdsIR XdsIRMap InfraIR InfraIRMap } -func newTranslateResult(gateways []*GatewayContext, httpRoutes []*HTTPRouteContext, xdsIR XdsIRMap, infraIR InfraIRMap) *TranslateResult { +func newTranslateResult(gateways []*GatewayContext, + httpRoutes []*HTTPRouteContext, tlsRoutes []*TLSRouteContext, + xdsIR XdsIRMap, infraIR InfraIRMap) *TranslateResult { translateResult := &TranslateResult{ XdsIR: xdsIR, InfraIR: infraIR, @@ -106,6 +123,9 @@ func newTranslateResult(gateways []*GatewayContext, httpRoutes []*HTTPRouteConte for _, httpRoute := range httpRoutes { translateResult.HTTPRoutes = append(translateResult.HTTPRoutes, httpRoute.HTTPRoute) } + for _, tlsRoute := range tlsRoutes { + translateResult.TLSRoutes = append(translateResult.TLSRoutes, tlsRoute.TLSRoute) + } return translateResult } @@ -123,10 +143,13 @@ func (t *Translator) Translate(resources *Resources) *TranslateResult { // Process all relevant HTTPRoutes. httpRoutes := t.ProcessHTTPRoutes(resources.HTTPRoutes, gateways, resources, xdsIR) + // Process all relevant TLSRoutes. + tlsRoutes := t.ProcessTLSRoutes(resources.TLSRoutes, gateways, resources, xdsIR) + // Sort xdsIR based on the Gateway API spec sortXdsIRMap(xdsIR) - return newTranslateResult(gateways, httpRoutes, xdsIR, infraIR) + return newTranslateResult(gateways, httpRoutes, tlsRoutes, xdsIR, infraIR) } func (t *Translator) GetRelevantGateways(gateways []*v1beta1.Gateway) []*GatewayContext { @@ -236,6 +259,10 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR XdsIRMap gwInfraIR := ir.NewInfra() gwInfraIR.Proxy.Name = irKey gwInfraIR.Proxy.GetProxyMetadata().Labels = GatewayOwnerLabels(gateway.Namespace, gateway.Name) + if len(t.ProxyImage) > 0 { + gwInfraIR.Proxy.Image = t.ProxyImage + } + // save the IR references in the map before the translation starts xdsIR[irKey] = gwXdsIR infraIR[irKey] = gwInfraIR @@ -246,6 +273,33 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR XdsIRMap for _, listener := range gateway.listeners { // Process protocol & supported kinds switch listener.Protocol { + case v1beta1.TLSProtocolType: + if listener.AllowedRoutes == nil || len(listener.AllowedRoutes.Kinds) == 0 { + listener.SetSupportedKinds(v1beta1.RouteGroupKind{Group: GroupPtr(v1beta1.GroupName), Kind: KindTLSRoute}) + } else { + for _, kind := range listener.AllowedRoutes.Kinds { + if kind.Group != nil && string(*kind.Group) != v1beta1.GroupName { + listener.SetCondition( + v1beta1.ListenerConditionResolvedRefs, + metav1.ConditionFalse, + v1beta1.ListenerReasonInvalidRouteKinds, + fmt.Sprintf("Group is not supported, group must be %s", v1beta1.GroupName), + ) + continue + } + + if kind.Kind != KindTLSRoute { + listener.SetCondition( + v1beta1.ListenerConditionResolvedRefs, + metav1.ConditionFalse, + v1beta1.ListenerReasonInvalidRouteKinds, + fmt.Sprintf("Kind is not supported, kind must be %s", KindTLSRoute), + ) + continue + } + listener.SetSupportedKinds(kind) + } + } case v1beta1.HTTPProtocolType, v1beta1.HTTPSProtocolType: if listener.AllowedRoutes == nil || len(listener.AllowedRoutes.Kinds) == 0 { listener.SetSupportedKinds(v1beta1.RouteGroupKind{Group: GroupPtr(v1beta1.GroupName), Kind: KindHTTPRoute}) @@ -258,6 +312,7 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR XdsIRMap v1beta1.ListenerReasonInvalidRouteKinds, fmt.Sprintf("Group is not supported, group must be %s", v1beta1.GroupName), ) + continue } if kind.Kind != KindHTTPRoute { @@ -267,7 +322,9 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR XdsIRMap v1beta1.ListenerReasonInvalidRouteKinds, fmt.Sprintf("Kind is not supported, kind must be %s", KindHTTPRoute), ) + continue } + listener.SetSupportedKinds(kind) } } default: @@ -390,7 +447,7 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR XdsIRMap listener.SetCondition( v1beta1.ListenerConditionResolvedRefs, metav1.ConditionFalse, - v1beta1.ListenerReasonInvalidCertificateRef, + v1beta1.ListenerReasonRefNotPermitted, fmt.Sprintf("Certificate ref to secret %s/%s not permitted by any ReferenceGrant", *certificateRef.Namespace, certificateRef.Name), ) break @@ -432,6 +489,36 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR XdsIRMap } listener.SetTLSSecret(secret) + case v1beta1.TLSProtocolType: + if listener.TLS == nil { + listener.SetCondition( + v1beta1.ListenerConditionReady, + metav1.ConditionFalse, + v1beta1.ListenerReasonInvalid, + fmt.Sprintf("Listener must have TLS set when protocol is %s.", listener.Protocol), + ) + break + } + + if listener.TLS.Mode != nil && *listener.TLS.Mode != v1beta1.TLSModePassthrough { + listener.SetCondition( + v1beta1.ListenerConditionReady, + metav1.ConditionFalse, + "UnsupportedTLSMode", + fmt.Sprintf("TLS %s mode is not supported, TLS mode must be Passthrough.", *listener.TLS.Mode), + ) + break + } + + if len(listener.TLS.CertificateRefs) > 0 { + listener.SetCondition( + v1beta1.ListenerConditionReady, + metav1.ConditionFalse, + v1beta1.ListenerReasonInvalid, + "Listener must not have TLS certificate refs set for TLS mode Passthrough", + ) + break + } } lConditions := listener.GetConditions() @@ -459,31 +546,39 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR XdsIRMap continue } + // Add the listener to the Xds IR. servicePort := int32(listener.Port) containerPort := servicePortToContainerPort(servicePort) - // Add the listener to the Xds IR. - irListener := &ir.HTTPListener{ - Name: irListenerName(listener), - Address: "0.0.0.0", - Port: uint32(containerPort), - TLS: irTLSConfig(listener.tlsSecret), - } - if listener.Hostname != nil { - irListener.Hostnames = append(irListener.Hostnames, string(*listener.Hostname)) - } else { - // Hostname specifies the virtual hostname to match for protocol types that define this concept. - // When unspecified, all hostnames are matched. This field is ignored for protocols that don’t require hostname based matching. - // see more https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.Listener. - irListener.Hostnames = append(irListener.Hostnames, "*") + switch listener.Protocol { + case v1beta1.HTTPProtocolType, v1beta1.HTTPSProtocolType: + irListener := &ir.HTTPListener{ + Name: irHTTPListenerName(listener), + Address: "0.0.0.0", + Port: uint32(containerPort), + TLS: irTLSConfig(listener.tlsSecret), + } + if listener.Hostname != nil { + irListener.Hostnames = append(irListener.Hostnames, string(*listener.Hostname)) + } else { + // Hostname specifies the virtual hostname to match for protocol types that define this concept. + // When unspecified, all hostnames are matched. This field is ignored for protocols that don’t require hostname based matching. + // see more https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.Listener. + irListener.Hostnames = append(irListener.Hostnames, "*") + } + gwXdsIR.HTTP = append(gwXdsIR.HTTP, irListener) } - gwXdsIR.HTTP = append(gwXdsIR.HTTP, irListener) // Add the listener to the Infra IR. Infra IR ports must have a unique port number. if !slices.Contains(foundPorts, servicePort) { foundPorts = append(foundPorts, servicePort) - proto := ir.HTTPProtocolType - if listener.Protocol == v1beta1.HTTPSProtocolType { + var proto ir.ProtocolType + switch listener.Protocol { + case v1beta1.HTTPProtocolType: + proto = ir.HTTPProtocolType + case v1beta1.HTTPSProtocolType: proto = ir.HTTPSProtocolType + case v1beta1.TLSProtocolType: + proto = ir.TLSProtocolType } infraPort := ir.ListenerPort{ Name: string(listener.Name), @@ -525,7 +620,7 @@ func buildRuleRouteDest(backendRef v1beta1.HTTPBackendRef, } if backendRef.Group != nil && *backendRef.Group != "" { - parentRef.SetCondition( + parentRef.SetCondition(httpRoute, v1beta1.RouteConditionResolvedRefs, metav1.ConditionFalse, v1beta1.RouteReasonInvalidKind, @@ -535,7 +630,7 @@ func buildRuleRouteDest(backendRef v1beta1.HTTPBackendRef, } if backendRef.Kind != nil && *backendRef.Kind != KindService { - parentRef.SetCondition( + parentRef.SetCondition(httpRoute, v1beta1.RouteConditionResolvedRefs, metav1.ConditionFalse, v1beta1.RouteReasonInvalidKind, @@ -559,7 +654,7 @@ func buildRuleRouteDest(backendRef v1beta1.HTTPBackendRef, }, resources.ReferenceGrants, ) { - parentRef.SetCondition( + parentRef.SetCondition(httpRoute, v1beta1.RouteConditionResolvedRefs, metav1.ConditionFalse, v1beta1.RouteReasonRefNotPermitted, @@ -570,7 +665,7 @@ func buildRuleRouteDest(backendRef v1beta1.HTTPBackendRef, } if backendRef.Port == nil { - parentRef.SetCondition( + parentRef.SetCondition(httpRoute, v1beta1.RouteConditionResolvedRefs, metav1.ConditionFalse, "PortNotSpecified", @@ -581,7 +676,7 @@ func buildRuleRouteDest(backendRef v1beta1.HTTPBackendRef, service := resources.GetService(NamespaceDerefOr(backendRef.Namespace, httpRoute.Namespace), string(backendRef.Name)) if service == nil { - parentRef.SetCondition( + parentRef.SetCondition(httpRoute, v1beta1.RouteConditionResolvedRefs, metav1.ConditionFalse, v1beta1.RouteReasonBackendNotFound, @@ -599,7 +694,7 @@ func buildRuleRouteDest(backendRef v1beta1.HTTPBackendRef, } if !portFound { - parentRef.SetCondition( + parentRef.SetCondition(httpRoute, v1beta1.RouteConditionResolvedRefs, metav1.ConditionFalse, "PortNotFound", @@ -628,42 +723,7 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways // Find out if this route attaches to one of our Gateway's listeners, // and if so, get the list of listeners that allow it to attach for each // parentRef. - var relevantRoute bool - for _, parentRef := range httpRoute.Spec.ParentRefs { - isRelevantParentRef, selectedListeners := GetReferencedListeners(parentRef, gateways) - - // Parent ref is not to a Gateway that we control: skip it - if !isRelevantParentRef { - continue - } - relevantRoute = true - - parentRefCtx := httpRoute.GetRouteParentContext(parentRef) - // Reset conditions since they will be recomputed during translation - parentRefCtx.ResetConditions() - - if !HasReadyListener(selectedListeners) { - parentRefCtx.SetCondition(v1beta1.RouteConditionAccepted, metav1.ConditionFalse, "NoReadyListeners", "There are no ready listeners for this parent ref") - continue - } - - var allowedListeners []*ListenerContext - for _, listener := range selectedListeners { - if listener.AllowsKind(v1beta1.RouteGroupKind{Group: GroupPtr(v1beta1.GroupName), Kind: KindHTTPRoute}) && listener.AllowsNamespace(resources.GetNamespace(httpRoute.Namespace)) { - allowedListeners = append(allowedListeners, listener) - } - } - - if len(allowedListeners) == 0 { - parentRefCtx.SetCondition(v1beta1.RouteConditionAccepted, metav1.ConditionFalse, v1beta1.RouteReasonNotAllowedByListeners, "No listeners included by this parent ref allowed this attachment.") - continue - } - - parentRefCtx.SetListeners(allowedListeners...) - - parentRefCtx.SetCondition(v1beta1.RouteConditionAccepted, metav1.ConditionTrue, v1beta1.RouteReasonAccepted, "Route is accepted") - } - + relevantRoute := processAllowedListenersForParentRefs(httpRoute, gateways, resources) if !relevantRoute { continue } @@ -672,7 +732,7 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways for _, parentRef := range httpRoute.parentRefs { // Skip parent refs that did not accept the route - if !parentRef.IsAccepted() { + if !parentRef.IsAccepted(httpRoute) { continue } @@ -700,8 +760,8 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways case v1beta1.HTTPRouteFilterRequestRedirect: // Can't have two redirects for the same route if redirectResponse != nil { - parentRef.SetCondition( - v1beta1.RouteConditionResolvedRefs, + parentRef.SetCondition(httpRoute, + v1beta1.RouteConditionAccepted, metav1.ConditionFalse, v1beta1.RouteReasonUnsupportedValue, "Cannot configure multiple requestRedirect filters for a single HTTPRouteRule", @@ -722,8 +782,8 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways redir.Scheme = redirect.Scheme } else { errMsg := fmt.Sprintf("Scheme: %s is unsupported, only 'https' and 'http' are supported", *redirect.Scheme) - parentRef.SetCondition( - v1beta1.RouteConditionResolvedRefs, + parentRef.SetCondition(httpRoute, + v1beta1.RouteConditionAccepted, metav1.ConditionFalse, v1beta1.RouteReasonUnsupportedValue, errMsg, @@ -734,8 +794,8 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways if redirect.Hostname != nil { if err := isValidHostname(string(*redirect.Hostname)); err != nil { - parentRef.SetCondition( - v1beta1.RouteConditionResolvedRefs, + parentRef.SetCondition(httpRoute, + v1beta1.RouteConditionAccepted, metav1.ConditionFalse, v1beta1.RouteReasonUnsupportedValue, err.Error(), @@ -763,8 +823,8 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways } default: errMsg := fmt.Sprintf("Redirect path type: %s is invalid, only \"ReplaceFullPath\" and \"ReplacePrefixMatch\" are supported", redirect.Path.Type) - parentRef.SetCondition( - v1beta1.RouteConditionResolvedRefs, + parentRef.SetCondition(httpRoute, + v1beta1.RouteConditionAccepted, metav1.ConditionFalse, v1beta1.RouteReasonUnsupportedValue, errMsg, @@ -780,8 +840,8 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways redir.StatusCode = &redirectCode } else { errMsg := fmt.Sprintf("Status code %d is invalid, only 302 and 301 are supported", redirectCode) - parentRef.SetCondition( - v1beta1.RouteConditionResolvedRefs, + parentRef.SetCondition(httpRoute, + v1beta1.RouteConditionAccepted, metav1.ConditionFalse, v1beta1.RouteReasonUnsupportedValue, errMsg, @@ -812,8 +872,8 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways for _, addHeader := range headersToAdd { emptyFilterConfig = false if addHeader.Name == "" { - parentRef.SetCondition( - v1beta1.RouteConditionResolvedRefs, + parentRef.SetCondition(httpRoute, + v1beta1.RouteConditionAccepted, metav1.ConditionFalse, v1beta1.RouteReasonUnsupportedValue, "RequestHeaderModifier Filter cannot add a header with an empty name", @@ -823,8 +883,8 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways } // Per Gateway API specification on HTTPHeaderName, : and / are invalid characters in header names if strings.Contains(string(addHeader.Name), "/") || strings.Contains(string(addHeader.Name), ":") { - parentRef.SetCondition( - v1beta1.RouteConditionResolvedRefs, + parentRef.SetCondition(httpRoute, + v1beta1.RouteConditionAccepted, metav1.ConditionFalse, v1beta1.RouteReasonUnsupportedValue, fmt.Sprintf("RequestHeaderModifier Filter cannot set headers with a '/' or ':' character in them. Header: %q", string(addHeader.Name)), @@ -842,12 +902,6 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways } if !canAddHeader { - parentRef.SetCondition( - v1beta1.RouteConditionResolvedRefs, - metav1.ConditionFalse, - v1beta1.RouteReasonUnsupportedValue, - fmt.Sprintf("RequestHeaderModifier Filter already configures request header: %s to be added, ignoring second entry", headerKey), - ) continue } @@ -869,8 +923,8 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways for _, setHeader := range headersToSet { if setHeader.Name == "" { - parentRef.SetCondition( - v1beta1.RouteConditionResolvedRefs, + parentRef.SetCondition(httpRoute, + v1beta1.RouteConditionAccepted, metav1.ConditionFalse, v1beta1.RouteReasonUnsupportedValue, "RequestHeaderModifier Filter cannot set a header with an empty name", @@ -879,8 +933,8 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways } // Per Gateway API specification on HTTPHeaderName, : and / are invalid characters in header names if strings.Contains(string(setHeader.Name), "/") || strings.Contains(string(setHeader.Name), ":") { - parentRef.SetCondition( - v1beta1.RouteConditionResolvedRefs, + parentRef.SetCondition(httpRoute, + v1beta1.RouteConditionAccepted, metav1.ConditionFalse, v1beta1.RouteReasonUnsupportedValue, fmt.Sprintf("RequestHeaderModifier Filter cannot set headers with a '/' or ':' character in them. Header: '%s'", string(setHeader.Name)), @@ -898,12 +952,6 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways } } if !canAddHeader { - parentRef.SetCondition( - v1beta1.RouteConditionResolvedRefs, - metav1.ConditionFalse, - v1beta1.RouteReasonUnsupportedValue, - fmt.Sprintf("RequestHeaderModifier Filter already configures request header: %s to be added/set, ignoring second entry", headerKey), - ) continue } newHeader := ir.AddHeader{ @@ -925,8 +973,8 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways } for _, removedHeader := range headersToRemove { if removedHeader == "" { - parentRef.SetCondition( - v1beta1.RouteConditionResolvedRefs, + parentRef.SetCondition(httpRoute, + v1beta1.RouteConditionAccepted, metav1.ConditionFalse, v1beta1.RouteReasonUnsupportedValue, "RequestHeaderModifier Filter cannot remove a header with an empty name", @@ -942,12 +990,6 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways } } if !canRemHeader { - parentRef.SetCondition( - v1beta1.RouteConditionResolvedRefs, - metav1.ConditionFalse, - v1beta1.RouteReasonUnsupportedValue, - fmt.Sprintf("RequestHeaderModifier Filter already configures request header: %s to be removed, ignoring second entry", removedHeader), - ) continue } @@ -958,19 +1000,32 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways // Update the status if the filter failed to configure any valid headers to add/remove if len(addRequestHeaders) == 0 && len(removeRequestHeaders) == 0 && !emptyFilterConfig { - parentRef.SetCondition( - v1beta1.RouteConditionResolvedRefs, + parentRef.SetCondition(httpRoute, + v1beta1.RouteConditionAccepted, metav1.ConditionFalse, v1beta1.RouteReasonUnsupportedValue, "RequestHeaderModifier Filter did not provide valid configuration to add/set/remove any headers", ) } - default: + case v1beta1.HTTPRouteFilterExtensionRef: // "If a reference to a custom filter type cannot be resolved, the filter MUST NOT be skipped. // Instead, requests that would have been processed by that filter MUST receive a HTTP error response." errMsg := fmt.Sprintf("Unknown custom filter type: %s", filter.Type) - parentRef.SetCondition( - v1beta1.RouteConditionResolvedRefs, + parentRef.SetCondition(httpRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonUnsupportedValue, + errMsg, + ) + directResponse = &ir.DirectResponse{ + Body: &errMsg, + StatusCode: 500, + } + default: + // Unsupported filters. + errMsg := fmt.Sprintf("Unsupported filter type: %s", filter.Type) + parentRef.SetCondition(httpRoute, + v1beta1.RouteConditionAccepted, metav1.ConditionFalse, v1beta1.RouteReasonUnsupportedValue, errMsg, @@ -1000,16 +1055,47 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways irRoute.PathMatch = &ir.StringMatch{ Exact: match.Path.Value, } + case v1beta1.PathMatchRegularExpression: + irRoute.PathMatch = &ir.StringMatch{ + SafeRegex: match.Path.Value, + } } } for _, headerMatch := range match.Headers { - if HeaderMatchTypeDerefOr(headerMatch.Type, v1beta1.HeaderMatchExact) == v1beta1.HeaderMatchExact { + switch HeaderMatchTypeDerefOr(headerMatch.Type, v1beta1.HeaderMatchExact) { + case v1beta1.HeaderMatchExact: irRoute.HeaderMatches = append(irRoute.HeaderMatches, &ir.StringMatch{ Name: string(headerMatch.Name), Exact: StringPtr(headerMatch.Value), }) + case v1beta1.HeaderMatchRegularExpression: + irRoute.HeaderMatches = append(irRoute.HeaderMatches, &ir.StringMatch{ + Name: string(headerMatch.Name), + SafeRegex: StringPtr(headerMatch.Value), + }) } } + for _, queryParamMatch := range match.QueryParams { + switch QueryParamMatchTypeDerefOr(queryParamMatch.Type, v1beta1.QueryParamMatchExact) { + case v1beta1.QueryParamMatchExact: + irRoute.QueryParamMatches = append(irRoute.QueryParamMatches, &ir.StringMatch{ + Name: queryParamMatch.Name, + Exact: StringPtr(queryParamMatch.Value), + }) + case v1beta1.QueryParamMatchRegularExpression: + irRoute.QueryParamMatches = append(irRoute.QueryParamMatches, &ir.StringMatch{ + Name: queryParamMatch.Name, + SafeRegex: StringPtr(queryParamMatch.Value), + }) + } + } + + if match.Method != nil { + irRoute.HeaderMatches = append(irRoute.HeaderMatches, &ir.StringMatch{ + Name: ":method", + Exact: StringPtr(string(*match.Method)), + }) + } // Add the redirect filter or direct response that were created earlier to all the irRoutes if redirectResponse != nil { @@ -1028,7 +1114,6 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways } for _, backendRef := range rule.BackendRefs { - destination, backendWeight := buildRuleRouteDest(backendRef, parentRef, httpRoute, resources) for _, route := range ruleRoutes { // If the route already has a direct response or redirect configured, then it was from a filter so skip @@ -1043,7 +1128,6 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways } } } - } // If the route has no valid backends then just use a direct response and don't fuss with weighted responses @@ -1064,7 +1148,7 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways var hasHostnameIntersection bool for _, listener := range parentRef.listeners { - hosts := ComputeHosts(httpRoute.Spec.Hostnames, listener.Hostname) + hosts := computeHosts(httpRoute.GetHostnames(), listener.Hostname) if len(hosts) == 0 { continue } @@ -1104,7 +1188,7 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways } irKey := irStringKey(listener.gateway) - irListener := xdsIR[irKey].GetListener(irListenerName(listener)) + irListener := xdsIR[irKey].GetHTTPListener(irHTTPListenerName(listener)) if irListener != nil { irListener.Routes = append(irListener.Routes, perHostRoutes...) } @@ -1117,14 +1201,18 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways } if !hasHostnameIntersection { - parentRef.SetCondition( + parentRef.SetCondition(httpRoute, v1beta1.RouteConditionAccepted, metav1.ConditionFalse, v1beta1.RouteReasonNoMatchingListenerHostname, "There were no hostname intersections between the HTTPRoute and this parent ref's Listener(s).", ) - } else { - parentRef.SetCondition( + } + + // If no negative conditions have been set, the route is considered "Accepted=True". + if parentRef.httpRoute != nil && + len(parentRef.httpRoute.Status.Parents[parentRef.routeParentStatusIdx].Conditions) == 0 { + parentRef.SetCondition(httpRoute, v1beta1.RouteConditionAccepted, metav1.ConditionTrue, v1beta1.RouteReasonAccepted, @@ -1137,6 +1225,262 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways return relevantHTTPRoutes } +func (t *Translator) ProcessTLSRoutes(tlsRoutes []*v1alpha2.TLSRoute, gateways []*GatewayContext, resources *Resources, xdsIR XdsIRMap) []*TLSRouteContext { + var relevantTLSRoutes []*TLSRouteContext + + for _, t := range tlsRoutes { + if t == nil { + panic("received nil tlsroute") + } + tlsRoute := &TLSRouteContext{TLSRoute: t} + + // Find out if this route attaches to one of our Gateway's listeners, + // and if so, get the list of listeners that allow it to attach for each + // parentRef. + relevantRoute := processAllowedListenersForParentRefs(tlsRoute, gateways, resources) + if !relevantRoute { + continue + } + + relevantTLSRoutes = append(relevantTLSRoutes, tlsRoute) + + for _, parentRef := range tlsRoute.parentRefs { + // Skip parent refs that did not accept the route + if !parentRef.IsAccepted(tlsRoute) { + continue + } + + // Need to compute Route rules within the parentRef loop because + // any conditions that come out of it have to go on each RouteParentStatus, + // not on the Route as a whole. + var routeDestinations []*ir.RouteDestination + + // compute backends + for _, rule := range tlsRoute.Spec.Rules { + for _, backendRef := range rule.BackendRefs { + if backendRef.Group != nil && *backendRef.Group != "" { + parentRef.SetCondition(tlsRoute, + v1beta1.RouteConditionResolvedRefs, + metav1.ConditionFalse, + v1beta1.RouteReasonInvalidKind, + "Group is invalid, only the core API group (specified by omitting the group field or setting it to an empty string) is supported", + ) + continue + } + + if backendRef.Kind != nil && *backendRef.Kind != KindService { + parentRef.SetCondition(tlsRoute, + v1beta1.RouteConditionResolvedRefs, + metav1.ConditionFalse, + v1beta1.RouteReasonInvalidKind, + "Kind is invalid, only Service is supported", + ) + continue + } + + if backendRef.Namespace != nil && string(*backendRef.Namespace) != "" && string(*backendRef.Namespace) != tlsRoute.Namespace { + if !isValidCrossNamespaceRef( + crossNamespaceFrom{ + group: v1beta1.GroupName, + kind: KindTLSRoute, + namespace: tlsRoute.Namespace, + }, + crossNamespaceTo{ + group: "", + kind: KindService, + namespace: string(*backendRef.Namespace), + name: string(backendRef.Name), + }, + resources.ReferenceGrants, + ) { + parentRef.SetCondition(tlsRoute, + v1beta1.RouteConditionResolvedRefs, + metav1.ConditionFalse, + v1beta1.RouteReasonRefNotPermitted, + fmt.Sprintf("Backend ref to service %s/%s not permitted by any ReferenceGrant", *backendRef.Namespace, backendRef.Name), + ) + continue + } + } + + if backendRef.Port == nil { + parentRef.SetCondition(tlsRoute, + v1beta1.RouteConditionResolvedRefs, + metav1.ConditionFalse, + "PortNotSpecified", + "A valid port number corresponding to a port on the Service must be specified", + ) + continue + } + + // TODO: [v1alpha2-v1beta1] Replace with NamespaceDerefOr when TLSRoute graduates to v1beta1. + serviceNamespace := NamespaceDerefOrAlpha(backendRef.Namespace, tlsRoute.Namespace) + service := resources.GetService(serviceNamespace, string(backendRef.Name)) + if service == nil { + parentRef.SetCondition(tlsRoute, + v1beta1.RouteConditionResolvedRefs, + metav1.ConditionFalse, + v1beta1.RouteReasonBackendNotFound, + fmt.Sprintf("Service %s/%s not found", serviceNamespace, string(backendRef.Name)), + ) + continue + } + + var portFound bool + for _, port := range service.Spec.Ports { + if port.Port == int32(*backendRef.Port) { + portFound = true + break + } + } + + if !portFound { + parentRef.SetCondition(tlsRoute, + v1beta1.RouteConditionResolvedRefs, + metav1.ConditionFalse, + "PortNotFound", + fmt.Sprintf("Port %d not found on service %s/%s", *backendRef.Port, serviceNamespace, string(backendRef.Name)), + ) + continue + } + + weight := uint32(1) + if backendRef.Weight != nil { + weight = uint32(*backendRef.Weight) + } + + routeDestinations = append(routeDestinations, &ir.RouteDestination{ + Host: service.Spec.ClusterIP, + Port: uint32(*backendRef.Port), + Weight: weight, + }) + } + + // TODO handle: + // - no valid backend refs + // - sum of weights for valid backend refs is 0 + // - returning 500's for invalid backend refs + // - etc. + } + + var hasHostnameIntersection bool + for _, listener := range parentRef.listeners { + hosts := computeHosts(tlsRoute.GetHostnames(), listener.Hostname) + if len(hosts) == 0 { + continue + } + + hasHostnameIntersection = true + + irKey := irStringKey(listener.gateway) + containerPort := servicePortToContainerPort(int32(listener.Port)) + // Create the TCP Listener while parsing the TLSRoute since + // the listener directly links to a routeDestination. + irListener := &ir.TCPListener{ + Name: irTCPListenerName(listener, tlsRoute), + Address: "0.0.0.0", + Port: uint32(containerPort), + TLS: &ir.TLSInspectorConfig{ + SNIs: hosts, + }, + Destinations: routeDestinations, + } + gwXdsIR := xdsIR[irKey] + gwXdsIR.TCP = append(gwXdsIR.TCP, irListener) + + // Theoretically there should only be one parent ref per + // Route that attaches to a given Listener, so fine to just increment here, but we + // might want to check to ensure we're not double-counting. + if len(routeDestinations) > 0 { + listener.IncrementAttachedRoutes() + } + } + + if !hasHostnameIntersection { + parentRef.SetCondition(tlsRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonNoMatchingListenerHostname, + "There were no hostname intersections between the HTTPRoute and this parent ref's Listener(s).", + ) + } + + // If no negative conditions have been set, the route is considered "Accepted=True". + if parentRef.tlsRoute != nil && + len(parentRef.tlsRoute.Status.Parents[parentRef.routeParentStatusIdx].Conditions) == 0 { + parentRef.SetCondition(tlsRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionTrue, + v1beta1.RouteReasonAccepted, + "Route is accepted", + ) + } + } + } + + return relevantTLSRoutes +} + +// processAllowedListenersForParentRefs finds out if the route attaches to one of our +// Gateways' listeners, and if so, gets the list of listeners that allow it to +// attach for each parentRef. +func processAllowedListenersForParentRefs(routeContext RouteContext, gateways []*GatewayContext, resources *Resources) bool { + var relevantRoute bool + + for _, parentRef := range routeContext.GetParentReferences() { + isRelevantParentRef, selectedListeners := GetReferencedListeners(parentRef, gateways) + + // Parent ref is not to a Gateway that we control: skip it + if !isRelevantParentRef { + continue + } + relevantRoute = true + + parentRefCtx := routeContext.GetRouteParentContext(parentRef) + // Reset conditions since they will be recomputed during translation + parentRefCtx.ResetConditions(routeContext) + + if !HasReadyListener(selectedListeners) { + parentRefCtx.SetCondition(routeContext, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + "NoReadyListeners", + "There are no ready listeners for this parent ref", + ) + continue + } + + var allowedListeners []*ListenerContext + for _, listener := range selectedListeners { + acceptedKind := routeContext.GetRouteType() + if listener.AllowsKind(v1beta1.RouteGroupKind{Group: GroupPtr(v1beta1.GroupName), Kind: v1beta1.Kind(acceptedKind)}) && + listener.AllowsNamespace(resources.GetNamespace(routeContext.GetNamespace())) { + allowedListeners = append(allowedListeners, listener) + } + } + + if len(allowedListeners) == 0 { + parentRefCtx.SetCondition(routeContext, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonNotAllowedByListeners, + "No listeners included by this parent ref allowed this attachment.", + ) + continue + } + + parentRefCtx.SetListeners(allowedListeners...) + + parentRefCtx.SetCondition(routeContext, + v1beta1.RouteConditionAccepted, + metav1.ConditionTrue, + v1beta1.RouteReasonAccepted, + "Route is accepted", + ) + } + return relevantRoute +} + type crossNamespaceFrom struct { group string kind string @@ -1193,7 +1537,6 @@ func isValidCrossNamespaceRef(from crossNamespaceFrom, to crossNamespaceTo, refe // Checks if a hostname is valid according to RFC 1123 and gateway API's requirement that it not be an IP address func isValidHostname(hostname string) error { - if errs := validation.IsDNS1123Subdomain(hostname); errs != nil { return fmt.Errorf("hostname %q is invalid for a redirect filter: %v", hostname, errs) } @@ -1225,12 +1568,16 @@ func irStringKey(gateway *v1beta1.Gateway) string { return fmt.Sprintf("%s-%s", gateway.Namespace, gateway.Name) } -func irListenerName(listener *ListenerContext) string { +func irHTTPListenerName(listener *ListenerContext) string { return fmt.Sprintf("%s-%s-%s", listener.gateway.Namespace, listener.gateway.Name, listener.Name) } -func routeName(httpRoute *HTTPRouteContext, ruleIdx, matchIdx int) string { - return fmt.Sprintf("%s-%s-rule-%d-match-%d", httpRoute.Namespace, httpRoute.Name, ruleIdx, matchIdx) +func irTCPListenerName(listener *ListenerContext, tlsRoute *TLSRouteContext) string { + return fmt.Sprintf("%s-%s-%s-%s", listener.gateway.Namespace, listener.gateway.Name, listener.Name, tlsRoute.Name) +} + +func routeName(route RouteContext, ruleIdx, matchIdx int) string { + return fmt.Sprintf("%s-%s-rule-%d-match-%d", route.GetNamespace(), route.GetName(), ruleIdx, matchIdx) } func irTLSConfig(tlsSecret *v1.Secret) *ir.TLSListenerConfig { diff --git a/internal/gatewayapi/translator_test.go b/internal/gatewayapi/translator_test.go index b62664e1e89..5615ab0b83a 100644 --- a/internal/gatewayapi/translator_test.go +++ b/internal/gatewayapi/translator_test.go @@ -1,3 +1,8 @@ +// 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 gatewayapi import ( @@ -43,6 +48,7 @@ func TestTranslate(t *testing.T) { translator := &Translator{ GatewayClassName: "envoy-gateway-class", + ProxyImage: "envoyproxy/envoy:translator-tests", } // Add common test fixtures diff --git a/internal/infrastructure/kubernetes/configmap.go b/internal/infrastructure/kubernetes/configmap.go index 0fe6e08553d..4e8dcfec97d 100644 --- a/internal/infrastructure/kubernetes/configmap.go +++ b/internal/infrastructure/kubernetes/configmap.go @@ -1,3 +1,8 @@ +// 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 kubernetes import ( @@ -13,6 +18,7 @@ import ( "github.com/envoyproxy/gateway/internal/envoygateway/config" "github.com/envoyproxy/gateway/internal/gatewayapi" "github.com/envoyproxy/gateway/internal/ir" + "github.com/envoyproxy/gateway/internal/provider/utils" ) const ( @@ -113,5 +119,6 @@ func (i *Infra) deleteConfigMap(ctx context.Context, infra *ir.Infra) error { } func expectedConfigMapName(proxyName string) string { - return fmt.Sprintf("%s-%s", config.EnvoyConfigMapPrefix, proxyName) + cMapName := utils.GetHashedName(proxyName) + return fmt.Sprintf("%s-%s", config.EnvoyPrefix, cMapName) } diff --git a/internal/infrastructure/kubernetes/configmap_test.go b/internal/infrastructure/kubernetes/configmap_test.go index 5051e81bbd4..3c7e4669123 100644 --- a/internal/infrastructure/kubernetes/configmap_test.go +++ b/internal/infrastructure/kubernetes/configmap_test.go @@ -1,3 +1,8 @@ +// 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 kubernetes import ( @@ -22,6 +27,7 @@ func TestExpectedConfigMap(t *testing.T) { cli := fakeclient.NewClientBuilder().WithScheme(envoygateway.GetScheme()).WithObjects().Build() kube := NewInfra(cli) infra := ir.NewInfra() + infra.Proxy.Name = "test" // An infra without Gateway owner labels should trigger @@ -35,7 +41,7 @@ func TestExpectedConfigMap(t *testing.T) { cm, err := kube.expectedConfigMap(infra) require.NoError(t, err) - require.Equal(t, "envoy-test", cm.Name) + require.Equal(t, "envoy-test-74657374", cm.Name) require.Equal(t, "envoy-gateway-system", cm.Namespace) require.Contains(t, cm.Data, sdsCAFilename) assert.Equal(t, sdsCAConfigMapData, cm.Data[sdsCAFilename]) @@ -65,7 +71,7 @@ func TestCreateOrUpdateConfigMap(t *testing.T) { expect: &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Namespace: config.EnvoyGatewayNamespace, - Name: "envoy-test", + Name: "envoy-test-74657374", Labels: map[string]string{ "app.gateway.envoyproxy.io/name": "envoy", gatewayapi.OwningGatewayNamespaceLabel: "default", @@ -92,7 +98,7 @@ func TestCreateOrUpdateConfigMap(t *testing.T) { expect: &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Namespace: config.EnvoyGatewayNamespace, - Name: "envoy-test", + Name: "envoy-test-74657374", Labels: map[string]string{ "app.gateway.envoyproxy.io/name": "envoy", gatewayapi.OwningGatewayNamespaceLabel: "default", diff --git a/internal/infrastructure/kubernetes/deployment.go b/internal/infrastructure/kubernetes/deployment.go index ef6796a87fc..f031a41a6dd 100644 --- a/internal/infrastructure/kubernetes/deployment.go +++ b/internal/infrastructure/kubernetes/deployment.go @@ -1,3 +1,8 @@ +// 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 kubernetes import ( @@ -18,6 +23,7 @@ import ( "github.com/envoyproxy/gateway/internal/envoygateway/config" "github.com/envoyproxy/gateway/internal/gatewayapi" "github.com/envoyproxy/gateway/internal/ir" + "github.com/envoyproxy/gateway/internal/provider/utils" xdsrunner "github.com/envoyproxy/gateway/internal/xds/server/runner" ) @@ -94,7 +100,8 @@ func (b *bootstrapConfig) render() error { } func expectedDeploymentName(proxyName string) string { - return fmt.Sprintf("%s-%s", config.EnvoyDeploymentPrefix, proxyName) + deploymentName := utils.GetHashedName(proxyName) + return fmt.Sprintf("%s-%s", config.EnvoyPrefix, deploymentName) } // expectedDeployment returns the expected Deployment based on the provided infra. diff --git a/internal/infrastructure/kubernetes/deployment_test.go b/internal/infrastructure/kubernetes/deployment_test.go index 4fd4a7e1728..764b3ce9b95 100644 --- a/internal/infrastructure/kubernetes/deployment_test.go +++ b/internal/infrastructure/kubernetes/deployment_test.go @@ -1,3 +1,8 @@ +// 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 kubernetes import ( diff --git a/internal/infrastructure/kubernetes/infra.go b/internal/infrastructure/kubernetes/infra.go index 402c7119ec6..b52094118b6 100644 --- a/internal/infrastructure/kubernetes/infra.go +++ b/internal/infrastructure/kubernetes/infra.go @@ -1,3 +1,8 @@ +// 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 kubernetes import ( @@ -28,8 +33,8 @@ func NewInfra(cli client.Client) *Infra { } } -// CreateInfra creates the managed kube infra, if it doesn't exist. -func (i *Infra) CreateInfra(ctx context.Context, infra *ir.Infra) error { +// CreateOrUpdateInfra creates the managed kube infra, if it doesn't exist. +func (i *Infra) CreateOrUpdateInfra(ctx context.Context, infra *ir.Infra) error { if infra == nil { return errors.New("infra ir is nil") } diff --git a/internal/infrastructure/kubernetes/infra_test.go b/internal/infrastructure/kubernetes/infra_test.go index a8a697a475f..26f037a816e 100644 --- a/internal/infrastructure/kubernetes/infra_test.go +++ b/internal/infrastructure/kubernetes/infra_test.go @@ -1,3 +1,8 @@ +// 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 kubernetes import ( @@ -60,8 +65,8 @@ func TestCreateInfra(t *testing.T) { Client: fakeclient.NewClientBuilder().WithScheme(envoygateway.GetScheme()).Build(), Namespace: "default", } - // Create the proxy infra. - err := kube.CreateInfra(context.Background(), tc.in) + // Create or update the proxy infra. + err := kube.CreateOrUpdateInfra(context.Background(), tc.in) if !tc.expect { require.Error(t, err) } else { diff --git a/internal/infrastructure/kubernetes/labels.go b/internal/infrastructure/kubernetes/labels.go index 3fe9edc2467..263bb9d97d2 100644 --- a/internal/infrastructure/kubernetes/labels.go +++ b/internal/infrastructure/kubernetes/labels.go @@ -1,3 +1,8 @@ +// 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 kubernetes import ( diff --git a/internal/infrastructure/kubernetes/labels_test.go b/internal/infrastructure/kubernetes/labels_test.go index 1ff33f8d23a..621efaa872b 100644 --- a/internal/infrastructure/kubernetes/labels_test.go +++ b/internal/infrastructure/kubernetes/labels_test.go @@ -1,3 +1,8 @@ +// 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 kubernetes import ( diff --git a/internal/infrastructure/kubernetes/service.go b/internal/infrastructure/kubernetes/service.go index 4a0f9e12b1b..8441aa978af 100644 --- a/internal/infrastructure/kubernetes/service.go +++ b/internal/infrastructure/kubernetes/service.go @@ -1,3 +1,8 @@ +// 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 kubernetes import ( @@ -14,10 +19,12 @@ import ( "github.com/envoyproxy/gateway/internal/envoygateway/config" "github.com/envoyproxy/gateway/internal/gatewayapi" "github.com/envoyproxy/gateway/internal/ir" + "github.com/envoyproxy/gateway/internal/provider/utils" ) func expectedServiceName(proxyName string) string { - return fmt.Sprintf("%s-%s", config.EnvoyServicePrefix, proxyName) + svcName := utils.GetHashedName(proxyName) + return fmt.Sprintf("%s-%s", config.EnvoyPrefix, svcName) } // expectedService returns the expected Service based on the provided infra. diff --git a/internal/infrastructure/kubernetes/service_test.go b/internal/infrastructure/kubernetes/service_test.go index 679ea8af440..f1ea3d1a2c0 100644 --- a/internal/infrastructure/kubernetes/service_test.go +++ b/internal/infrastructure/kubernetes/service_test.go @@ -1,3 +1,8 @@ +// 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 kubernetes import ( diff --git a/internal/infrastructure/kubernetes/serviceaccount.go b/internal/infrastructure/kubernetes/serviceaccount.go index 2a20ee742d8..97110c1d2ca 100644 --- a/internal/infrastructure/kubernetes/serviceaccount.go +++ b/internal/infrastructure/kubernetes/serviceaccount.go @@ -1,3 +1,8 @@ +// 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 kubernetes import ( @@ -9,16 +14,15 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "github.com/envoyproxy/gateway/internal/envoygateway/config" "github.com/envoyproxy/gateway/internal/gatewayapi" "github.com/envoyproxy/gateway/internal/ir" -) - -const ( - envoyServiceAccountPrefix = "envoy" + "github.com/envoyproxy/gateway/internal/provider/utils" ) func expectedServiceAccountName(proxyName string) string { - return fmt.Sprintf("%s-%s", envoyServiceAccountPrefix, proxyName) + svcActName := utils.GetHashedName(proxyName) + return fmt.Sprintf("%s-%s", config.EnvoyPrefix, svcActName) } // expectedServiceAccount returns the expected proxy serviceAccount. diff --git a/internal/infrastructure/kubernetes/serviceaccount_test.go b/internal/infrastructure/kubernetes/serviceaccount_test.go index 0eaddbcb3cb..c02719843ab 100644 --- a/internal/infrastructure/kubernetes/serviceaccount_test.go +++ b/internal/infrastructure/kubernetes/serviceaccount_test.go @@ -1,3 +1,8 @@ +// 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 kubernetes import ( @@ -73,7 +78,7 @@ func TestCreateOrUpdateServiceAccount(t *testing.T) { }, ObjectMeta: metav1.ObjectMeta{ Namespace: "test", - Name: "envoy-test", + Name: "envoy-test-74657374", Labels: map[string]string{ "app.gateway.envoyproxy.io/name": "envoy", gatewayapi.OwningGatewayNamespaceLabel: "default", @@ -118,7 +123,52 @@ func TestCreateOrUpdateServiceAccount(t *testing.T) { }, ObjectMeta: metav1.ObjectMeta{ Namespace: "test", - Name: "envoy-test", + Name: "envoy-test-74657374", + Labels: map[string]string{ + "app.gateway.envoyproxy.io/name": "envoy", + gatewayapi.OwningGatewayNamespaceLabel: "default", + gatewayapi.OwningGatewayNameLabel: "gateway-1", + }, + }, + }, + }, + { + name: "hashed-name", + ns: "test", + in: &ir.Infra{ + Proxy: &ir.ProxyInfra{ + Name: "very-long-name-that-will-be-hashed-and-cut-off-because-its-too-long", + Metadata: &ir.InfraMetadata{ + Labels: map[string]string{ + gatewayapi.OwningGatewayNamespaceLabel: "default", + gatewayapi.OwningGatewayNameLabel: "gateway-1", + }, + }, + }, + }, + current: &corev1.ServiceAccount{ + TypeMeta: metav1.TypeMeta{ + Kind: "ServiceAccount", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "very-long-name-that-will-be-hashed-and-cut-off-because-its-too-long", + Labels: map[string]string{ + "app.gateway.envoyproxy.io/name": "envoy", + gatewayapi.OwningGatewayNamespaceLabel: "default", + gatewayapi.OwningGatewayNameLabel: "gateway-1", + }, + }, + }, + want: &corev1.ServiceAccount{ + TypeMeta: metav1.TypeMeta{ + Kind: "ServiceAccount", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "envoy-very-long-name-that-will-be-hashed-and-cut-off-b-76657279", Labels: map[string]string{ "app.gateway.envoyproxy.io/name": "envoy", gatewayapi.OwningGatewayNamespaceLabel: "default", diff --git a/internal/infrastructure/manager.go b/internal/infrastructure/manager.go index 4b86068fffc..a263dc1566c 100644 --- a/internal/infrastructure/manager.go +++ b/internal/infrastructure/manager.go @@ -1,6 +1,12 @@ +// 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 infrastructure import ( + "context" "fmt" "sigs.k8s.io/controller-runtime/pkg/client" @@ -10,26 +16,30 @@ import ( "github.com/envoyproxy/gateway/internal/envoygateway" "github.com/envoyproxy/gateway/internal/envoygateway/config" "github.com/envoyproxy/gateway/internal/infrastructure/kubernetes" + "github.com/envoyproxy/gateway/internal/ir" ) +var _ Manager = (*kubernetes.Infra)(nil) + // Manager provides the scaffolding for managing infrastructure. -type Manager struct { - // TODO: create a common infra interface - *kubernetes.Infra +type Manager interface { + // CreateOrUpdateInfra creates or updates infra. + CreateOrUpdateInfra(ctx context.Context, infra *ir.Infra) error + // DeleteInfra deletes infra + DeleteInfra(ctx context.Context, infra *ir.Infra) error } // NewManager returns a new infrastructure Manager. -func NewManager(cfg *config.Server) (*Manager, error) { - mgr := new(Manager) - +func NewManager(cfg *config.Server) (Manager, error) { + var mgr Manager if cfg.EnvoyGateway.Provider.Type == v1alpha1.ProviderTypeKubernetes { cli, err := client.New(clicfg.GetConfigOrDie(), client.Options{Scheme: envoygateway.GetScheme()}) if err != nil { return nil, err } - mgr.Infra = kubernetes.NewInfra(cli) + mgr = kubernetes.NewInfra(cli) } else { - // Kube is the only supported provider type. + // Kube is the only supported provider type for now. return nil, fmt.Errorf("unsupported provider type %v", cfg.EnvoyGateway.Provider.Type) } diff --git a/internal/infrastructure/runner/runner.go b/internal/infrastructure/runner/runner.go index 3101212701e..cb9ec0d551c 100644 --- a/internal/infrastructure/runner/runner.go +++ b/internal/infrastructure/runner/runner.go @@ -1,3 +1,8 @@ +// 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 ( @@ -5,6 +10,7 @@ import ( "github.com/envoyproxy/gateway/internal/envoygateway/config" "github.com/envoyproxy/gateway/internal/infrastructure" + "github.com/envoyproxy/gateway/internal/ir" "github.com/envoyproxy/gateway/internal/message" ) @@ -15,7 +21,7 @@ type Config struct { type Runner struct { Config - mgr *infrastructure.Manager + mgr infrastructure.Manager } func (r *Runner) Name() string { @@ -41,9 +47,8 @@ func (r *Runner) Start(ctx context.Context) error { func (r *Runner) subscribeAndTranslate(ctx context.Context) { // Subscribe to resources - for snapshot := range r.InfraIR.Subscribe(ctx) { - r.Logger.Info("received a notification") - for _, update := range snapshot.Updates { + message.HandleSubscription(r.InfraIR.Subscribe(ctx), + func(update message.Update[string, *ir.Infra]) { val := update.Value if update.Delete { @@ -52,11 +57,11 @@ func (r *Runner) subscribeAndTranslate(ctx context.Context) { } } else { // Manage the proxy infra. - if err := r.mgr.CreateInfra(ctx, val); err != nil { + if err := r.mgr.CreateOrUpdateInfra(ctx, val); err != nil { r.Logger.Error(err, "failed to create new infra") } } - } - } + }, + ) r.Logger.Info("subscriber shutting down") } diff --git a/internal/ir/infra.go b/internal/ir/infra.go index 52e05ea7c39..488647be21b 100644 --- a/internal/ir/infra.go +++ b/internal/ir/infra.go @@ -1,3 +1,8 @@ +// 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 ir import ( @@ -11,7 +16,7 @@ import ( const ( DefaultProxyName = "default" - DefaultProxyImage = "envoyproxy/envoy:v1.23-latest" + DefaultProxyImage = "envoyproxy/envoy-dev:latest" ) // Infra defines managed infrastructure. @@ -31,7 +36,7 @@ type ProxyInfra struct { // Config defines user-facing configuration of the managed proxy infrastructure. Config *v1alpha1.EnvoyProxy // Image is the container image used for the managed proxy infrastructure. - // If unset, defaults to "envoyproxy/envoy:v1.23-latest". + // If unset, defaults to "envoyproxy/envoy-dev:latest". Image string // Listeners define the listeners exposed by the proxy infrastructure. Listeners []ProxyListener @@ -79,6 +84,9 @@ const ( // HTTPSProtocolType accepts HTTP/1.1 or HTTP/2 sessions over TLS. HTTPSProtocolType ProtocolType = "HTTPS" + + // Accepts TLS sessions over TCP. + TLSProtocolType ProtocolType = "TLS" ) // NewInfra returns a new Infra with default parameters. diff --git a/internal/ir/infra_test.go b/internal/ir/infra_test.go index 3a2f7f28b0a..72524ea37a4 100644 --- a/internal/ir/infra_test.go +++ b/internal/ir/infra_test.go @@ -1,3 +1,8 @@ +// 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 ir import ( diff --git a/internal/ir/xds.go b/internal/ir/xds.go index d03de20b285..7afee1b39d5 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -1,3 +1,8 @@ +// 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 ir import ( @@ -8,10 +13,11 @@ import ( ) var ( - ErrHTTPListenerNameEmpty = errors.New("field Name must be specified") - ErrHTTPListenerAddressInvalid = errors.New("field Address must be a valid IP address") - ErrHTTPListenerPortInvalid = errors.New("field Port specified is invalid") + ErrListenerNameEmpty = errors.New("field Name must be specified") + ErrListenerAddressInvalid = errors.New("field Address must be a valid IP address") + ErrListenerPortInvalid = errors.New("field Port specified is invalid") ErrHTTPListenerHostnamesEmpty = errors.New("field Hostnames must be specified with at least a single hostname entry") + ErrTCPListenesSNIsEmpty = errors.New("field SNIs must be specified with at least a single server name entry") ErrTLSServerCertEmpty = errors.New("field ServerCertificate must be specified") ErrTLSPrivateKey = errors.New("field PrivateKey must be specified") ErrHTTPRouteNameEmpty = errors.New("field Name must be specified") @@ -35,6 +41,10 @@ var ( type Xds struct { // HTTP listeners exposed by the gateway. HTTP []*HTTPListener + // TCP Listeners exposed by the gateway. + TCP []*TCPListener + // UDP Listeners exposed by the gateway. + UDP []*UDPListener } // Validate the fields within the Xds structure. @@ -45,9 +55,56 @@ func (x Xds) Validate() error { errs = multierror.Append(errs, err) } } + for _, tcp := range x.TCP { + if err := tcp.Validate(); err != nil { + errs = multierror.Append(errs, err) + } + } + for _, udp := range x.UDP { + if err := udp.Validate(); err != nil { + errs = multierror.Append(errs, err) + } + } return errs } +func (x Xds) GetHTTPListener(name string) *HTTPListener { + for _, listener := range x.HTTP { + if listener.Name == name { + return listener + } + } + return nil +} + +func (x Xds) GetTCPListener(name string) *TCPListener { + for _, listener := range x.TCP { + if listener.Name == name { + return listener + } + } + return nil +} + +func (x Xds) GetUDPListener(name string) *UDPListener { + for _, listener := range x.UDP { + if listener.Name == name { + return listener + } + } + return nil +} + +// Printable returns a deep copy of the resource that can be safely logged. +func (x Xds) Printable() *Xds { + out := x.DeepCopy() + for _, listener := range out.HTTP { + // Omit field + listener.TLS = nil + } + return out +} + // HTTPListener holds the listener configuration. // +k8s:deepcopy-gen=true type HTTPListener struct { @@ -66,28 +123,21 @@ type HTTPListener struct { TLS *TLSListenerConfig // Routes associated with HTTP traffic to the service. Routes []*HTTPRoute -} - -func (x Xds) GetListener(name string) *HTTPListener { - for _, listener := range x.HTTP { - if listener.Name == name { - return listener - } - } - return nil + // IsHTTP2 is set if the upstream client as well as the downstream server are configured to serve HTTP2 traffic. + IsHTTP2 bool } // Validate the fields within the HTTPListener structure func (h HTTPListener) Validate() error { var errs error if h.Name == "" { - errs = multierror.Append(errs, ErrHTTPListenerNameEmpty) + errs = multierror.Append(errs, ErrListenerNameEmpty) } if ip := net.ParseIP(h.Address); ip == nil { - errs = multierror.Append(errs, ErrHTTPListenerAddressInvalid) + errs = multierror.Append(errs, ErrListenerAddressInvalid) } if h.Port == 0 { - errs = multierror.Append(errs, ErrHTTPListenerPortInvalid) + errs = multierror.Append(errs, ErrListenerPortInvalid) } if len(h.Hostnames) == 0 { errs = multierror.Append(errs, ErrHTTPListenerHostnamesEmpty) @@ -234,6 +284,20 @@ type RouteDestination struct { Weight uint32 } +// Validate the fields within the RouteDestination structure +func (r RouteDestination) Validate() error { + var errs error + // Only support IP hosts for now + if ip := net.ParseIP(r.Host); ip == nil { + errs = multierror.Append(errs, ErrRouteDestinationHostInvalid) + } + if r.Port == 0 { + errs = multierror.Append(errs, ErrRouteDestinationPortInvalid) + } + + return errs +} + // Add header configures a headder to be added to a request. // +k8s:deepcopy-gen=true type AddHeader struct { @@ -336,20 +400,6 @@ func (r HTTPPathModifier) Validate() error { return errs } -// Validate the fields within the RouteDestination structure -func (r RouteDestination) Validate() error { - var errs error - // Only support IP hosts for now - if ip := net.ParseIP(r.Host); ip == nil { - errs = multierror.Append(errs, ErrRouteDestinationHostInvalid) - } - if r.Port == 0 { - errs = multierror.Append(errs, ErrRouteDestinationPortInvalid) - } - - return errs -} - // StringMatch holds the various match conditions. // Only one of Exact, Prefix or SafeRegex can be set. // +k8s:deepcopy-gen=true @@ -384,3 +434,96 @@ func (s StringMatch) Validate() error { return errs } + +// TCPListener holds the TCP listener configuration. +// +k8s:deepcopy-gen=true +type TCPListener struct { + // Name of the TCPListener + Name string + // Address that the listener should listen on. + Address string + // Port on which the service can be expected to be accessed by clients. + Port uint32 + // TLS information required for TLS Passthrough, If provided, incoming + // connections' server names are inspected and routed to backends accordingly. + TLS *TLSInspectorConfig + // Destinations associated with TCP traffic to the service. + Destinations []*RouteDestination +} + +// Validate the fields within the TCPListener structure +func (h TCPListener) Validate() error { + var errs error + if h.Name == "" { + errs = multierror.Append(errs, ErrListenerNameEmpty) + } + if ip := net.ParseIP(h.Address); ip == nil { + errs = multierror.Append(errs, ErrListenerAddressInvalid) + } + if h.Port == 0 { + errs = multierror.Append(errs, ErrListenerPortInvalid) + } + if h.TLS != nil { + if err := h.TLS.Validate(); err != nil { + errs = multierror.Append(errs, err) + } + } + for _, route := range h.Destinations { + if err := route.Validate(); err != nil { + errs = multierror.Append(errs, err) + } + } + return errs +} + +// TLSInspectorConfig holds the configuration required for inspecting TLS +// passthrough connections. +// +k8s:deepcopy-gen=true +type TLSInspectorConfig struct { + // Server names that are compared against the server names of a new connection. + // Wildcard hosts are supported in the prefix form. Partial wildcards are not + // supported, and values like *w.example.com are invalid. + // SNIs are used only in case of TLS Passthrough. + SNIs []string +} + +func (t TLSInspectorConfig) Validate() error { + var errs error + if len(t.SNIs) == 0 { + errs = multierror.Append(errs, ErrTCPListenesSNIsEmpty) + } + return errs +} + +// UDPListener holds the UDP listener configuration. +// +k8s:deepcopy-gen=true +type UDPListener struct { + // Name of the UDPListener + Name string + // Address that the listener should listen on. + Address string + // Port on which the service can be expected to be accessed by clients. + Port uint32 + // Destinations associated with UDP traffic to the service. + Destinations []*RouteDestination +} + +// Validate the fields within the UDPListener structure +func (h UDPListener) Validate() error { + var errs error + if h.Name == "" { + errs = multierror.Append(errs, ErrListenerNameEmpty) + } + if ip := net.ParseIP(h.Address); ip == nil { + errs = multierror.Append(errs, ErrListenerAddressInvalid) + } + if h.Port == 0 { + errs = multierror.Append(errs, ErrListenerPortInvalid) + } + for _, route := range h.Destinations { + if err := route.Validate(); err != nil { + errs = multierror.Append(errs, err) + } + } + return errs +} diff --git a/internal/ir/xds_test.go b/internal/ir/xds_test.go index da45b040cdb..cf06dab00dc 100644 --- a/internal/ir/xds_test.go +++ b/internal/ir/xds_test.go @@ -1,3 +1,8 @@ +// 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 ir import ( @@ -16,6 +21,17 @@ var ( Hostnames: []string{"example.com"}, Routes: []*HTTPRoute{&happyHTTPRoute}, } + happyHTTPSListener = HTTPListener{ + Name: "happy", + Address: "0.0.0.0", + Port: 80, + Hostnames: []string{"example.com"}, + TLS: &TLSListenerConfig{ + ServerCertificate: []byte{1, 2, 3}, + PrivateKey: []byte{1, 2, 3}, + }, + Routes: []*HTTPRoute{&happyHTTPRoute}, + } invalidAddrHTTPListener = HTTPListener{ Name: "invalid-addr", Address: "1.0.0", @@ -45,6 +61,59 @@ var ( Routes: []*HTTPRoute{&weightedInvalidBackendsHTTPRoute}, } + // TCPListener + happyTCPListenerTLSPassthrough = TCPListener{ + Name: "happy", + Address: "0.0.0.0", + Port: 80, + TLS: &TLSInspectorConfig{SNIs: []string{"example.com"}}, + Destinations: []*RouteDestination{&happyRouteDestination}, + } + invalidNameTCPListenerTLSPassthrough = TCPListener{ + Address: "0.0.0.0", + Port: 80, + TLS: &TLSInspectorConfig{SNIs: []string{"example.com"}}, + Destinations: []*RouteDestination{&happyRouteDestination}, + } + invalidAddrTCPListenerTLSPassthrough = TCPListener{ + Name: "invalid-addr", + Address: "1.0.0", + Port: 80, + TLS: &TLSInspectorConfig{SNIs: []string{"example.com"}}, + Destinations: []*RouteDestination{&happyRouteDestination}, + } + invalidSNITCPListenerTLSPassthrough = TCPListener{ + Address: "0.0.0.0", + Port: 80, + TLS: &TLSInspectorConfig{SNIs: []string{}}, + Destinations: []*RouteDestination{&happyRouteDestination}, + } + + // UDPListener + happyUDPListener = UDPListener{ + Name: "happy", + Address: "0.0.0.0", + Port: 80, + Destinations: []*RouteDestination{&happyRouteDestination}, + } + invalidNameUDPListener = UDPListener{ + Address: "0.0.0.0", + Port: 80, + Destinations: []*RouteDestination{&happyRouteDestination}, + } + invalidAddrUDPListener = UDPListener{ + Name: "invalid-addr", + Address: "1.0.0", + Port: 80, + Destinations: []*RouteDestination{&happyRouteDestination}, + } + invalidPortUDPListenerT = UDPListener{ + Name: "invalid-port", + Address: "0.0.0.0", + Port: 0, + Destinations: []*RouteDestination{&happyRouteDestination}, + } + // HTTPRoute happyHTTPRoute = HTTPRoute{ Name: "happy", @@ -244,12 +313,19 @@ func TestValidateXds(t *testing.T) { }, want: nil, }, + { + name: "happy tls", + input: Xds{ + TCP: []*TCPListener{&happyTCPListenerTLSPassthrough}, + }, + want: nil, + }, { name: "invalid listener", input: Xds{ HTTP: []*HTTPListener{&happyHTTPListener, &invalidAddrHTTPListener, &invalidRouteMatchHTTPListener}, }, - want: []error{ErrHTTPListenerAddressInvalid, ErrHTTPRouteMatchEmpty}, + want: []error{ErrListenerAddressInvalid, ErrHTTPRouteMatchEmpty}, }, { name: "invalid backend", @@ -300,12 +376,12 @@ func TestValidateHTTPListener(t *testing.T) { Hostnames: []string{"example.com"}, Routes: []*HTTPRoute{&happyHTTPRoute}, }, - want: []error{ErrHTTPListenerNameEmpty}, + want: []error{ErrListenerNameEmpty}, }, { name: "invalid addr", input: invalidAddrHTTPListener, - want: []error{ErrHTTPListenerAddressInvalid}, + want: []error{ErrListenerAddressInvalid}, }, { name: "invalid port and hostnames", @@ -314,7 +390,7 @@ func TestValidateHTTPListener(t *testing.T) { Address: "1.0.0", Routes: []*HTTPRoute{&happyHTTPRoute}, }, - want: []error{ErrHTTPListenerPortInvalid, ErrHTTPListenerHostnamesEmpty}, + want: []error{ErrListenerPortInvalid, ErrHTTPListenerHostnamesEmpty}, }, { name: "invalid route match", @@ -337,6 +413,48 @@ func TestValidateHTTPListener(t *testing.T) { } } +func TestValidateTCPListener(t *testing.T) { + tests := []struct { + name string + input TCPListener + want []error + }{ + { + name: "tls passthrough happy", + input: happyTCPListenerTLSPassthrough, + want: nil, + }, + { + name: "tls passthrough invalid name", + input: invalidNameTCPListenerTLSPassthrough, + want: []error{ErrListenerNameEmpty}, + }, + { + name: "tls passthrough invalid addr", + input: invalidAddrTCPListenerTLSPassthrough, + want: []error{ErrListenerAddressInvalid}, + }, + { + name: "tls passthrough empty SNIs", + input: invalidSNITCPListenerTLSPassthrough, + want: []error{ErrTCPListenesSNIsEmpty}, + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + if test.want == nil { + require.NoError(t, test.input.Validate()) + } else { + got := test.input.Validate() + for _, w := range test.want { + assert.ErrorContains(t, got, w.Error()) + } + } + }) + } +} + func TestValidateTLSListenerConfig(t *testing.T) { tests := []struct { name string @@ -376,7 +494,48 @@ func TestValidateTLSListenerConfig(t *testing.T) { } }) } +} +func TestValidateUDPListener(t *testing.T) { + tests := []struct { + name string + input UDPListener + want []error + }{ + { + name: "udp happy", + input: happyUDPListener, + want: nil, + }, + { + name: "udp invalid name", + input: invalidNameUDPListener, + want: []error{ErrListenerNameEmpty}, + }, + { + name: "udp invalid addr", + input: invalidAddrUDPListener, + want: []error{ErrListenerAddressInvalid}, + }, + { + name: "udp invalid port", + input: invalidPortUDPListenerT, + want: []error{ErrListenerPortInvalid}, + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + if test.want == nil { + require.NoError(t, test.input.Validate()) + } else { + got := test.input.Validate() + for _, w := range test.want { + assert.ErrorContains(t, got, w.Error()) + } + } + }) + } } func TestValidateHTTPRoute(t *testing.T) { @@ -562,3 +721,41 @@ func TestValidateStringMatch(t *testing.T) { }) } } + +func TestPrintable(t *testing.T) { + tests := []struct { + name string + input Xds + want *Xds + }{ + { + name: "empty", + input: Xds{}, + want: &Xds{}, + }, + { + name: "http", + input: Xds{ + HTTP: []*HTTPListener{&happyHTTPListener}, + }, + want: &Xds{ + HTTP: []*HTTPListener{&happyHTTPListener}, + }, + }, + { + name: "https", + input: Xds{ + HTTP: []*HTTPListener{&happyHTTPSListener}, + }, + want: &Xds{ + HTTP: []*HTTPListener{&happyHTTPListener}, + }, + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, *test.want, *test.input.Printable()) + }) + } +} diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index b765727d610..6458241fdc0 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -1,6 +1,11 @@ //go:build !ignore_autogenerated // +build !ignore_autogenerated +// 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. + // Code generated by controller-gen. DO NOT EDIT. package ir @@ -358,6 +363,57 @@ func (in *StringMatch) DeepCopy() *StringMatch { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TCPListener) DeepCopyInto(out *TCPListener) { + *out = *in + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLSInspectorConfig) + (*in).DeepCopyInto(*out) + } + if in.Destinations != nil { + in, out := &in.Destinations, &out.Destinations + *out = make([]*RouteDestination, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(RouteDestination) + **out = **in + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TCPListener. +func (in *TCPListener) DeepCopy() *TCPListener { + if in == nil { + return nil + } + out := new(TCPListener) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TLSInspectorConfig) DeepCopyInto(out *TLSInspectorConfig) { + *out = *in + if in.SNIs != nil { + in, out := &in.SNIs, &out.SNIs + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSInspectorConfig. +func (in *TLSInspectorConfig) DeepCopy() *TLSInspectorConfig { + if in == nil { + return nil + } + out := new(TLSInspectorConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLSListenerConfig) DeepCopyInto(out *TLSListenerConfig) { *out = *in @@ -383,6 +439,32 @@ func (in *TLSListenerConfig) DeepCopy() *TLSListenerConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UDPListener) DeepCopyInto(out *UDPListener) { + *out = *in + if in.Destinations != nil { + in, out := &in.Destinations, &out.Destinations + *out = make([]*RouteDestination, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(RouteDestination) + **out = **in + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UDPListener. +func (in *UDPListener) DeepCopy() *UDPListener { + if in == nil { + return nil + } + out := new(UDPListener) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Xds) DeepCopyInto(out *Xds) { *out = *in @@ -397,6 +479,28 @@ func (in *Xds) DeepCopyInto(out *Xds) { } } } + if in.TCP != nil { + in, out := &in.TCP, &out.TCP + *out = make([]*TCPListener, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(TCPListener) + (*in).DeepCopyInto(*out) + } + } + } + if in.UDP != nil { + in, out := &in.UDP, &out.UDP + *out = make([]*UDPListener, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(UDPListener) + (*in).DeepCopyInto(*out) + } + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Xds. diff --git a/internal/log/log.go b/internal/log/log.go index 54371fdb61b..cd51d3a80eb 100644 --- a/internal/log/log.go +++ b/internal/log/log.go @@ -1,3 +1,8 @@ +// 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 log import ( diff --git a/internal/message/types.go b/internal/message/types.go index 6d7ec1a64d5..e986d576261 100644 --- a/internal/message/types.go +++ b/internal/message/types.go @@ -1,9 +1,15 @@ +// 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 message import ( "github.com/telepresenceio/watchable" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" + gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" "github.com/envoyproxy/gateway/internal/ir" @@ -15,11 +21,16 @@ type ProviderResources struct { GatewayClasses watchable.Map[string, *gwapiv1b1.GatewayClass] Gateways watchable.Map[types.NamespacedName, *gwapiv1b1.Gateway] HTTPRoutes watchable.Map[types.NamespacedName, *gwapiv1b1.HTTPRoute] + TLSRoutes watchable.Map[types.NamespacedName, *gwapiv1a2.TLSRoute] Namespaces watchable.Map[string, *corev1.Namespace] Services watchable.Map[types.NamespacedName, *corev1.Service] + Secrets watchable.Map[types.NamespacedName, *corev1.Secret] + + ReferenceGrants watchable.Map[types.NamespacedName, *gwapiv1a2.ReferenceGrant] GatewayStatuses watchable.Map[types.NamespacedName, *gwapiv1b1.Gateway] HTTPRouteStatuses watchable.Map[types.NamespacedName, *gwapiv1b1.HTTPRoute] + TLSRouteStatuses watchable.Map[types.NamespacedName, *gwapiv1a2.TLSRoute] } func (p *ProviderResources) GetGatewayClasses() []*gwapiv1b1.GatewayClass { @@ -56,6 +67,17 @@ func (p *ProviderResources) GetHTTPRoutes() []*gwapiv1b1.HTTPRoute { return res } +func (p *ProviderResources) GetTLSRoutes() []*gwapiv1a2.TLSRoute { + if p.TLSRoutes.Len() == 0 { + return nil + } + res := make([]*gwapiv1a2.TLSRoute, 0, p.TLSRoutes.Len()) + for _, v := range p.TLSRoutes.LoadAll() { + res = append(res, v) + } + return res +} + func (p *ProviderResources) GetNamespaces() []*corev1.Namespace { if p.Namespaces.Len() == 0 { return nil @@ -79,6 +101,28 @@ func (p *ProviderResources) GetServices() []*corev1.Service { return res } +func (p *ProviderResources) GetSecrets() []*corev1.Secret { + if p.Secrets.Len() == 0 { + return nil + } + res := make([]*corev1.Secret, 0, p.Secrets.Len()) + for _, v := range p.Secrets.LoadAll() { + res = append(res, v) + } + return res +} + +func (p *ProviderResources) GetReferenceGrants() []*gwapiv1a2.ReferenceGrant { + if p.ReferenceGrants.Len() == 0 { + return nil + } + res := make([]*gwapiv1a2.ReferenceGrant, 0, p.ReferenceGrants.Len()) + for _, v := range p.ReferenceGrants.LoadAll() { + res = append(res, v) + } + return res +} + // XdsIR message type XdsIR struct { watchable.Map[string, *ir.Xds] diff --git a/internal/message/types_test.go b/internal/message/types_test.go index 36e16495471..9525efac84a 100644 --- a/internal/message/types_test.go +++ b/internal/message/types_test.go @@ -1,3 +1,8 @@ +// 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 message import ( @@ -7,6 +12,7 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" ) @@ -34,6 +40,12 @@ func TestProviderResources(t *testing.T) { Namespace: "test", }, } + t1 := &gwapiv1a2.TLSRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tlsroute1", + Namespace: "test", + }, + } s1 := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "service1", @@ -46,6 +58,7 @@ func TestProviderResources(t *testing.T) { assert.Nil(t, resources.GetGatewayClasses()) assert.Nil(t, resources.GetGateways()) assert.Nil(t, resources.GetHTTPRoutes()) + assert.Nil(t, resources.GetTLSRoutes()) assert.Nil(t, resources.GetServices()) // Add resources @@ -64,6 +77,12 @@ func TestProviderResources(t *testing.T) { } resources.HTTPRoutes.Store(r1Key, r1) + t1Key := types.NamespacedName{ + Namespace: t1.GetNamespace(), + Name: t1.GetName(), + } + resources.TLSRoutes.Store(t1Key, t1) + s1Key := types.NamespacedName{ Namespace: s1.GetNamespace(), Name: s1.GetName(), @@ -83,6 +102,9 @@ func TestProviderResources(t *testing.T) { hrs := resources.GetHTTPRoutes() assert.Equal(t, len(hrs), 1) + trs := resources.GetTLSRoutes() + assert.Equal(t, len(trs), 1) + svcs := resources.GetServices() assert.Equal(t, len(svcs), 1) @@ -110,6 +132,12 @@ func TestProviderResources(t *testing.T) { Namespace: "test", }, } + t2 := &gwapiv1a2.TLSRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tlsroute2", + Namespace: "test", + }, + } s2 := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "service2", @@ -131,6 +159,12 @@ func TestProviderResources(t *testing.T) { } resources.HTTPRoutes.Store(r2Key, r2) + t2Key := types.NamespacedName{ + Namespace: t2.GetNamespace(), + Name: t2.GetName(), + } + resources.TLSRoutes.Store(t2Key, t2) + s2Key := types.NamespacedName{ Namespace: s2.GetNamespace(), Name: s2.GetName(), @@ -150,6 +184,9 @@ func TestProviderResources(t *testing.T) { hrs = resources.GetHTTPRoutes() assert.ElementsMatch(t, hrs, []*gwapiv1b1.HTTPRoute{r1, r2}) + trs = resources.GetTLSRoutes() + assert.ElementsMatch(t, trs, []*gwapiv1a2.TLSRoute{t1, t2}) + svcs = resources.GetServices() assert.ElementsMatch(t, svcs, []*corev1.Service{s1, s2}) } diff --git a/internal/message/watchutil.go b/internal/message/watchutil.go new file mode 100644 index 00000000000..4fccb9059da --- /dev/null +++ b/internal/message/watchutil.go @@ -0,0 +1,39 @@ +// 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 message + +import ( + "github.com/telepresenceio/watchable" +) + +type Update[K comparable, V any] watchable.Update[K, V] + +// HandleSubscription takes a channel returned by +// watchable.Map.Subscribe() (or .SubscribeSubset()), and calls the +// given function for each initial value in the map, and for any +// updates. +// +// This is better than simply iterating over snapshot.Updates because +// it handles the case where the the watchable.Map already contains +// entries before .Subscribe is called. +func HandleSubscription[K comparable, V any]( + subscription <-chan watchable.Snapshot[K, V], + handle func(Update[K, V]), +) { + if snapshot, ok := <-subscription; ok { + for k, v := range snapshot.State { + handle(Update[K, V]{ + Key: k, + Value: v, + }) + } + } + for snapshot := range subscription { + for _, update := range snapshot.Updates { + handle(Update[K, V](update)) + } + } +} diff --git a/internal/message/watchutil_test.go b/internal/message/watchutil_test.go new file mode 100644 index 00000000000..873c8b2d53c --- /dev/null +++ b/internal/message/watchutil_test.go @@ -0,0 +1,61 @@ +// 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 message_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/telepresenceio/watchable" + + "github.com/envoyproxy/gateway/internal/message" +) + +func TestHandleSubscriptionAlreadyClosed(t *testing.T) { + ch := make(chan watchable.Snapshot[string, any]) + close(ch) + + var calls int + message.HandleSubscription[string, any]( + ch, + func(message.Update[string, any]) { calls++ }, + ) + assert.Equal(t, 0, calls) +} + +func TestHandleSubscriptionAlreadyInitialized(t *testing.T) { + var m watchable.Map[string, any] + m.Store("foo", "bar") + + endCtx, end := context.WithCancel(context.Background()) + go func() { + <-endCtx.Done() + m.Store("baz", "qux") + m.Delete("qux") // no-op + m.Store("foo", "bar") // no-op + m.Delete("baz") + time.Sleep(100 * time.Millisecond) + m.Close() + }() + + var storeCalls int + var deleteCalls int + message.HandleSubscription[string, any]( + m.Subscribe(context.Background()), + func(update message.Update[string, any]) { + end() + if update.Delete { + deleteCalls++ + } else { + storeCalls++ + } + }, + ) + assert.Equal(t, 2, storeCalls) + assert.Equal(t, 1, deleteCalls) +} diff --git a/internal/provider/kubernetes/config/envoy-gateway/deploy_and_ns.yaml b/internal/provider/kubernetes/config/envoy-gateway/deploy_and_ns.yaml index f16cdb1e8be..1b0b45c2482 100644 --- a/internal/provider/kubernetes/config/envoy-gateway/deploy_and_ns.yaml +++ b/internal/provider/kubernetes/config/envoy-gateway/deploy_and_ns.yaml @@ -42,6 +42,18 @@ spec: - name: certs mountPath: /certs readOnly: true + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 # TODO(user): Configure the resources accordingly based on the project requirements. # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ resources: diff --git a/internal/provider/kubernetes/config/envoy-gateway/envoy-gateway.yaml b/internal/provider/kubernetes/config/envoy-gateway/envoy-gateway.yaml index 128d8f89f8f..86be0afa6f2 100644 --- a/internal/provider/kubernetes/config/envoy-gateway/envoy-gateway.yaml +++ b/internal/provider/kubernetes/config/envoy-gateway/envoy-gateway.yaml @@ -2,5 +2,5 @@ apiVersion: config.gateway.envoyproxy.io/v1alpha1 kind: EnvoyGateway provider: type: Kubernetes - gateway: - controllerName: gateway.envoyproxy.io/gatewayclass-controller +gateway: + controllerName: gateway.envoyproxy.io/gatewayclass-controller diff --git a/internal/provider/kubernetes/config/rbac/role.yaml b/internal/provider/kubernetes/config/rbac/role.yaml index ba7e139e0df..66a48f7f1d4 100644 --- a/internal/provider/kubernetes/config/rbac/role.yaml +++ b/internal/provider/kubernetes/config/rbac/role.yaml @@ -31,6 +31,7 @@ rules: - httproutes - referencegrants - referencepolicies + - tlsroutes verbs: - get - list @@ -42,5 +43,6 @@ rules: - gatewayclasses/status - gateways/status - httproutes/status + - tlsroutes/status verbs: - update diff --git a/internal/provider/kubernetes/gateway.go b/internal/provider/kubernetes/gateway.go index c6e3a01b9df..f7f958da0a8 100644 --- a/internal/provider/kubernetes/gateway.go +++ b/internal/provider/kubernetes/gateway.go @@ -1,5 +1,14 @@ -// Portions of this code are based on code from Contour, available at: -// https://github.com/projectcontour/contour/blob/main/internal/controller/gateway.go +// 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. + +// This file contains code derived from Contour, +// https://github.com/projectcontour/contour +// from the source file +// https://github.com/projectcontour/contour/blob/main/internal/controller/gateway.go// and is provided here subject to the following: +// Copyright Project Contour Authors +// SPDX-License-Identifier: Apache-2.0 package kubernetes @@ -20,6 +29,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" + gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" "github.com/envoyproxy/gateway/internal/envoygateway/config" @@ -80,6 +90,16 @@ func newGatewayController(mgr manager.Manager, cfg *config.Server, su status.Upd if err := c.Watch(&source.Kind{Type: &appsv1.Deployment{}}, r.enqueueRequestForOwningGateway()); err != nil { return err } + // Trigger gateway reconciliation when a Secret that is referenced + // by a managed Gateway has changed. + if err := c.Watch(&source.Kind{Type: &corev1.Secret{}}, r.enqueueRequestForGatewaySecrets()); err != nil { + return err + } + // Trigger gateway reconciliation when a ReferenceGrant that refers + // to a managed Gateway has changed. + if err := c.Watch(&source.Kind{Type: &gwapiv1a2.ReferenceGrant{}}, r.enqueueRequestForReferencedGateway()); err != nil { + return err + } return nil } @@ -136,6 +156,106 @@ func (r *gatewayReconciler) enqueueRequestForOwningGateway() handler.EventHandle }) } +// enqueueRequestForGatewaySecrets returns an event handler that maps events for +// Secrets referenced by managed Gateways to reconcile requests for those Gateway objects. +func (r *gatewayReconciler) enqueueRequestForGatewaySecrets() handler.EventHandler { + return handler.EnqueueRequestsFromMapFunc(func(a client.Object) []reconcile.Request { + secret, ok := a.(*corev1.Secret) + if !ok { + r.log.Info("bypassing reconciliation due to unexpected object type", "type", a) + return nil + } + + ctx := context.Background() + var gateways gwapiv1b1.GatewayList + if err := r.client.List(ctx, &gateways); err != nil { + return nil + } + + var reqs []reconcile.Request + for i := range gateways.Items { + gw := gateways.Items[i] + if r.hasMatchingController(&gw) { + for j := range gw.Spec.Listeners { + if terminatesTLS(&gw.Spec.Listeners[j]) { + secrets, _, err := r.secretsAndRefGrantsForGateway(ctx, &gw) + if err != nil { + return nil + } + for _, s := range secrets { + if s.Namespace == secret.Namespace && s.Name == secret.Name { + req := reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: gw.Namespace, + Name: gw.Name, + }, + } + reqs = append(reqs, req) + } + } + } + } + } + } + + return reqs + }) +} + +// enqueueRequestForReferencedGateway returns an event handler that maps events for +// resources that reference a managed Gateway to reconcile requests for those Gateway objects. +// Note: A ReferenceGrant is the only supported object type. +func (r *gatewayReconciler) enqueueRequestForReferencedGateway() handler.EventHandler { + return handler.EnqueueRequestsFromMapFunc(func(a client.Object) []reconcile.Request { + rg, ok := a.(*gwapiv1a2.ReferenceGrant) + if !ok { + r.log.Info("bypassing reconciliation due to unexpected object type", "type", a) + return nil + } + + var refs []types.NamespacedName + for _, to := range rg.Spec.To { + if to.Group == gwapiv1a2.GroupName && + to.Kind == gatewayapi.KindGateway && + to.Name != nil { + ref := types.NamespacedName{Namespace: rg.Namespace, Name: string(*to.Name)} + refs = append(refs, ref) + } + } + for _, from := range rg.Spec.From { + if from.Group == gwapiv1a2.GroupName && + from.Kind == gatewayapi.KindGateway { + ref := types.NamespacedName{Namespace: string(from.Namespace), Name: rg.Name} + refs = append(refs, ref) + } + } + + ctx := context.Background() + var gateways gwapiv1b1.GatewayList + if err := r.client.List(ctx, &gateways); err != nil { + return nil + } + + var reqs []reconcile.Request + for i := range gateways.Items { + gw := gateways.Items[i] + for _, ref := range refs { + if gw.Namespace == ref.Namespace && gw.Name == ref.Name && r.hasMatchingController(&gw) { + req := reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: gw.Namespace, + Name: gw.Name, + }, + } + reqs = append(reqs, req) + } + } + } + + return reqs + }) +} + // Reconcile finds all the Gateways for the GatewayClass with an "Accepted: true" condition // and passes all Gateways for the configured GatewayClass to the IR for processing. func (r *gatewayReconciler) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { @@ -197,22 +317,103 @@ func (r *gatewayReconciler) Reconcile(ctx context.Context, request reconcile.Req "namespace", gw.Namespace, "name", gw.Name) } + // Get the secret and referenceGrants of the Gateway's TLS configuration. + secrets, refGrants, err := r.secretsAndRefGrantsForGateway(ctx, &gw) + if err != nil { + r.log.Info("failed to get secrets and referencegrants for gateway", + "namespace", gw.Namespace, "name", gw.Name) + } + for i := range secrets { + secret := secrets[i] + // Store the secrets in the resource map. + key := utils.NamespacedName(&secret) + r.resources.Secrets.Store(key, &secret) + } + for i := range refGrants { + rg := refGrants[i] + // Store the referencegrants in the resource map. + key := utils.NamespacedName(&rg) + r.resources.ReferenceGrants.Store(key, &rg) + // Store the referencegrant namespace in the resource map. + key = types.NamespacedName{Name: rg.Namespace} + refNs := corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: rg.Namespace, + }, + } + if err := r.client.Get(ctx, key, &refNs); err != nil { + r.log.Info("failed to get referencegrant namespace", "name", refNs.Name) + return reconcile.Result{}, nil + } + r.resources.Namespaces.Store(refNs.Name, &refNs) + } + // update scheduled condition status.UpdateGatewayStatusScheduledCondition(&gw, true) // update address field and ready condition status.UpdateGatewayStatusReadyCondition(&gw, svc, deployment) - // publish status - key := utils.NamespacedName(&gw) - r.resources.GatewayStatuses.Store(key, &gw) - r.resources.Gateways.Store(key, &gw) + key := utils.NamespacedName(&gw) + // publish status + // do it inline since this code flow updates the + // Status.Addresses field whereas the message bus / subscriber + // does not. + r.statusUpdater.Send(status.Update{ + NamespacedName: key, + Resource: new(gwapiv1b1.Gateway), + Mutator: status.MutatorFunc(func(obj client.Object) client.Object { + g, ok := obj.(*gwapiv1b1.Gateway) + if !ok { + panic(fmt.Sprintf("unsupported object type %T", obj)) + } + gCopy := g.DeepCopy() + gCopy.Status.Conditions = status.MergeConditions(gCopy.Status.Conditions, gw.Status.Conditions...) + gCopy.Status.Addresses = gw.Status.Addresses + return gCopy + + }), + }) + + // only store the resource if it does not exist or it has a newer spec. + if v, ok := r.resources.Gateways.Load(key); !ok || (gw.Generation > v.Generation) { + r.resources.Gateways.Store(key, &gw) + } if key == request.NamespacedName { found = true } } if !found { + gw, ok := r.resources.Gateways.Load(request.NamespacedName) + if !ok { + r.log.Info("failed to find accepted gateway in the watchable map", "namespace", request.Namespace, "name", request.Name) + return reconcile.Result{}, nil + } + r.resources.Gateways.Delete(request.NamespacedName) + // Delete the TLS secrets from the resource map if no other managed + // Gateways reference them. + secrets, _, err := r.secretsAndRefGrantsForGateway(ctx, gw) + if err != nil { + r.log.Info("failed to get secrets and referencegrants for gateway", + "namespace", gw.Namespace, "name", gw.Name) + } + for i := range secrets { + secret := secrets[i] + referenced, err := r.gatewaysRefSecret(ctx, &secret) + switch { + case err != nil: + r.log.Error(err, "failed to verify if other gateways reference secret") + case !referenced: + r.log.Info("no other gateways reference secret; deleting from resource map", + "namespace", secret.Namespace, "name", secret.Name) + key := utils.NamespacedName(&secret) + r.resources.Secrets.Delete(key) + default: + r.log.Info("other gateways reference secret; keeping the secret in the resource map", + "namespace", secret.Namespace, "name", secret.Name) + } + } } r.log.WithName(request.Namespace).WithName(request.Name).Info("reconciled gateway") @@ -280,6 +481,124 @@ func (r *gatewayReconciler) envoyServiceForGateway(ctx context.Context, gateway return svc, nil } +// gatewaysRefSecret returns true if a managed Gateway references the provided secret. +// An error is returned if an error is encountered while checking. +func (r *gatewayReconciler) gatewaysRefSecret(ctx context.Context, secret *corev1.Secret) (bool, error) { + if secret == nil { + return false, fmt.Errorf("secret is nil") + } + gateways := &gwapiv1b1.GatewayList{} + if err := r.client.List(ctx, gateways); err != nil { + return false, fmt.Errorf("error listing gatewayclasses: %v", err) + } + for i := range gateways.Items { + gw := gateways.Items[i] + if r.hasMatchingController(&gw) { + secrets, _, err := r.secretsAndRefGrantsForGateway(ctx, &gw) + if err != nil { + return false, err + } + for _, s := range secrets { + if s.Namespace == secret.Namespace && s.Name == secret.Name { + return true, nil + } + } + } + } + + return false, nil +} + +// secretsAndRefGrantsForGateway returns the Secrets referenced by the provided gateway listeners. +// If the provided Gateway references a Secret in a different namespace, a list of +// ReferenceGrants is returned that permit the cross namespace Secret reference. +func (r *gatewayReconciler) secretsAndRefGrantsForGateway(ctx context.Context, gateway *gwapiv1b1.Gateway) ([]corev1.Secret, []gwapiv1a2.ReferenceGrant, error) { + var secrets []corev1.Secret + var returnedGrants []gwapiv1a2.ReferenceGrant + for i := range gateway.Spec.Listeners { + listener := gateway.Spec.Listeners[i] + if terminatesTLS(&listener) { + for j := range listener.TLS.CertificateRefs { + ref := listener.TLS.CertificateRefs[j] + if refsSecret(&ref) { + if ref.Namespace != nil { + // A ReferenceGrant is required for cross namespace secret references. + refGrants := &gwapiv1a2.ReferenceGrantList{} + opts := client.ListOptions{Namespace: string(*ref.Namespace)} + if err := r.client.List(ctx, refGrants, &opts); err != nil { + return nil, nil, fmt.Errorf("error listing referencegrants") + } + var gwRefd, secretRefd bool + for _, rg := range refGrants.Items { + for _, from := range rg.Spec.From { + if from.Group == gwapiv1a2.GroupName && + from.Kind == gatewayapi.KindGateway { + gwRefd = true + break + } + } + for _, to := range rg.Spec.To { + if to.Group == corev1.GroupName && + to.Kind == gatewayapi.KindSecret { + if to.Name == nil || *to.Name == gwapiv1a2.ObjectName(ref.Name) { + secretRefd = true + break + } + } + } + if gwRefd && secretRefd { + returnedGrants = append(returnedGrants, rg) + key := types.NamespacedName{ + Namespace: string(*ref.Namespace), + Name: string(ref.Name), + } + secret := new(corev1.Secret) + if err := r.client.Get(ctx, key, secret); err != nil { + r.resources.Secrets.Delete(key) + return nil, nil, fmt.Errorf("failed to get secret: %v", err) + } + secrets = append(secrets, *secret) + } + } + } else { + // The secret is in the Gateway's namespace. + key := types.NamespacedName{ + Namespace: gateway.Namespace, + Name: string(ref.Name), + } + secret := new(corev1.Secret) + if err := r.client.Get(ctx, key, secret); err != nil { + r.resources.Secrets.Delete(key) + return nil, nil, fmt.Errorf("failed to get secret: %v", err) + } + secrets = append(secrets, *secret) + } + } + } + } + } + + return secrets, returnedGrants, nil +} + +// terminatesTLS returns true if the provided gateway contains a listener configured +// for TLS termination. +func terminatesTLS(listener *gwapiv1b1.Listener) bool { + if listener.TLS != nil && + listener.Protocol == gwapiv1b1.HTTPSProtocolType && + listener.TLS.Mode != nil && + *listener.TLS.Mode == gwapiv1b1.TLSModeTerminate { + return true + } + return false +} + +// refsSecret returns true if ref refers to a Secret. +func refsSecret(ref *gwapiv1b1.SecretObjectReference) bool { + return (ref.Group == nil || *ref.Group == corev1.GroupName) && + (ref.Kind == nil || *ref.Kind == gatewayapi.KindSecret) +} + // addFinalizer adds the gatewayclass finalizer to the provided gc, if it doesn't exist. func (r *gatewayReconciler) addFinalizer(ctx context.Context, gc *gwapiv1b1.GatewayClass) error { if !slice.ContainsString(gc.Finalizers, gatewayClassFinalizer) { @@ -324,13 +643,11 @@ func (r *gatewayReconciler) envoyDeploymentForGateway(ctx context.Context, gatew // Kubernetes API Server func (r *gatewayReconciler) subscribeAndUpdateStatus(ctx context.Context) { // Subscribe to resources - for snapshot := range r.resources.GatewayStatuses.Subscribe(ctx) { - r.log.Info("received a status notification") - updates := snapshot.Updates - for _, update := range updates { + message.HandleSubscription(r.resources.GatewayStatuses.Subscribe(ctx), + func(update message.Update[types.NamespacedName, *gwapiv1b1.Gateway]) { // skip delete updates. if update.Delete { - continue + return } key := update.Key val := update.Value @@ -343,21 +660,21 @@ func (r *gatewayReconciler) subscribeAndUpdateStatus(ctx context.Context) { panic(fmt.Sprintf("unsupported object type %T", obj)) } gCopy := g.DeepCopy() - gCopy.Status.Conditions = status.MergeConditions(gCopy.Status.Conditions, val.Status.Conditions...) - gCopy.Status.Addresses = val.Status.Addresses gCopy.Status.Listeners = val.Status.Listeners return gCopy }), }) - } - } + }, + ) r.log.Info("status subscriber shutting down") } func infraServiceName(gateway *gwapiv1b1.Gateway) string { - return fmt.Sprintf("%s-%s-%s", config.EnvoyServicePrefix, gateway.Namespace, gateway.Name) + infraName := utils.GetHashedName(fmt.Sprintf("%s-%s", gateway.Namespace, gateway.Name)) + return fmt.Sprintf("%s-%s", config.EnvoyPrefix, infraName) } func infraDeploymentName(gateway *gwapiv1b1.Gateway) string { - return fmt.Sprintf("%s-%s-%s", config.EnvoyDeploymentPrefix, gateway.Namespace, gateway.Name) + infraName := utils.GetHashedName(fmt.Sprintf("%s-%s", gateway.Namespace, gateway.Name)) + return fmt.Sprintf("%s-%s", config.EnvoyPrefix, infraName) } diff --git a/internal/provider/kubernetes/gateway_test.go b/internal/provider/kubernetes/gateway_test.go index 4138364e807..7d7e1fe2530 100644 --- a/internal/provider/kubernetes/gateway_test.go +++ b/internal/provider/kubernetes/gateway_test.go @@ -1,3 +1,8 @@ +// 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 kubernetes import ( @@ -6,14 +11,17 @@ import ( "testing" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" + gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" "github.com/envoyproxy/gateway/api/config/v1alpha1" "github.com/envoyproxy/gateway/internal/envoygateway" + "github.com/envoyproxy/gateway/internal/gatewayapi" "github.com/envoyproxy/gateway/internal/log" ) @@ -96,6 +104,23 @@ func TestGatewayHasMatchingController(t *testing.T) { }, expect: false, }, + { + name: "matching but very long name", + obj: &gwapiv1b1.Gateway{ + TypeMeta: metav1.TypeMeta{ + Kind: "Gateway", + APIVersion: fmt.Sprintf("%s/%s", gwapiv1b1.GroupName, gwapiv1b1.GroupVersion.Version), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "superdupermegalongnamethatisridiculouslylongandwaylongerthanitshouldeverbeinsideofkubernetes", + Namespace: "test", + }, + Spec: gwapiv1b1.GatewaySpec{ + GatewayClassName: gwapiv1b1.ObjectName(match.Name), + }, + }, + expect: true, + }, } // Create the reconciler. @@ -419,3 +444,422 @@ func TestRemoveFinalizer(t *testing.T) { }) } } + +func TestSecretsAndRefGrantsForGateway(t *testing.T) { + testCases := []struct { + name string + gw *gwapiv1b1.Gateway + secrets []corev1.Secret + refGrants []gwapiv1a2.ReferenceGrant + }{ + { + name: "gateway with no https listeners", + gw: &gwapiv1b1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-gw", + Namespace: "test-ns", + }, + Spec: gwapiv1b1.GatewaySpec{ + GatewayClassName: "test-gc", + Listeners: []gwapiv1b1.Listener{ + { + Name: "http", + Port: gwapiv1b1.PortNumber(int32(80)), + Protocol: gwapiv1b1.HTTPProtocolType, + }, + }, + }, + }, + }, + { + name: "gateway with one https listener and one same namespace secret", + gw: &gwapiv1b1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-gw", + Namespace: "test-ns", + }, + Spec: gwapiv1b1.GatewaySpec{ + GatewayClassName: "test-gc", + Listeners: []gwapiv1b1.Listener{ + { + Name: "tls", + Port: gwapiv1b1.PortNumber(int32(443)), + Protocol: gwapiv1b1.HTTPSProtocolType, + TLS: &gwapiv1b1.GatewayTLSConfig{ + Mode: gatewayapi.TLSModeTypePtr(gwapiv1b1.TLSModeTerminate), + CertificateRefs: []gwapiv1b1.SecretObjectReference{ + { + Name: gwapiv1b1.ObjectName("test-secret"), + }, + }, + }, + }, + }, + }, + }, + secrets: []corev1.Secret{ + { + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + Namespace: "test-ns", + ResourceVersion: "1", + }, + }, + }, + }, + { + name: "gateway with one http and one https listener with one same namespace secret", + gw: &gwapiv1b1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-gw", + Namespace: "test-ns", + }, + Spec: gwapiv1b1.GatewaySpec{ + GatewayClassName: "test-gc", + Listeners: []gwapiv1b1.Listener{ + { + Name: "http", + Port: gwapiv1b1.PortNumber(int32(80)), + Protocol: gwapiv1b1.HTTPProtocolType, + }, + { + Name: "tls", + Port: gwapiv1b1.PortNumber(int32(443)), + Protocol: gwapiv1b1.HTTPSProtocolType, + TLS: &gwapiv1b1.GatewayTLSConfig{ + Mode: gatewayapi.TLSModeTypePtr(gwapiv1b1.TLSModeTerminate), + CertificateRefs: []gwapiv1b1.SecretObjectReference{ + { + Name: gwapiv1b1.ObjectName("test-secret"), + }, + }, + }, + }, + }, + }, + }, + secrets: []corev1.Secret{ + { + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + Namespace: "test-ns", + ResourceVersion: "1", + }, + }, + }, + }, + { + name: "gateway with two https listeners each with two same namespace secrets", + gw: &gwapiv1b1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-gw", + Namespace: "test-ns", + }, + Spec: gwapiv1b1.GatewaySpec{ + GatewayClassName: "test-gc", + Listeners: []gwapiv1b1.Listener{ + { + Name: "tls1", + Port: gwapiv1b1.PortNumber(int32(443)), + Protocol: gwapiv1b1.HTTPSProtocolType, + TLS: &gwapiv1b1.GatewayTLSConfig{ + Mode: gatewayapi.TLSModeTypePtr(gwapiv1b1.TLSModeTerminate), + CertificateRefs: []gwapiv1b1.SecretObjectReference{ + { + Name: gwapiv1b1.ObjectName("test-secret1"), + }, + { + Name: gwapiv1b1.ObjectName("test-secret2"), + }, + }, + }, + }, + { + Name: "tls2", + Port: gwapiv1b1.PortNumber(int32(443)), + Protocol: gwapiv1b1.HTTPSProtocolType, + TLS: &gwapiv1b1.GatewayTLSConfig{ + Mode: gatewayapi.TLSModeTypePtr(gwapiv1b1.TLSModeTerminate), + CertificateRefs: []gwapiv1b1.SecretObjectReference{ + { + Name: gwapiv1b1.ObjectName("test-secret3"), + }, + { + Name: gwapiv1b1.ObjectName("test-secret4"), + }, + }, + }, + }, + }, + }, + }, + secrets: []corev1.Secret{ + { + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret1", + Namespace: "test-ns", + ResourceVersion: "1", + }, + }, + { + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret2", + Namespace: "test-ns", + ResourceVersion: "1", + }, + }, + { + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret3", + Namespace: "test-ns", + ResourceVersion: "1", + }, + }, + { + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret4", + Namespace: "test-ns", + ResourceVersion: "1", + }, + }, + }, + }, + { + name: "gateway with one https listener and two same namespace secret", + gw: &gwapiv1b1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-gw", + Namespace: "test-ns", + }, + Spec: gwapiv1b1.GatewaySpec{ + GatewayClassName: "test-gc", + Listeners: []gwapiv1b1.Listener{ + { + Name: "tls", + Port: gwapiv1b1.PortNumber(int32(443)), + Protocol: gwapiv1b1.HTTPSProtocolType, + TLS: &gwapiv1b1.GatewayTLSConfig{ + Mode: gatewayapi.TLSModeTypePtr(gwapiv1b1.TLSModeTerminate), + CertificateRefs: []gwapiv1b1.SecretObjectReference{ + { + Name: gwapiv1b1.ObjectName("test-secret1"), + }, + { + Name: gwapiv1b1.ObjectName("test-secret2"), + }, + }, + }, + }, + }, + }, + }, + secrets: []corev1.Secret{ + { + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret1", + Namespace: "test-ns", + ResourceVersion: "1", + }, + }, + { + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret2", + Namespace: "test-ns", + ResourceVersion: "1", + }, + }, + }, + }, + { + name: "gateway with one https listener and one different namespace secret", + gw: &gwapiv1b1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-gw", + Namespace: "test-ns", + }, + Spec: gwapiv1b1.GatewaySpec{ + GatewayClassName: "test-gc", + Listeners: []gwapiv1b1.Listener{ + { + Name: "tls", + Port: gwapiv1b1.PortNumber(int32(443)), + Protocol: gwapiv1b1.HTTPSProtocolType, + TLS: &gwapiv1b1.GatewayTLSConfig{ + Mode: gatewayapi.TLSModeTypePtr(gwapiv1b1.TLSModeTerminate), + CertificateRefs: []gwapiv1b1.SecretObjectReference{ + { + Name: gwapiv1b1.ObjectName("test-secret"), + Namespace: gatewayapi.NamespacePtr("test-ns2"), + }, + }, + }, + }, + }, + }, + }, + secrets: []corev1.Secret{ + { + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + Namespace: "test-ns2", + ResourceVersion: "1", + }, + }, + }, + refGrants: []gwapiv1a2.ReferenceGrant{ + { + TypeMeta: metav1.TypeMeta{ + Kind: "ReferenceGrant", + APIVersion: gwapiv1a2.GroupVersion.Version, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-refgrant", + Namespace: "test-ns2", + }, + Spec: gwapiv1a2.ReferenceGrantSpec{ + From: []gwapiv1a2.ReferenceGrantFrom{ + { + Group: gwapiv1a2.GroupName, + Kind: gatewayapi.KindGateway, + Namespace: gwapiv1a2.Namespace("test-ns"), + }, + }, + To: []gwapiv1a2.ReferenceGrantTo{ + { + Group: corev1.GroupName, + Kind: gatewayapi.KindSecret, + }, + }, + }, + }, + }, + }, + { + name: "gateway with one https listener and one different namespace secret with specific referencegrant", + gw: &gwapiv1b1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-gw", + Namespace: "test-ns", + }, + Spec: gwapiv1b1.GatewaySpec{ + GatewayClassName: "test-gc", + Listeners: []gwapiv1b1.Listener{ + { + Name: "tls", + Port: gwapiv1b1.PortNumber(int32(443)), + Protocol: gwapiv1b1.HTTPSProtocolType, + TLS: &gwapiv1b1.GatewayTLSConfig{ + Mode: gatewayapi.TLSModeTypePtr(gwapiv1b1.TLSModeTerminate), + CertificateRefs: []gwapiv1b1.SecretObjectReference{ + { + Name: gwapiv1b1.ObjectName("test-secret"), + Namespace: gatewayapi.NamespacePtr("test-ns2"), + }, + }, + }, + }, + }, + }, + }, + secrets: []corev1.Secret{ + { + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + Namespace: "test-ns2", + ResourceVersion: "1", + }, + }, + }, + refGrants: []gwapiv1a2.ReferenceGrant{ + { + TypeMeta: metav1.TypeMeta{ + Kind: "ReferenceGrant", + APIVersion: gwapiv1a2.GroupVersion.Version, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-refgrant", + Namespace: "test-ns2", + }, + Spec: gwapiv1a2.ReferenceGrantSpec{ + From: []gwapiv1a2.ReferenceGrantFrom{ + { + Group: gwapiv1a2.GroupName, + Kind: gatewayapi.KindGateway, + Namespace: gwapiv1a2.Namespace("test-ns"), + }, + }, + To: []gwapiv1a2.ReferenceGrantTo{ + { + Group: corev1.GroupName, + Kind: gatewayapi.KindSecret, + Name: gatewayapi.ObjectNamePtr("test-secret"), + }, + }, + }, + }, + }, + }, + } + + // Create the reconciler. + r := new(gatewayReconciler) + ctx := context.Background() + + for i := range testCases { + tc := testCases[i] + t.Run(tc.name, func(t *testing.T) { + var objs []client.Object + for j := range tc.secrets { + objs = append(objs, &tc.secrets[j]) + } + for k := range tc.refGrants { + objs = append(objs, &tc.refGrants[k]) + } + r.client = fakeclient.NewClientBuilder().WithScheme(envoygateway.GetScheme()).WithObjects(objs...).Build() + secrets, refGrants, err := r.secretsAndRefGrantsForGateway(ctx, tc.gw) + require.NoError(t, err) + require.Equal(t, tc.secrets, secrets) + require.Equal(t, tc.refGrants, refGrants) + }) + } +} diff --git a/internal/provider/kubernetes/gatewayclass.go b/internal/provider/kubernetes/gatewayclass.go index 2f4d0f31897..8a360202bb7 100644 --- a/internal/provider/kubernetes/gatewayclass.go +++ b/internal/provider/kubernetes/gatewayclass.go @@ -1,4 +1,9 @@ -// Portions of this code are based on code from Contour, available at: +// 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. + +// TODO Portions of this code are based on code from Contour, available at: // https://github.com/projectcontour/contour/blob/main/internal/controller/gatewayclass.go package kubernetes diff --git a/internal/provider/kubernetes/gatewayclass_test.go b/internal/provider/kubernetes/gatewayclass_test.go index a2e8497762e..4200a2379e8 100644 --- a/internal/provider/kubernetes/gatewayclass_test.go +++ b/internal/provider/kubernetes/gatewayclass_test.go @@ -1,3 +1,8 @@ +// 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 kubernetes import ( diff --git a/internal/provider/kubernetes/helpers.go b/internal/provider/kubernetes/helpers.go new file mode 100644 index 00000000000..7c94208b203 --- /dev/null +++ b/internal/provider/kubernetes/helpers.go @@ -0,0 +1,81 @@ +// 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 kubernetes + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" +) + +// validateParentRefs validates the provided routeParentReferences, returning the +// referenced Gateways managed by Envoy Gateway. The only supported parentRef +// is a Gateway. +func validateParentRefs(ctx context.Context, client client.Client, namespace string, + gatewayClassController gwapiv1b1.GatewayController, + routeParentReferences []gwapiv1b1.ParentReference) ([]gwapiv1b1.Gateway, error) { + + var ret []gwapiv1b1.Gateway + for i := range routeParentReferences { + ref := routeParentReferences[i] + if ref.Kind != nil && *ref.Kind != "Gateway" { + return nil, fmt.Errorf("invalid Kind %q", *ref.Kind) + } + if ref.Group != nil && *ref.Group != gwapiv1b1.GroupName { + return nil, fmt.Errorf("invalid Group %q", *ref.Group) + } + + // Ensure the referenced Gateway exists, using the route's namespace unless + // specified by the parentRef. + ns := namespace + if ref.Namespace != nil { + ns = string(*ref.Namespace) + } + gwKey := types.NamespacedName{ + Namespace: ns, + Name: string(ref.Name), + } + + gw := new(gwapiv1b1.Gateway) + if err := client.Get(ctx, gwKey, gw); err != nil { + return nil, fmt.Errorf("failed to get gateway %s/%s: %v", gwKey.Namespace, gwKey.Name, err) + } + + gcKey := types.NamespacedName{Name: string(gw.Spec.GatewayClassName)} + gc := new(gwapiv1b1.GatewayClass) + if err := client.Get(ctx, gcKey, gc); err != nil { + return nil, fmt.Errorf("failed to get gatewayclass %s: %v", gcKey.Name, err) + } + if gc.Spec.ControllerName == gatewayClassController { + ret = append(ret, *gw) + } + } + + return ret, nil +} + +// isRoutePresentInNamespace checks if any kind of Routes - HTTPRoute, TLSRoute +// exists in the namespace ns. +func isRoutePresentInNamespace(ctx context.Context, c client.Client, ns string) (bool, error) { + tlsRouteList := &gwapiv1a2.TLSRouteList{} + if err := c.List(ctx, tlsRouteList, &client.ListOptions{Namespace: ns}); err != nil { + return false, fmt.Errorf("error listing tlsroutes") + } + + httpRouteList := &gwapiv1b1.HTTPRouteList{} + if err := c.List(ctx, httpRouteList, &client.ListOptions{Namespace: ns}); err != nil { + return false, fmt.Errorf("error listing httproutes") + } + + if len(tlsRouteList.Items)+len(httpRouteList.Items) > 0 { + return true, nil + } + return false, nil +} diff --git a/internal/provider/kubernetes/httproute.go b/internal/provider/kubernetes/httproute.go index 88eb98a591d..e99ef92cbf6 100644 --- a/internal/provider/kubernetes/httproute.go +++ b/internal/provider/kubernetes/httproute.go @@ -1,5 +1,15 @@ -// Portions of this code are based on code from Contour, available at: +// 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. + +// This file contains code derived from Contour, +// https://github.com/projectcontour/contour +// from the source file // https://github.com/projectcontour/contour/blob/main/internal/controller/httproute.go +// and is provided here subject to the following: +// Copyright Project Contour Authors +// SPDX-License-Identifier: Apache-2.0 package kubernetes @@ -28,6 +38,8 @@ import ( ) const ( + kindHTTPRoute = "HTTPRoute" + serviceHTTPRouteIndex = "serviceHTTPRouteBackendRef" ) @@ -37,18 +49,20 @@ type httpRouteReconciler struct { statusUpdater status.Updater classController gwapiv1b1.GatewayController - resources *message.ProviderResources + resources *message.ProviderResources + referenceStore *providerReferenceStore } // newHTTPRouteController creates the httproute controller from mgr. The controller will be pre-configured // to watch for HTTPRoute objects across all namespaces. -func newHTTPRouteController(mgr manager.Manager, cfg *config.Server, su status.Updater, resources *message.ProviderResources) error { +func newHTTPRouteController(mgr manager.Manager, cfg *config.Server, su status.Updater, resources *message.ProviderResources, referenceStore *providerReferenceStore) error { r := &httpRouteReconciler{ client: mgr.GetClient(), log: cfg.Logger, classController: gwapiv1b1.GatewayController(cfg.EnvoyGateway.Gateway.ControllerName), statusUpdater: su, resources: resources, + referenceStore: referenceStore, } c, err := controller.New("httproute", mgr, controller.Options{Reconciler: r}) @@ -129,7 +143,7 @@ func (r *httpRouteReconciler) getHTTPRoutesForGateway(obj client.Object) []recon requests := []reconcile.Request{} for i := range routes.Items { route := routes.Items[i] - gateways, err := r.validateParentRefs(ctx, &route) + gateways, err := validateParentRefs(ctx, r.client, route.Namespace, r.classController, route.Spec.ParentRefs) if err != nil { r.log.Info("invalid parentRefs for httproute, bypassing reconciliation", "object", obj) continue @@ -195,7 +209,7 @@ func (r *httpRouteReconciler) Reconcile(ctx context.Context, request reconcile.R } // Validate the route. - gws, err := r.validateParentRefs(ctx, &route) + gws, err := validateParentRefs(ctx, r.client, route.Namespace, r.classController, route.Spec.ParentRefs) if err != nil { // Remove the route from the watchable map since it's invalid. r.resources.HTTPRoutes.Delete(routeKey) @@ -212,10 +226,11 @@ func (r *httpRouteReconciler) Reconcile(ctx context.Context, request reconcile.R return reconcile.Result{}, nil } - // Store the httproute in the resource map. - r.resources.HTTPRoutes.Store(routeKey, &route) - log.Info("added httproute to resource map") - + // only store the resource if it does not exist or it has a newer spec. + if v, ok := r.resources.HTTPRoutes.Load(routeKey); !ok || (route.Generation > v.Generation) { + r.resources.HTTPRoutes.Store(routeKey, &route) + log.Info("added httproute to resource map") + } // Get the route's namespace from the cache. nsKey := types.NamespacedName{Name: route.Namespace} ns := new(corev1.Namespace) @@ -253,6 +268,10 @@ func (r *httpRouteReconciler) Reconcile(ctx context.Context, request reconcile.R // the resource map if it exists. if _, ok := r.resources.Services.Load(svcKey); ok { r.resources.Services.Delete(svcKey) + r.referenceStore.removeRouteToServicesMapping( + ObjectKindNamespacedName{kindHTTPRoute, route.Namespace, route.Name}, + svcKey, + ) log.Info("deleted service from resource map") } } @@ -262,6 +281,10 @@ func (r *httpRouteReconciler) Reconcile(ctx context.Context, request reconcile.R // The backendRef Service exists, so add it to the resource map. r.resources.Services.Store(svcKey, svc) + r.referenceStore.updateRouteToServicesMapping( + ObjectKindNamespacedName{kindHTTPRoute, route.Namespace, route.Name}, + svcKey, + ) log.Info("added service to resource map") } } @@ -273,16 +296,25 @@ func (r *httpRouteReconciler) Reconcile(ctx context.Context, request reconcile.R log.Info("deleted httproute from resource map") // Delete the Namespace and Service from the resource maps if no other - // routes exist in the namespace. - routeList = &gwapiv1b1.HTTPRouteList{} - if err := r.client.List(ctx, routeList, &client.ListOptions{Namespace: request.Namespace}); err != nil { - return reconcile.Result{}, fmt.Errorf("error listing httproutes") + // routes (TLSRoute or HTTPRoute) exist in the namespace. + found, err := isRoutePresentInNamespace(ctx, r.client, request.NamespacedName.Namespace) + if err != nil { + return reconcile.Result{}, err } - if len(routeList.Items) == 0 { + if !found { r.resources.Namespaces.Delete(request.Namespace) log.Info("deleted namespace from resource map") - r.resources.Services.Delete(request.NamespacedName) - log.Info("deleted service from resource map") + } + + // Delete the Service from the resource maps if no other + // routes (TLSRoute or HTTPRoute) reference that Service. + routeServices := r.referenceStore.getRouteToServicesMapping(ObjectKindNamespacedName{kindHTTPRoute, request.Namespace, request.Name}) + for svc := range routeServices { + r.referenceStore.removeRouteToServicesMapping(ObjectKindNamespacedName{kindHTTPRoute, request.Namespace, request.Name}, svc) + if !r.referenceStore.isServiceReferredByRoutes(svc) { + r.resources.Services.Delete(svc) + log.Info("deleted service from resource map", "namespace", svc.Namespace, "name", svc.Name) + } } } @@ -317,13 +349,11 @@ func validateBackendRef(ref *gwapiv1b1.HTTPBackendRef) error { // Kubernetes API Server func (r *httpRouteReconciler) subscribeAndUpdateStatus(ctx context.Context) { // Subscribe to resources - for snapshot := range r.resources.HTTPRouteStatuses.Subscribe(ctx) { - r.log.Info("received a status notification") - updates := snapshot.Updates - for _, update := range updates { + message.HandleSubscription(r.resources.HTTPRouteStatuses.Subscribe(ctx), + func(update message.Update[types.NamespacedName, *gwapiv1b1.HTTPRoute]) { // skip delete updates. if update.Delete { - continue + return } key := update.Key val := update.Value @@ -340,50 +370,7 @@ func (r *httpRouteReconciler) subscribeAndUpdateStatus(ctx context.Context) { return hCopy }), }) - } - } + }, + ) r.log.Info("status subscriber shutting down") } - -// validateParentRefs validates parentRefs for the provided route, returning the referenced Gateways -// managed by Envoy Gateway. The only supported parentRef is a Gateway. -func (r *httpRouteReconciler) validateParentRefs(ctx context.Context, route *gwapiv1b1.HTTPRoute) ([]gwapiv1b1.Gateway, error) { - if route == nil { - return nil, fmt.Errorf("httproute is nil") - } - - var ret []gwapiv1b1.Gateway - for i := range route.Spec.ParentRefs { - ref := route.Spec.ParentRefs[i] - if ref.Kind != nil && *ref.Kind != "Gateway" { - return nil, fmt.Errorf("invalid Kind %q", *ref.Kind) - } - if ref.Group != nil && *ref.Group != gwapiv1b1.GroupName { - return nil, fmt.Errorf("invalid Group %q", *ref.Group) - } - // Ensure the referenced Gateway exists, using the route's namespace unless - // specified by the parentRef. - ns := route.Namespace - if ref.Namespace != nil { - ns = string(*ref.Namespace) - } - gwKey := types.NamespacedName{ - Namespace: ns, - Name: string(ref.Name), - } - gw := new(gwapiv1b1.Gateway) - if err := r.client.Get(ctx, gwKey, gw); err != nil { - return nil, fmt.Errorf("failed to get gateway %s/%s: %v", gwKey.Namespace, gwKey.Name, err) - } - gcKey := types.NamespacedName{Name: string(gw.Spec.GatewayClassName)} - gc := new(gwapiv1b1.GatewayClass) - if err := r.client.Get(ctx, gcKey, gc); err != nil { - return nil, fmt.Errorf("failed to get gatewayclass %s: %v", gcKey.Name, err) - } - if gc.Spec.ControllerName == r.classController { - ret = append(ret, *gw) - } - } - - return ret, nil -} diff --git a/internal/provider/kubernetes/httproute_test.go b/internal/provider/kubernetes/httproute_test.go index 6fd509c78b0..605213984d6 100644 --- a/internal/provider/kubernetes/httproute_test.go +++ b/internal/provider/kubernetes/httproute_test.go @@ -1,3 +1,8 @@ +// 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 kubernetes import ( @@ -732,7 +737,7 @@ func TestValidateParentRefs(t *testing.T) { objs = append(objs, tc.gateways[i]) } r.client = fakeclient.NewClientBuilder().WithScheme(envoygateway.GetScheme()).WithObjects(objs...).Build() - gws, err := r.validateParentRefs(ctx, tc.route) + gws, err := validateParentRefs(ctx, r.client, tc.route.Namespace, r.classController, tc.route.Spec.ParentRefs) if tc.expected { require.NoError(t, err) } else { diff --git a/internal/provider/kubernetes/kubernetes.go b/internal/provider/kubernetes/kubernetes.go index 1383078572b..0a6903e3d3a 100644 --- a/internal/provider/kubernetes/kubernetes.go +++ b/internal/provider/kubernetes/kubernetes.go @@ -1,3 +1,8 @@ +// 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 kubernetes import ( @@ -7,6 +12,7 @@ import ( "k8s.io/client-go/rest" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/manager" "github.com/envoyproxy/gateway/internal/envoygateway" @@ -27,11 +33,12 @@ type Provider struct { func New(cfg *rest.Config, svr *config.Server, resources *message.ProviderResources) (*Provider, error) { // TODO: Decide which mgr opts should be exposed through envoygateway.provider.kubernetes API. mgrOpts := manager.Options{ - Scheme: envoygateway.GetScheme(), - Logger: svr.Logger, - LeaderElection: false, - LeaderElectionID: "5b9825d2.gateway.envoyproxy.io", - MetricsBindAddress: ":8080", + Scheme: envoygateway.GetScheme(), + Logger: svr.Logger, + LeaderElection: false, + HealthProbeBindAddress: ":8081", + LeaderElectionID: "5b9825d2.gateway.envoyproxy.io", + MetricsBindAddress: ":8080", } mgr, err := ctrl.NewManager(cfg, mgrOpts) if err != nil { @@ -43,6 +50,9 @@ func New(cfg *rest.Config, svr *config.Server, resources *message.ProviderResour return nil, fmt.Errorf("failed to add status update handler %v", err) } + // Initialize kubernetes provider referenceStore to store additional object mappings. + referenceStore := newProviderReferenceStore() + // Create and register the controllers with the manager. if err := newGatewayClassController(mgr, svr, updateHandler.Writer(), resources); err != nil { return nil, fmt.Errorf("failed to create gatewayclass controller: %w", err) @@ -50,9 +60,23 @@ func New(cfg *rest.Config, svr *config.Server, resources *message.ProviderResour if err := newGatewayController(mgr, svr, updateHandler.Writer(), resources); err != nil { return nil, fmt.Errorf("failed to create gateway controller: %w", err) } - if err := newHTTPRouteController(mgr, svr, updateHandler.Writer(), resources); err != nil { + + if err := newHTTPRouteController(mgr, svr, updateHandler.Writer(), resources, referenceStore); err != nil { return nil, fmt.Errorf("failed to create httproute controller: %w", err) } + if err := newTLSRouteController(mgr, svr, updateHandler.Writer(), resources, referenceStore); err != nil { + return nil, fmt.Errorf("failed to create tlsroute controller: %w", err) + } + + // Add health check health probes. + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + return nil, fmt.Errorf("unable to set up health check: %w", err) + } + + // Add ready check health probes. + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + return nil, fmt.Errorf("unable to set up ready check: %w", err) + } return &Provider{ manager: mgr, diff --git a/internal/provider/kubernetes/kubernetes_test.go b/internal/provider/kubernetes/kubernetes_test.go index fec19e3d455..13202eed5ef 100644 --- a/internal/provider/kubernetes/kubernetes_test.go +++ b/internal/provider/kubernetes/kubernetes_test.go @@ -1,6 +1,13 @@ //go:build integration // +build integration +// 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 kubernetes import ( @@ -21,6 +28,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/envtest" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" + gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" "github.com/envoyproxy/gateway/api/config/v1alpha1" @@ -58,10 +66,12 @@ func TestProvider(t *testing.T) { }() testcases := map[string]func(context.Context, *testing.T, *Provider, *message.ProviderResources){ - "gatewayclass controller name": testGatewayClassController, - "gatewayclass accepted status": testGatewayClassAcceptedStatus, - "gateway scheduled status": testGatewayScheduledStatus, - "httproute": testHTTPRoute, + "gatewayclass controller name": testGatewayClassController, + "gatewayclass accepted status": testGatewayClassAcceptedStatus, + "gateway scheduled status": testGatewayScheduledStatus, + "httproute": testHTTPRoute, + "tlsroute": testTLSRoute, + "stale service cleanup route deletion": testServiceCleanupForMultipleRoutes, } for name, tc := range testcases { t.Run(name, func(t *testing.T) { @@ -83,17 +93,40 @@ func startEnv() (*envtest.Environment, *rest.Config, error) { return env, cfg, nil } -func testGatewayClassController(ctx context.Context, t *testing.T, provider *Provider, resources *message.ProviderResources) { - cli := provider.manager.GetClient() - - gc := &gwapiv1b1.GatewayClass{ +func getGatewayClass(name string) *gwapiv1b1.GatewayClass { + return &gwapiv1b1.GatewayClass{ ObjectMeta: metav1.ObjectMeta{ - Name: "test-gc-controllername", + Name: name, }, Spec: gwapiv1b1.GatewayClassSpec{ - ControllerName: v1alpha1.GatewayControllerName, + ControllerName: gwapiv1b1.GatewayController(v1alpha1.GatewayControllerName), + }, + } +} + +func getService(name, namespace string, ports map[string]int32) *corev1.Service { + service := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{}, }, } + for name, port := range ports { + service.Spec.Ports = append(service.Spec.Ports, corev1.ServicePort{ + Name: name, + Port: port, + }) + } + return service +} + +func testGatewayClassController(ctx context.Context, t *testing.T, provider *Provider, resources *message.ProviderResources) { + cli := provider.manager.GetClient() + + gc := getGatewayClass("test-gc-controllername") require.NoError(t, cli.Create(ctx, gc)) defer func() { @@ -109,14 +142,7 @@ func testGatewayClassController(ctx context.Context, t *testing.T, provider *Pro func testGatewayClassAcceptedStatus(ctx context.Context, t *testing.T, provider *Provider, resources *message.ProviderResources) { cli := provider.manager.GetClient() - gc := &gwapiv1b1.GatewayClass{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-gc-accepted-status", - }, - Spec: gwapiv1b1.GatewayClassSpec{ - ControllerName: v1alpha1.GatewayControllerName, - }, - } + gc := getGatewayClass("test-gc-accepted-status") require.NoError(t, cli.Create(ctx, gc)) defer func() { @@ -145,14 +171,7 @@ func testGatewayClassAcceptedStatus(ctx context.Context, t *testing.T, provider func testGatewayScheduledStatus(ctx context.Context, t *testing.T, provider *Provider, resources *message.ProviderResources) { cli := provider.manager.GetClient() - gc := &gwapiv1b1.GatewayClass{ - ObjectMeta: metav1.ObjectMeta{ - Name: "gc-scheduled-status-test", - }, - Spec: gwapiv1b1.GatewayClassSpec{ - ControllerName: gwapiv1b1.GatewayController(v1alpha1.GatewayControllerName), - }, - } + gc := getGatewayClass("gc-scheduled-status-test") require.NoError(t, cli.Create(ctx, gc)) // Ensure the GatewayClass reports "Ready". @@ -235,23 +254,22 @@ func testGatewayScheduledStatus(ctx context.Context, t *testing.T, provider *Pro return cli.Get(ctx, key, gw) == nil }, defaultWait, defaultTick) gws, _ := resources.Gateways.Load(key) - assert.Equal(t, gw, gws) + // Only check if the spec is equal + // The watchable map will not store a resource + // with an updated status if the spec has not changed + // to eliminate this endless loop: + // reconcile->store->translate->update-status->reconcile + assert.Equal(t, gw.Spec, gws.Spec) } -func testHTTPRoute(ctx context.Context, t *testing.T, provider *Provider, resources *message.ProviderResources) { +// Test that even when resources such as the Service/Deployment get hashed names (because of a gateway with a very long name) +func testLongNameHashedResources(ctx context.Context, t *testing.T, provider *Provider, resources *message.ProviderResources) { cli := provider.manager.GetClient() - gc := &gwapiv1b1.GatewayClass{ - ObjectMeta: metav1.ObjectMeta{ - Name: "httproute-test", - }, - Spec: gwapiv1b1.GatewayClassSpec{ - ControllerName: gwapiv1b1.GatewayController(v1alpha1.GatewayControllerName), - }, - } + gc := getGatewayClass("envoy-gateway-class") require.NoError(t, cli.Create(ctx, gc)) - // Ensure the GatewayClass reports ready. + // Ensure the GatewayClass reports "Ready". require.Eventually(t, func() bool { if err := cli.Get(ctx, types.NamespacedName{Name: gc.Name}, gc); err != nil { return false @@ -271,12 +289,12 @@ func testHTTPRoute(ctx context.Context, t *testing.T, provider *Provider, resour }() // Create the namespace for the Gateway under test. - ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "httproute-test"}} + ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "envoy-gateway"}} require.NoError(t, cli.Create(ctx, ns)) gw := &gwapiv1b1.Gateway{ ObjectMeta: metav1.ObjectMeta{ - Name: "httproute-test", + Name: "gatewaywithaverylongnamethatwillresultinhashedresources", Namespace: ns.Name, }, Spec: gwapiv1b1.GatewaySpec{ @@ -292,28 +310,113 @@ func testHTTPRoute(ctx context.Context, t *testing.T, provider *Provider, resour } require.NoError(t, cli.Create(ctx, gw)) + // Ensure the Gateway is ready and gets an address. + ready := false + hasAddress := false + require.Eventually(t, func() bool { + if err := cli.Get(ctx, types.NamespacedName{Namespace: gw.Namespace, Name: gw.Name}, gw); err != nil { + return false + } + + for _, cond := range gw.Status.Conditions { + fmt.Printf("Condition: %v", cond) + if cond.Type == string(gwapiv1b1.GatewayConditionReady) && cond.Status == metav1.ConditionTrue { + ready = true + } + } + + if gw.Status.Addresses != nil { + hasAddress = len(gw.Status.Addresses) >= 1 + } + + return ready && hasAddress + }, defaultWait, defaultTick) + defer func() { require.NoError(t, cli.Delete(ctx, gw)) }() - svc := &corev1.Service{ + // Ensure the gatewayclass has been finalized. + require.NoError(t, cli.Get(ctx, types.NamespacedName{Name: gc.Name}, gc)) + require.Contains(t, gc.Finalizers, gatewayClassFinalizer) + + // Ensure the number of Gateways in the Gateway resource table is as expected. + require.Eventually(t, func() bool { + return resources.Gateways.Len() == 1 + }, defaultWait, defaultTick) + + // Ensure the test Gateway in the Gateway resources is as expected. + key := types.NamespacedName{ + Namespace: gw.Namespace, + Name: gw.Name, + } + require.Eventually(t, func() bool { + return cli.Get(ctx, key, gw) == nil + }, defaultWait, defaultTick) + gws, _ := resources.Gateways.Load(key) + // Only check if the spec is equal + // The watchable map will not store a resource + // with an updated status if the spec has not changed + // to eliminate this endless loop: + // reconcile->store->translate->update-status->reconcile + assert.Equal(t, gw.Spec, gws.Spec) +} + +func testHTTPRoute(ctx context.Context, t *testing.T, provider *Provider, resources *message.ProviderResources) { + cli := provider.manager.GetClient() + + gc := getGatewayClass("httproute-test") + require.NoError(t, cli.Create(ctx, gc)) + + // Ensure the GatewayClass reports ready. + require.Eventually(t, func() bool { + if err := cli.Get(ctx, types.NamespacedName{Name: gc.Name}, gc); err != nil { + return false + } + + for _, cond := range gc.Status.Conditions { + if cond.Type == string(gwapiv1b1.GatewayClassConditionStatusAccepted) && cond.Status == metav1.ConditionTrue { + return true + } + } + + return false + }, defaultWait, defaultTick) + + defer func() { + require.NoError(t, cli.Delete(ctx, gc)) + }() + + // Create the namespace for the Gateway under test. + ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "httproute-test"}} + require.NoError(t, cli.Create(ctx, ns)) + + gw := &gwapiv1b1.Gateway{ ObjectMeta: metav1.ObjectMeta{ + Name: "httproute-test", Namespace: ns.Name, - Name: "test", }, - Spec: corev1.ServiceSpec{ - Ports: []corev1.ServicePort{ - { - Name: "http", - Port: 80, - }, + Spec: gwapiv1b1.GatewaySpec{ + GatewayClassName: gwapiv1b1.ObjectName(gc.Name), + Listeners: []gwapiv1b1.Listener{ { - Name: "https", - Port: 443, + Name: "test", + Port: gwapiv1b1.PortNumber(int32(8080)), + Protocol: gwapiv1b1.HTTPProtocolType, }, }, }, } + require.NoError(t, cli.Create(ctx, gw)) + + defer func() { + require.NoError(t, cli.Delete(ctx, gw)) + }() + + svc := getService("test", ns.Name, map[string]int32{ + "http": 80, + "https": 443, + }) require.NoError(t, cli.Create(ctx, svc)) @@ -579,3 +682,254 @@ func testHTTPRoute(ctx context.Context, t *testing.T, provider *Provider, resour }) } } + +func testTLSRoute(ctx context.Context, t *testing.T, provider *Provider, resources *message.ProviderResources) { + cli := provider.manager.GetClient() + + gc := getGatewayClass("tlsroute-test") + require.NoError(t, cli.Create(ctx, gc)) + + defer func() { + require.NoError(t, cli.Delete(ctx, gc)) + }() + + // Create the namespace for the Gateway under test. + ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "tlsroute-test"}} + require.NoError(t, cli.Create(ctx, ns)) + + gw := &gwapiv1b1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tlsroute-test", + Namespace: ns.Name, + }, + Spec: gwapiv1b1.GatewaySpec{ + GatewayClassName: gwapiv1b1.ObjectName(gc.Name), + Listeners: []gwapiv1b1.Listener{ + { + Name: "test", + Port: gwapiv1b1.PortNumber(int32(8080)), + Protocol: gwapiv1b1.TLSProtocolType, + }, + }, + }, + } + require.NoError(t, cli.Create(ctx, gw)) + + defer func() { + require.NoError(t, cli.Delete(ctx, gw)) + }() + + svc := getService("test", ns.Name, map[string]int32{ + "tls": 90, + }) + require.NoError(t, cli.Create(ctx, svc)) + defer func() { + require.NoError(t, cli.Delete(ctx, svc)) + }() + + var testCases = []struct { + name string + route gwapiv1a2.TLSRoute + }{ + { + name: "tlsroute", + route: gwapiv1a2.TLSRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tlsroute-test", + Namespace: ns.Name, + }, + Spec: gwapiv1a2.TLSRouteSpec{ + CommonRouteSpec: gwapiv1a2.CommonRouteSpec{ + ParentRefs: []gwapiv1a2.ParentReference{ + { + Name: gwapiv1a2.ObjectName(gw.Name), + }, + }, + }, + Hostnames: []gwapiv1a2.Hostname{"test.hostname.local"}, + Rules: []gwapiv1a2.TLSRouteRule{ + { + BackendRefs: []gwapiv1a2.BackendRef{ + { + BackendObjectReference: gwapiv1a2.BackendObjectReference{ + Name: "test", + }, + }, + }, + }, + }, + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + require.NoError(t, cli.Create(ctx, &testCase.route)) + defer func() { + require.NoError(t, cli.Delete(ctx, &testCase.route)) + }() + + require.Eventually(t, func() bool { + return resources.TLSRoutes.Len() == 1 + }, defaultWait, defaultTick) + + // Ensure the test TLSRoute in the TLSRoute resources is as expected. + key := types.NamespacedName{ + Namespace: testCase.route.Namespace, + Name: testCase.route.Name, + } + require.Eventually(t, func() bool { + return cli.Get(ctx, key, &testCase.route) == nil + }, defaultWait, defaultTick) + troutes, _ := resources.TLSRoutes.Load(key) + assert.Equal(t, &testCase.route, troutes) + + // Ensure the TLSRoute Namespace is in the Namespace resource map. + require.Eventually(t, func() bool { + _, ok := resources.Namespaces.Load(testCase.route.Namespace) + return ok + }, defaultWait, defaultTick) + + // Ensure the Service is in the resource map. + svcKey := utils.NamespacedName(svc) + require.Eventually(t, func() bool { + _, ok := resources.Services.Load(svcKey) + return ok + }, defaultWait, defaultTick) + }) + } +} + +// testServiceCleanupForMultipleRoutes creates multiple Routes pointing to the +// same backend Service, and checks whether the Service is properly removed +// from the resource map after Route deletion. +func testServiceCleanupForMultipleRoutes(ctx context.Context, t *testing.T, provider *Provider, resources *message.ProviderResources) { + cli := provider.manager.GetClient() + + gc := getGatewayClass("service-cleanup-test") + require.NoError(t, cli.Create(ctx, gc)) + defer func() { + require.NoError(t, cli.Delete(ctx, gc)) + }() + + // Create the namespace for the Gateway under test. + ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "service-cleanup-test"}} + require.NoError(t, cli.Create(ctx, ns)) + + gw := &gwapiv1b1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service-cleanup-test", + Namespace: ns.Name, + }, + Spec: gwapiv1b1.GatewaySpec{ + GatewayClassName: gwapiv1b1.ObjectName(gc.Name), + Listeners: []gwapiv1b1.Listener{ + { + Name: "httptest", + Port: gwapiv1b1.PortNumber(int32(8080)), + Protocol: gwapiv1b1.HTTPProtocolType, + }, + { + Name: "tlstest", + Port: gwapiv1b1.PortNumber(int32(8043)), + Protocol: gwapiv1b1.TLSProtocolType, + }, + }, + }, + } + require.NoError(t, cli.Create(ctx, gw)) + defer func() { + require.NoError(t, cli.Delete(ctx, gw)) + }() + + svc := getService("test-common-svc", ns.Name, map[string]int32{ + "http": 80, + "tls": 90, + }) + require.NoError(t, cli.Create(ctx, svc)) + defer func() { + require.NoError(t, cli.Delete(ctx, svc)) + }() + + tlsRoute := gwapiv1a2.TLSRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tlsroute-test", + Namespace: ns.Name, + }, + Spec: gwapiv1a2.TLSRouteSpec{ + CommonRouteSpec: gwapiv1a2.CommonRouteSpec{ + ParentRefs: []gwapiv1a2.ParentReference{{ + Name: gwapiv1a2.ObjectName(gw.Name), + }}, + }, + Hostnames: []gwapiv1a2.Hostname{"test-tls.hostname.local"}, + Rules: []gwapiv1a2.TLSRouteRule{{ + BackendRefs: []gwapiv1a2.BackendRef{{ + BackendObjectReference: gwapiv1a2.BackendObjectReference{ + Name: "test-common-svc", + }}, + }}, + }, + }, + } + + httpRoute := gwapiv1b1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "httproute-test", + Namespace: ns.Name, + }, + Spec: gwapiv1b1.HTTPRouteSpec{ + CommonRouteSpec: gwapiv1b1.CommonRouteSpec{ + ParentRefs: []gwapiv1b1.ParentReference{{ + Name: gwapiv1b1.ObjectName(gw.Name), + }}, + }, + Hostnames: []gwapiv1b1.Hostname{"test-http.hostname.local"}, + Rules: []gwapiv1b1.HTTPRouteRule{{ + Matches: []gwapiv1b1.HTTPRouteMatch{{ + Path: &gwapiv1b1.HTTPPathMatch{ + Type: gatewayapi.PathMatchTypePtr(gwapiv1b1.PathMatchPathPrefix), + Value: gatewayapi.StringPtr("/"), + }, + }}, + BackendRefs: []gwapiv1b1.HTTPBackendRef{{ + BackendRef: gwapiv1b1.BackendRef{ + BackendObjectReference: gwapiv1b1.BackendObjectReference{ + Name: "test-common-svc", + }, + }, + }}, + }}, + }, + } + + // Create the TLSRoute and HTTPRoute + require.NoError(t, cli.Create(ctx, &tlsRoute)) + require.NoError(t, cli.Create(ctx, &httpRoute)) + + // Check that the Service is present in the resource map + key := types.NamespacedName{ + Namespace: svc.Namespace, + Name: svc.Name, + } + + require.Eventually(t, func() bool { + rSvc, _ := resources.Services.Load(key) + return rSvc != nil + }, defaultWait, defaultTick) + + // Delete the TLSRoute, and check if the Service is still present + require.NoError(t, cli.Delete(ctx, &tlsRoute)) + require.Eventually(t, func() bool { + rSvc, _ := resources.Services.Load(key) + return rSvc != nil + }, defaultWait, defaultTick) + + // Delete the HTTPRoute, and check if the Service is also removed + require.NoError(t, cli.Delete(ctx, &httpRoute)) + require.Eventually(t, func() bool { + rSvc, _ := resources.Services.Load(key) + return rSvc == nil + }, defaultWait, defaultTick) +} diff --git a/internal/provider/kubernetes/rbac.go b/internal/provider/kubernetes/rbac.go index 32a72ee1937..8222daec3e5 100644 --- a/internal/provider/kubernetes/rbac.go +++ b/internal/provider/kubernetes/rbac.go @@ -1,7 +1,12 @@ +// 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 kubernetes -// +kubebuilder:rbac:groups="gateway.networking.k8s.io",resources=gatewayclasses;gateways;httproutes;referencepolicies;referencegrants,verbs=get;list;watch;update -// +kubebuilder:rbac:groups="gateway.networking.k8s.io",resources=gatewayclasses/status;gateways/status;httproutes/status,verbs=update +// +kubebuilder:rbac:groups="gateway.networking.k8s.io",resources=gatewayclasses;gateways;httproutes;tlsroutes;referencepolicies;referencegrants,verbs=get;list;watch;update +// +kubebuilder:rbac:groups="gateway.networking.k8s.io",resources=gatewayclasses/status;gateways/status;httproutes/status;tlsroutes/status,verbs=update // RBAC for watched resources of Gateway API controllers. // +kubebuilder:rbac:groups="",resources=secrets;services;namespaces,verbs=get;list;watch diff --git a/internal/provider/kubernetes/secrets.go b/internal/provider/kubernetes/secrets.go index 3027ecb0dc8..b3a0ae251d7 100644 --- a/internal/provider/kubernetes/secrets.go +++ b/internal/provider/kubernetes/secrets.go @@ -1,3 +1,8 @@ +// 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 kubernetes import ( diff --git a/internal/provider/kubernetes/store.go b/internal/provider/kubernetes/store.go new file mode 100644 index 00000000000..3ecc514fda5 --- /dev/null +++ b/internal/provider/kubernetes/store.go @@ -0,0 +1,77 @@ +// 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 kubernetes + +import ( + "sync" + + "k8s.io/apimachinery/pkg/types" +) + +// providerReferenceStore maintains additional mappings related to Kubernetes provider +// resources. The mappings are regularly updated from the reconcilers based +// on the existence of the object in the Kubernetes datastore. +type providerReferenceStore struct { + mu sync.Mutex + + // routeToServicesMappings maintains a mapping of a Route object, + // and the Services it references. For instance + // HTTPRoute/ns1/route1 -> { ns1/svc1, ns1/svc2, ns2/svc1 } + // TLSRoute/ns1/route1 -> { ns1/svc1, ns2/svc2 } + routeToServicesMappings map[ObjectKindNamespacedName]map[types.NamespacedName]struct{} +} + +type ObjectKindNamespacedName struct { + kind string + namespace string + name string +} + +func newProviderReferenceStore() *providerReferenceStore { + return &providerReferenceStore{ + routeToServicesMappings: map[ObjectKindNamespacedName]map[types.NamespacedName]struct{}{}, + } +} + +func (p *providerReferenceStore) getRouteToServicesMapping(route ObjectKindNamespacedName) map[types.NamespacedName]struct{} { + p.mu.Lock() + defer p.mu.Unlock() + + return p.routeToServicesMappings[route] +} + +func (p *providerReferenceStore) updateRouteToServicesMapping(route ObjectKindNamespacedName, service types.NamespacedName) { + p.mu.Lock() + defer p.mu.Unlock() + + if len(p.routeToServicesMappings[route]) == 0 { + p.routeToServicesMappings[route] = map[types.NamespacedName]struct{}{service: {}} + } else { + p.routeToServicesMappings[route][service] = struct{}{} + } +} + +func (p *providerReferenceStore) removeRouteToServicesMapping(route ObjectKindNamespacedName, service types.NamespacedName) { + p.mu.Lock() + defer p.mu.Unlock() + + delete(p.routeToServicesMappings[route], service) + if len(p.routeToServicesMappings[route]) == 0 { + delete(p.routeToServicesMappings, route) + } +} + +func (p *providerReferenceStore) isServiceReferredByRoutes(service types.NamespacedName) bool { + p.mu.Lock() + defer p.mu.Unlock() + + for _, svcs := range p.routeToServicesMappings { + if _, ok := svcs[service]; ok { + return true + } + } + return false +} diff --git a/internal/provider/kubernetes/store_test.go b/internal/provider/kubernetes/store_test.go new file mode 100644 index 00000000000..66c3c4bfdf3 --- /dev/null +++ b/internal/provider/kubernetes/store_test.go @@ -0,0 +1,67 @@ +// 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 kubernetes + +import ( + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/types" +) + +func TestProviderReferenceStore(t *testing.T) { + cache := newProviderReferenceStore() + + testCases := []struct { + name string + test func(t *testing.T, c *providerReferenceStore) + }{ + { + name: "route to service mappings", + test: testRouteToServicesMappings, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + tc.test(t, cache) + }) + } +} + +func testRouteToServicesMappings(t *testing.T, cache *providerReferenceStore) { + httpr1 := ObjectKindNamespacedName{"HTTPRoute", "ns1", "r1"} + tlsr1 := ObjectKindNamespacedName{"TLSRoute", "ns1", "r1"} + + ns1svc1 := types.NamespacedName{Namespace: "ns1", Name: "svc1"} + ns1svc2 := types.NamespacedName{Namespace: "ns1", Name: "svc2"} + + // Add HTTPRoute/ns1/r1 -> ns1/svc1 mapping + cache.updateRouteToServicesMapping(httpr1, ns1svc1) + require.Equal(t, map[types.NamespacedName]struct{}{ns1svc1: {}}, cache.getRouteToServicesMapping(httpr1)) + + // Add HTTPRoute/ns1/r1 -> ns1/svc2 mapping + // Add TLSRoute/ns1/r1 -> ns1/svc2 mapping + cache.updateRouteToServicesMapping(tlsr1, ns1svc2) + cache.updateRouteToServicesMapping(httpr1, ns1svc2) + require.Equal(t, map[types.NamespacedName]struct{}{ns1svc2: {}}, cache.getRouteToServicesMapping(tlsr1)) + require.Equal(t, map[types.NamespacedName]struct{}{ns1svc1: {}, ns1svc2: {}}, cache.getRouteToServicesMapping(httpr1)) + + // Remove HTTPRoute/ns1/r1 -> ns1/svc1 mapping + cache.removeRouteToServicesMapping(httpr1, ns1svc1) + require.Equal(t, map[types.NamespacedName]struct{}{ns1svc2: {}}, cache.getRouteToServicesMapping(httpr1)) + + // Remove TLSRoute/ns1/r1 -> ns1/svc2 mapping + cache.removeRouteToServicesMapping(tlsr1, ns1svc2) + require.Equal(t, map[types.NamespacedName]struct{}(nil), cache.getRouteToServicesMapping(tlsr1)) + + // Verify that ns1svc2 is still referred by another route (HTTPRoute/ns1/r1) + require.Equal(t, true, cache.isServiceReferredByRoutes(ns1svc2)) + + // Verify that ns1svc1 is not referred by any other route + require.Equal(t, false, cache.isServiceReferredByRoutes(ns1svc1)) +} diff --git a/internal/provider/kubernetes/testdata/in/referencegrant-experimental-crd.yaml b/internal/provider/kubernetes/testdata/in/referencegrant-experimental-crd.yaml new file mode 100644 index 00000000000..0cc826ee422 --- /dev/null +++ b/internal/provider/kubernetes/testdata/in/referencegrant-experimental-crd.yaml @@ -0,0 +1,149 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/1086 + gateway.networking.k8s.io/bundle-version: v0.5.0 + gateway.networking.k8s.io/channel: experimental + creationTimestamp: null + name: referencegrants.gateway.networking.k8s.io +spec: + group: gateway.networking.k8s.io + names: + categories: + - gateway-api + kind: ReferenceGrant + listKind: ReferenceGrantList + plural: referencegrants + shortNames: + - refgrant + singular: referencegrant + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha2 + schema: + openAPIV3Schema: + description: "ReferenceGrant identifies kinds of resources in other namespaces + that are trusted to reference the specified kinds of resources in the same + namespace as the policy. \n Each ReferenceGrant can be used to represent + a unique trust relationship. Additional Reference Grants can be used to + add to the set of trusted sources of inbound references for the namespace + they are defined within. \n All cross-namespace references in Gateway API + (with the exception of cross-namespace Gateway-route attachment) require + a ReferenceGrant. \n Support: Core" + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of ReferenceGrant. + properties: + from: + description: "From describes the trusted namespaces and kinds that + can reference the resources described in \"To\". Each entry in this + list must be considered to be an additional place that references + can be valid from, or to put this another way, entries must be combined + using OR. \n Support: Core" + items: + description: ReferenceGrantFrom describes trusted namespaces and + kinds. + properties: + group: + description: "Group is the group of the referent. When empty, + the Kubernetes core API group is inferred. \n Support: Core" + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: "Kind is the kind of the referent. Although implementations + may support additional resources, the following types are + part of the \"Core\" support level for this field. \n When + used to permit a SecretObjectReference: \n * Gateway \n When + used to permit a BackendObjectReference: \n * HTTPRoute * + TCPRoute * TLSRoute * UDPRoute" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + namespace: + description: "Namespace is the namespace of the referent. \n + Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - group + - kind + - namespace + type: object + maxItems: 16 + minItems: 1 + type: array + to: + description: "To describes the resources that may be referenced by + the resources described in \"From\". Each entry in this list must + be considered to be an additional place that references can be valid + to, or to put this another way, entries must be combined using OR. + \n Support: Core" + items: + description: ReferenceGrantTo describes what Kinds are allowed as + targets of the references. + properties: + group: + description: "Group is the group of the referent. When empty, + the Kubernetes core API group is inferred. \n Support: Core" + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: "Kind is the kind of the referent. Although implementations + may support additional resources, the following types are + part of the \"Core\" support level for this field: \n * Secret + when used to permit a SecretObjectReference * Service when + used to permit a BackendObjectReference" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. When unspecified, + this policy refers to all resources of the specified Group + and Kind in the local namespace. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + type: object + maxItems: 16 + minItems: 1 + type: array + required: + - from + - to + type: object + type: object + served: true + storage: true + subresources: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/internal/provider/kubernetes/testdata/in/tlsroute-experimental-crd.yaml b/internal/provider/kubernetes/testdata/in/tlsroute-experimental-crd.yaml new file mode 100644 index 00000000000..fb30827b003 --- /dev/null +++ b/internal/provider/kubernetes/testdata/in/tlsroute-experimental-crd.yaml @@ -0,0 +1,542 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/1086 + gateway.networking.k8s.io/bundle-version: v0.6.0-dev + gateway.networking.k8s.io/channel: experimental + creationTimestamp: null + name: tlsroutes.gateway.networking.k8s.io +spec: + group: gateway.networking.k8s.io + names: + categories: + - gateway-api + kind: TLSRoute + listKind: TLSRouteList + plural: tlsroutes + singular: tlsroute + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha2 + schema: + openAPIV3Schema: + description: "The TLSRoute resource is similar to TCPRoute, but can be configured + to match against TLS-specific metadata. This allows more flexibility in + matching streams for a given TLS listener. \n If you need to forward traffic + to a single target for a TLS listener, you could choose to use a TCPRoute + with a TLS listener." + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of TLSRoute. + properties: + hostnames: + description: "Hostnames defines a set of SNI names that should match + against the SNI attribute of TLS ClientHello message in TLS handshake. + This matches the RFC 1123 definition of a hostname with 2 notable + exceptions: \n 1. IPs are not allowed in SNI names per RFC 6066. + 2. A hostname may be prefixed with a wildcard label (`*.`). The + wildcard label must appear by itself as the first label. \n If + a hostname is specified by both the Listener and TLSRoute, there + must be at least one intersecting hostname for the TLSRoute to be + attached to the Listener. For example: \n * A Listener with `test.example.com` + as the hostname matches TLSRoutes that have either not specified + any hostnames, or have specified at least one of `test.example.com` + or `*.example.com`. * A Listener with `*.example.com` as the hostname + matches TLSRoutes that have either not specified any hostnames + or have specified at least one hostname that matches the Listener + hostname. For example, `test.example.com` and `*.example.com` + would both match. On the other hand, `example.com` and `test.example.net` + would not match. \n If both the Listener and TLSRoute have specified + hostnames, any TLSRoute hostnames that do not match the Listener + hostname MUST be ignored. For example, if a Listener specified `*.example.com`, + and the TLSRoute specified `test.example.com` and `test.example.net`, + `test.example.net` must not be considered for a match. \n If both + the Listener and TLSRoute have specified hostnames, and none match + with the criteria above, then the TLSRoute is not accepted. The + implementation must raise an 'Accepted' Condition with a status + of `False` in the corresponding RouteParentStatus. \n Support: Core" + items: + description: "Hostname is the fully qualified domain name of a network + host. This matches the RFC 1123 definition of a hostname with + 2 notable exceptions: \n 1. IPs are not allowed. 2. A hostname + may be prefixed with a wildcard label (`*.`). The wildcard label + must appear by itself as the first label. \n Hostname can be \"precise\" + which is a domain name without the terminating dot of a network + host (e.g. \"foo.example.com\") or \"wildcard\", which is a domain + name prefixed with a single wildcard label (e.g. `*.example.com`). + \n Note that as per RFC1035 and RFC1123, a *label* must consist + of lower case alphanumeric characters or '-', and must start and + end with an alphanumeric character. No other punctuation is allowed." + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + maxItems: 16 + type: array + parentRefs: + description: "ParentRefs references the resources (usually Gateways) + that a Route wants to be attached to. Note that the referenced parent + resource needs to allow this for the attachment to be complete. + For Gateways, that means the Gateway needs to allow attachment from + Routes of this kind and namespace. \n The only kind of parent resource + with \"Core\" support is Gateway. This API may be extended in the + future to support additional kinds of parent resources such as one + of the route kinds. \n It is invalid to reference an identical parent + more than once. It is valid to reference multiple distinct sections + within the same parent resource, such as 2 Listeners within a Gateway. + \n It is possible to separately reference multiple distinct objects + that may be collapsed by an implementation. For example, some implementations + may choose to merge compatible Gateway Listeners together. If that + is the case, the list of routes attached to those resources should + also be merged." + items: + description: "ParentReference identifies an API object (usually + a Gateway) that can be considered a parent of this resource (usually + a route). The only kind of parent resource with \"Core\" support + is Gateway. This API may be extended in the future to support + additional kinds of parent resources, such as HTTPRoute. \n The + API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid." + properties: + group: + default: gateway.networking.k8s.io + description: "Group is the group of the referent. \n Support: + Core" + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: "Kind is kind of the referent. \n Support: Core + (Gateway) \n Support: Custom (Other Resources)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: "Name is the name of the referent. \n Support: + Core" + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the referent. When + unspecified, this refers to the local namespace of the Route. + \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: "Port is the network port this Route targets. It + can be interpreted differently based on the type of parent + resource. \n When the parent resource is a Gateway, this targets + all listeners listening on the specified port that also support + this kind of Route(and select this Route). It's not recommended + to set `Port` unless the networking behaviors specified in + a Route must apply to a specific port as opposed to a listener(s) + whose port(s) may be changed. When both Port and SectionName + are specified, the name and port of the selected listener + must match both specified values. \n Implementations MAY choose + to support other parent resources. Implementations supporting + other types of parent resources MUST clearly document how/if + Port is interpreted. \n For the purpose of status, an attachment + is considered successful as long as the parent resource accepts + it partially. For example, Gateway listeners can restrict + which Routes can attach to them by Route kind, namespace, + or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this + Route, the Route MUST be considered detached from the Gateway. + \n Support: Extended \n " + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: "SectionName is the name of a section within the + target resource. In the following resources, SectionName is + interpreted as the following: \n * Gateway: Listener Name. + When both Port (experimental) and SectionName are specified, + the name and port of the selected listener must match both + specified values. \n Implementations MAY choose to support + attaching Routes to other resources. If that is the case, + they MUST clearly document how SectionName is interpreted. + \n When unspecified (empty string), this will reference the + entire resource. For the purpose of status, an attachment + is considered successful if at least one section in the parent + resource accepts it. For example, Gateway listeners can restrict + which Routes can attach to them by Route kind, namespace, + or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this + Route, the Route MUST be considered detached from the Gateway. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + maxItems: 32 + type: array + rules: + description: Rules are a list of TLS matchers and actions. + items: + description: TLSRouteRule is the configuration for a given rule. + properties: + backendRefs: + description: "BackendRefs defines the backend(s) where matching + requests should be sent. If unspecified or invalid (refers + to a non-existent resource or a Service with no endpoints), + the rule performs no forwarding; if no filters are specified + that would result in a response being sent, the underlying + implementation must actively reject request attempts to this + backend, by rejecting the connection or returning a 500 status + code. Request rejections must respect weight; if an invalid + backend is requested to have 80% of requests, then 80% of + requests must be rejected instead. \n Support: Core for Kubernetes + Service \n Support: Custom for any other resource \n Support + for weight: Extended" + items: + description: "BackendRef defines how a Route should forward + a request to a Kubernetes resource. \n Note that when a + namespace is specified, a ReferenceGrant object is required + in the referent namespace to allow that namespace's owner + to accept the reference. See the ReferenceGrant documentation + for details." + properties: + group: + default: "" + description: Group is the group of the referent. For example, + "networking.k8s.io". When unspecified (empty string), + core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: Kind is kind of the referent. For example + "HTTPRoute" or "Service". Defaults to "Service" when + not specified. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the backend. + When unspecified, the local namespace is inferred. \n + Note that when a 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. \n Support: + Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: Port specifies the destination port number + to use for this resource. Port is required when the + referent is a Kubernetes Service. In this case, the + port number is the service port number, not the target + port. For other resources, destination port might be + derived from the referent resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + weight: + default: 1 + description: "Weight specifies the proportion of requests + forwarded to the referenced backend. This is computed + as weight/(sum of all weights in this BackendRefs list). + For non-zero values, there may be some epsilon from + the exact proportion defined here depending on the precision + an implementation supports. Weight is not a percentage + and the sum of weights does not need to equal 100. \n + If only one backend is specified and it has a weight + greater than 0, 100% of the traffic is forwarded to + that backend. If weight is set to 0, no traffic should + be forwarded for this entry. If unspecified, weight + defaults to 1. \n Support for this field varies based + on the context where used." + format: int32 + maximum: 1000000 + minimum: 0 + type: integer + required: + - name + type: object + maxItems: 16 + minItems: 1 + type: array + type: object + maxItems: 16 + minItems: 1 + type: array + required: + - rules + type: object + status: + description: Status defines the current state of TLSRoute. + properties: + parents: + description: "Parents is a list of parent resources (usually Gateways) + that are associated with the route, and the status of the route + with respect to each parent. When this route attaches to a parent, + the controller that manages the parent must add an entry to this + list when the controller first sees the route and should update + the entry as appropriate when the route or gateway is modified. + \n Note that parent references that cannot be resolved by an implementation + of this API will not be added to this list. Implementations of this + API can only populate Route status for the Gateways/parent resources + they are responsible for. \n A maximum of 32 Gateways will be represented + in this list. An empty list means the route has not been attached + to any Gateway." + items: + description: RouteParentStatus describes the status of a route with + respect to an associated Parent. + properties: + conditions: + description: "Conditions describes the status of the route with + respect to the Gateway. Note that the route's availability + is also subject to the Gateway's own status conditions and + listener status. \n If the Route's ParentRef specifies an + existing Gateway that supports Routes of this kind AND that + Gateway's controller has sufficient access, then that Gateway's + controller MUST set the \"Accepted\" condition on the Route, + to indicate whether the route has been accepted or rejected + by the Gateway, and why. \n A Route MUST be considered \"Accepted\" + if at least one of the Route's rules is implemented by the + Gateway. \n There are a number of cases where the \"Accepted\" + condition may not be set due to lack of controller visibility, + that includes when: \n * The Route refers to a non-existent + parent. * The Route is of a type that the controller does + not support. * The Route is in a namespace the controller + does not have access to." + items: + description: "Condition contains details for one aspect of + the current state of this API Resource. --- This struct + is intended for direct use as an array at the field path + .status.conditions. For example, type FooStatus struct{ + \ // Represents the observations of a foo's current state. + \ // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type + \ // +patchStrategy=merge // +listType=map // + +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` + \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should + be when the underlying condition changed. If that is + not known, then using the time when the API field changed + is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, + if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the + current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier + indicating the reason for the condition's last transition. + Producers of specific condition types may define expected + values and meanings for this field, and whether the + values are considered a guaranteed API. The value should + be a CamelCase string. This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across + resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability + to deconflict is important. The regex it matches is + (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: "ControllerName is a domain/path string that indicates + the name of the controller that wrote this status. This corresponds + with the controllerName field on GatewayClass. \n Example: + \"example.net/gateway-controller\". \n The format of this + field is DOMAIN \"/\" PATH, where DOMAIN and PATH are valid + Kubernetes names (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + \n Controllers MUST populate this field when writing status. + Controllers should ensure that entries to status populated + with their ControllerName are cleaned up when they are no + longer necessary." + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + parentRef: + description: ParentRef corresponds with a ParentRef in the spec + that this RouteParentStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: "Group is the group of the referent. \n Support: + Core" + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: "Kind is kind of the referent. \n Support: + Core (Gateway) \n Support: Custom (Other Resources)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: "Name is the name of the referent. \n Support: + Core" + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the referent. + When unspecified, this refers to the local namespace of + the Route. \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: "Port is the network port this Route targets. + It can be interpreted differently based on the type of + parent resource. \n When the parent resource is a Gateway, + this targets all listeners listening on the specified + port that also support this kind of Route(and select this + Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to + a specific port as opposed to a listener(s) whose port(s) + may be changed. When both Port and SectionName are specified, + the name and port of the selected listener must match + both specified values. \n Implementations MAY choose to + support other parent resources. Implementations supporting + other types of parent resources MUST clearly document + how/if Port is interpreted. \n For the purpose of status, + an attachment is considered successful as long as the + parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them + by Route kind, namespace, or hostname. If 1 of 2 Gateway + listeners accept attachment from the referencing Route, + the Route MUST be considered successfully attached. If + no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + \n Support: Extended \n " + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: "SectionName is the name of a section within + the target resource. In the following resources, SectionName + is interpreted as the following: \n * Gateway: Listener + Name. When both Port (experimental) and SectionName are + specified, the name and port of the selected listener + must match both specified values. \n Implementations MAY + choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName + is interpreted. \n When unspecified (empty string), this + will reference the entire resource. For the purpose of + status, an attachment is considered successful if at least + one section in the parent resource accepts it. For example, + Gateway listeners can restrict which Routes can attach + to them by Route kind, namespace, or hostname. If 1 of + 2 Gateway listeners accept attachment from the referencing + Route, the Route MUST be considered successfully attached. + If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + required: + - controllerName + - parentRef + type: object + maxItems: 32 + type: array + required: + - parents + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/internal/provider/kubernetes/tlsroute.go b/internal/provider/kubernetes/tlsroute.go new file mode 100644 index 00000000000..9cd3db39e9c --- /dev/null +++ b/internal/provider/kubernetes/tlsroute.go @@ -0,0 +1,353 @@ +// 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. + +// This file contains code derived from Contour, +// https://github.com/projectcontour/contour +// from the source file +// https://github.com/projectcontour/contour/blob/main/internal/controller/tlsroute.go +// and is provided here subject to the following: +// Copyright Project Contour Authors +// SPDX-License-Identifier: Apache-2.0 + +package kubernetes + +import ( + "context" + "fmt" + + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" + + "github.com/envoyproxy/gateway/internal/envoygateway/config" + "github.com/envoyproxy/gateway/internal/gatewayapi" + "github.com/envoyproxy/gateway/internal/message" + "github.com/envoyproxy/gateway/internal/provider/utils" + "github.com/envoyproxy/gateway/internal/status" +) + +const ( + kindTLSRoute = "TLSRoute" + + serviceTLSRouteIndex = "serviceTLSRouteBackendRef" +) + +type tlsRouteReconciler struct { + client client.Client + log logr.Logger + statusUpdater status.Updater + classController gwapiv1b1.GatewayController + + resources *message.ProviderResources + referenceStore *providerReferenceStore +} + +// newTLSRouteController creates the tlsroute controller from mgr. The controller will be pre-configured +// to watch for TLSRoute objects across all namespaces. +func newTLSRouteController(mgr manager.Manager, cfg *config.Server, su status.Updater, resources *message.ProviderResources, referenceStore *providerReferenceStore) error { + r := &tlsRouteReconciler{ + client: mgr.GetClient(), + log: cfg.Logger, + classController: gwapiv1b1.GatewayController(cfg.EnvoyGateway.Gateway.ControllerName), + statusUpdater: su, + resources: resources, + referenceStore: referenceStore, + } + + c, err := controller.New("tlsroute", mgr, controller.Options{Reconciler: r}) + if err != nil { + return err + } + r.log.Info("created tlsroute controller") + + if err := c.Watch( + &source.Kind{Type: &gwapiv1a2.TLSRoute{}}, + &handler.EnqueueRequestForObject{}, + ); err != nil { + return err + } + + // Subscribe to status updates + go r.subscribeAndUpdateStatus(context.Background()) + + // Add indexing on TLSRoute, for Service objects that are referenced in TLSRoute objects + // via `.spec.rules.backendRefs`. This helps in querying for TLSRoutes that are affected by + // a particular Service CRUD. + if err := mgr.GetFieldIndexer().IndexField(context.Background(), &gwapiv1a2.TLSRoute{}, serviceTLSRouteIndex, func(rawObj client.Object) []string { + tlsRoute := rawObj.(*gwapiv1a2.TLSRoute) + var backendServices []string + for _, rule := range tlsRoute.Spec.Rules { + for _, backend := range rule.BackendRefs { + if string(*backend.Kind) == gatewayapi.KindService { + // If an explicit Service namespace is not provided, use the TLSRoute namespace to + // lookup the provided Service Name. + backendServices = append(backendServices, + types.NamespacedName{ + Namespace: gatewayapi.NamespaceDerefOrAlpha(backend.Namespace, tlsRoute.Namespace), + Name: string(backend.Name), + }.String(), + ) + } + } + } + return backendServices + }); err != nil { + return err + } + + // Watch Gateway CRUDs and reconcile affected TLSRoutes. + if err := c.Watch( + &source.Kind{Type: &gwapiv1b1.Gateway{}}, + handler.EnqueueRequestsFromMapFunc(r.getTLSRoutesForGateway), + ); err != nil { + return err + } + + // Watch Service CRUDs and reconcile affected TLSRoutes. + if err := c.Watch( + &source.Kind{Type: &corev1.Service{}}, + handler.EnqueueRequestsFromMapFunc(r.getTLSRoutesForService), + ); err != nil { + return err + } + + r.log.Info("watching tlsroute objects") + return nil +} + +// getTLSRoutesForGateway uses a Gateway obj to fetch TLSRoutes, iterating +// through them and creating a reconciliation request for each valid TLSRoute +// that references obj. +func (r *tlsRouteReconciler) getTLSRoutesForGateway(obj client.Object) []reconcile.Request { + ctx := context.Background() + + gw, ok := obj.(*gwapiv1b1.Gateway) + if !ok { + r.log.Info("unexpected object type, bypassing reconciliation", "object", obj) + return []reconcile.Request{} + } + + routes := &gwapiv1a2.TLSRouteList{} + if err := r.client.List(ctx, routes); err != nil { + return []reconcile.Request{} + } + + requests := []reconcile.Request{} + for i := range routes.Items { + route := routes.Items[i] + gateways, err := validateParentRefs(ctx, r.client, route.Namespace, r.classController, gatewayapi.UpgradeParentReferences(route.Spec.ParentRefs)) + if err != nil { + r.log.Info("invalid parentRefs for tlsroute, bypassing reconciliation", "object", obj) + continue + } + for j := range gateways { + if gateways[j].Namespace == gw.Namespace && gateways[j].Name == gw.Name { + req := reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: route.Namespace, + Name: route.Name, + }, + } + requests = append(requests, req) + break + } + } + } + + return requests +} + +// getTLSRoutesForService uses a Service obj to fetch TLSRoutes that references +// the Service using `.spec.rules.backendRefs`. The affected TLSRoutes are then +// pushed for reconciliation. +func (r *tlsRouteReconciler) getTLSRoutesForService(obj client.Object) []reconcile.Request { + affectedTLSRouteList := &gwapiv1a2.TLSRouteList{} + + if err := r.client.List(context.Background(), affectedTLSRouteList, &client.ListOptions{ + FieldSelector: fields.OneTermEqualSelector(serviceTLSRouteIndex, utils.NamespacedName(obj).String()), + }); err != nil { + return []reconcile.Request{} + } + + requests := make([]reconcile.Request, len(affectedTLSRouteList.Items)) + for i, item := range affectedTLSRouteList.Items { + requests[i] = reconcile.Request{ + NamespacedName: utils.NamespacedName(item.DeepCopy()), + } + } + + return requests +} + +func (r *tlsRouteReconciler) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { + log := r.log.WithValues("namespace", request.Namespace, "name", request.Name) + + log.Info("reconciling tlsroute") + + // Fetch all TLSRoutes from the cache. + routeList := &gwapiv1a2.TLSRouteList{} + if err := r.client.List(ctx, routeList); err != nil { + return reconcile.Result{}, fmt.Errorf("error listing tlsroutes") + } + + found := false + for i := range routeList.Items { + // See if this route from the list matched the reconciled route. + route := routeList.Items[i] + routeKey := utils.NamespacedName(&route) + if routeKey == request.NamespacedName { + found = true + } + + // Store the tlsroute in the resource map. + r.resources.TLSRoutes.Store(routeKey, &route) + log.Info("added tlsroute to resource map") + + // Get the route's namespace from the cache. + nsKey := types.NamespacedName{Name: route.Namespace} + ns := new(corev1.Namespace) + if err := r.client.Get(ctx, nsKey, ns); err != nil { + if errors.IsNotFound(err) { + // The route's namespace doesn't exist in the cache, so remove it from + // the namespace resource map if it exists. + if _, ok := r.resources.Namespaces.Load(nsKey.Name); ok { + r.resources.Namespaces.Delete(nsKey.Name) + log.Info("deleted namespace from resource map") + } + } + return reconcile.Result{}, fmt.Errorf("failed to get namespace %s", nsKey.Name) + } + + // The route's namespace exists, so add it to the resource map. + r.resources.Namespaces.Store(nsKey.Name, ns) + log.Info("added namespace to resource map") + + // Get the route's backendRefs from the cache. Note that a Service is the + // only supported kind. + for i := range route.Spec.Rules { + for j := range route.Spec.Rules[i].BackendRefs { + ref := route.Spec.Rules[i].BackendRefs[j] + if err := validateTLSRouteBackendRef(&ref); err != nil { + return reconcile.Result{}, fmt.Errorf("invalid backendRef: %w", err) + } + + // The backendRef is valid, so get the referenced service from the cache. + svcKey := types.NamespacedName{Namespace: route.Namespace, Name: string(ref.Name)} + svc := new(corev1.Service) + if err := r.client.Get(ctx, svcKey, svc); err != nil { + if errors.IsNotFound(err) { + // The ref's service doesn't exist in the cache, so remove it from + // the resource map if it exists. + if _, ok := r.resources.Services.Load(svcKey); ok { + r.resources.Services.Delete(svcKey) + r.referenceStore.removeRouteToServicesMapping( + ObjectKindNamespacedName{kindTLSRoute, route.Namespace, route.Name}, + svcKey, + ) + log.Info("deleted service from resource map") + } + } + return reconcile.Result{}, fmt.Errorf("failed to get service %s/%s", + svcKey.Namespace, svcKey.Name) + } + + // The backendRef Service exists, so add it to the resource map. + r.resources.Services.Store(svcKey, svc) + r.referenceStore.updateRouteToServicesMapping( + ObjectKindNamespacedName{kindTLSRoute, route.Namespace, route.Name}, + svcKey, + ) + log.Info("added service to resource map") + } + } + } + + if !found { + // Delete the tlsroute from the resource map. + r.resources.TLSRoutes.Delete(request.NamespacedName) + log.Info("deleted tlsroute from resource map") + + // Delete the Namespace from the resource maps if no other + // routes (TLSRoute/HTTPRoute) exist in the namespace. + if found, err := isRoutePresentInNamespace(ctx, r.client, request.NamespacedName.Namespace); err != nil { + return reconcile.Result{}, err + } else if !found { + r.resources.Namespaces.Delete(request.Namespace) + log.Info("deleted namespace from resource map") + } + + // Delete the Service from the resource maps if no other + // routes (TLSRoute or HTTPRoute) reference that Service. + routeServices := r.referenceStore.getRouteToServicesMapping(ObjectKindNamespacedName{kindTLSRoute, request.Namespace, request.Name}) + for svc := range routeServices { + r.referenceStore.removeRouteToServicesMapping(ObjectKindNamespacedName{kindTLSRoute, request.Namespace, request.Name}, svc) + if !r.referenceStore.isServiceReferredByRoutes(svc) { + r.resources.Services.Delete(svc) + log.Info("deleted service from resource map", "namespace", svc.Namespace, "name", svc.Name) + } + } + } + + log.Info("reconciled tlsroute") + + return reconcile.Result{}, nil +} + +// validateTLSRouteBackendRef validates that ref is a reference to a local Service. +func validateTLSRouteBackendRef(ref *gwapiv1a2.BackendRef) error { + switch { + case ref == nil: + return nil + case ref.Group != nil && *ref.Group != corev1.GroupName: + return fmt.Errorf("invalid group; must be nil or empty string") + case ref.Kind != nil && *ref.Kind != gatewayapi.KindService: + return fmt.Errorf("invalid kind %q; must be %q", + *ref.BackendObjectReference.Kind, gatewayapi.KindService) + case ref.Namespace != nil: + return fmt.Errorf("invalid namespace; must be nil") + } + + return nil +} + +// subscribeAndUpdateStatus subscribes to tlsroute status updates and writes it into the +// Kubernetes API Server +func (r *tlsRouteReconciler) subscribeAndUpdateStatus(ctx context.Context) { + // Subscribe to resources + message.HandleSubscription(r.resources.TLSRouteStatuses.Subscribe(ctx), + func(update message.Update[types.NamespacedName, *gwapiv1a2.TLSRoute]) { + // skip delete updates. + if update.Delete { + return + } + key := update.Key + val := update.Value + r.statusUpdater.Send(status.Update{ + NamespacedName: key, + Resource: new(gwapiv1a2.TLSRoute), + Mutator: status.MutatorFunc(func(obj client.Object) client.Object { + t, ok := obj.(*gwapiv1a2.TLSRoute) + if !ok { + panic(fmt.Sprintf("unsupported object type %T", obj)) + } + tCopy := t.DeepCopy() + tCopy.Status.Parents = val.Status.Parents + return tCopy + }), + }) + }, + ) + r.log.Info("status subscriber shutting down") +} diff --git a/internal/provider/runner/runner.go b/internal/provider/runner/runner.go index b8678eb0edd..251acc50e9a 100644 --- a/internal/provider/runner/runner.go +++ b/internal/provider/runner/runner.go @@ -1,3 +1,8 @@ +// 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 ( diff --git a/internal/provider/runner/runner_test.go b/internal/provider/runner/runner_test.go index 8411e7664ed..170df8182e0 100644 --- a/internal/provider/runner/runner_test.go +++ b/internal/provider/runner/runner_test.go @@ -1,11 +1,16 @@ +// 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" "testing" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - ctrl "sigs.k8s.io/controller-runtime" "github.com/envoyproxy/gateway/api/config/v1alpha1" "github.com/envoyproxy/gateway/internal/envoygateway/config" @@ -51,7 +56,8 @@ func TestStart(t *testing.T) { ProviderResources: new(message.ProviderResources), }, } - ctx := ctrl.SetupSignalHandler() + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) err := runner.Start(ctx) if tc.expect { require.NoError(t, err) diff --git a/internal/provider/utils/utils.go b/internal/provider/utils/utils.go index 715e5588bce..353e3ae08d8 100644 --- a/internal/provider/utils/utils.go +++ b/internal/provider/utils/utils.go @@ -1,6 +1,15 @@ +// 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 utils import ( + "crypto/sha256" + "fmt" + "strings" + "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -12,3 +21,16 @@ func NamespacedName(obj client.Object) types.NamespacedName { Name: obj.GetName(), } } + +// Returns a partially hashed name for the string including up to 48 characters of the original name before the hash +func GetHashedName(name string) string { + + h := sha256.New() // Using sha256 instead of sha1 due to Blocklisted import crypto/sha1: weak cryptographic primitive (gosec) + hsha := h.Sum([]byte(name)) + hashedName := strings.ToLower(fmt.Sprintf("%x", hsha)) + + if len(name) > 48 { + return fmt.Sprintf("%s-%s", name[0:48], hashedName[0:8]) + } + return fmt.Sprintf("%s-%s", name, hashedName[0:8]) +} diff --git a/internal/status/conditions.go b/internal/status/conditions.go index 03dcffcc4a0..f83873fe6c8 100644 --- a/internal/status/conditions.go +++ b/internal/status/conditions.go @@ -1,5 +1,15 @@ -// Portions of this code are based on code from Contour, available at: +// 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. + +// This file contains code derived from Contour, +// https://github.com/projectcontour/contour +// from the source file // https://github.com/projectcontour/contour/blob/main/internal/status/gatewayclassconditions.go +// and is provided here subject to the following: +// Copyright Project Contour Authors +// SPDX-License-Identifier: Apache-2.0 package status diff --git a/internal/status/conditions_test.go b/internal/status/conditions_test.go index 6cc8a52cf99..ba2f2cd21f3 100644 --- a/internal/status/conditions_test.go +++ b/internal/status/conditions_test.go @@ -1,5 +1,15 @@ -// Portions of this code are based on code from Contour, available at: +// 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. + +// This file contains code derived from Contour, +// https://github.com/projectcontour/contour +// from the source file // https://github.com/projectcontour/contour/blob/main/internal/status/gatewayclassconditions_test.go +// and is provided here subject to the following: +// Copyright Project Contour Authors +// SPDX-License-Identifier: Apache-2.0 package status diff --git a/internal/status/gateway.go b/internal/status/gateway.go index feb8ba73182..d802d09e7b8 100644 --- a/internal/status/gateway.go +++ b/internal/status/gateway.go @@ -1,3 +1,8 @@ +// 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 status import ( diff --git a/internal/status/gatewayclass.go b/internal/status/gatewayclass.go index 41581b22904..2853c258a58 100644 --- a/internal/status/gatewayclass.go +++ b/internal/status/gatewayclass.go @@ -1,5 +1,15 @@ -// Portions of this code are based on code from Contour, available at: +// 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. + +// This file contains code derived from Contour, +// https://github.com/projectcontour/contour +// from the source file // https://github.com/projectcontour/contour/blob/main/internal/status/gatewayclass.go +// and is provided here subject to the following: +// Copyright Project Contour Authors +// SPDX-License-Identifier: Apache-2.0 package status diff --git a/internal/status/status.go b/internal/status/status.go index c58a488c0bc..58dca4b576b 100644 --- a/internal/status/status.go +++ b/internal/status/status.go @@ -1,5 +1,15 @@ -// Portions of this code are based on code from Contour, available at: +// 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. + +// This file contains code derived from Contour, +// https://github.com/projectcontour/contour +// from the source file // https://github.com/projectcontour/contour/blob/main/internal/k8s/status.go +// and is provided here subject to the following: +// Copyright Project Contour Authors +// SPDX-License-Identifier: Apache-2.0 package status @@ -13,6 +23,7 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/retry" "sigs.k8s.io/controller-runtime/pkg/client" + gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" ) @@ -143,6 +154,7 @@ func (u *UpdateWriter) Send(update Update) { // GatewayClasses // Gateway // HTTPRoute +// TLSRoute func isStatusEqual(objA, objB interface{}) bool { opts := cmpopts.IgnoreFields(metav1.Condition{}, "LastTransitionTime", "ObservedGeneration") switch a := objA.(type) { @@ -164,6 +176,12 @@ func isStatusEqual(objA, objB interface{}) bool { return true } } + case *gwapiv1a2.TLSRoute: + if b, ok := objB.(*gwapiv1a2.TLSRoute); ok { + if cmp.Equal(a.Status, b.Status, opts) { + return true + } + } } return false } diff --git a/internal/utils/env/env.go b/internal/utils/env/env.go index 3a91d4b158b..da55be76b83 100644 --- a/internal/utils/env/env.go +++ b/internal/utils/env/env.go @@ -1,3 +1,8 @@ +// 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 env import ( diff --git a/internal/utils/env/env_test.go b/internal/utils/env/env_test.go index 118d2e8108e..357b69ef4d6 100644 --- a/internal/utils/env/env_test.go +++ b/internal/utils/env/env_test.go @@ -1,3 +1,8 @@ +// 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 env import ( diff --git a/internal/utils/slice/slice.go b/internal/utils/slice/slice.go index ad88dfd32da..cc75f9f0e59 100644 --- a/internal/utils/slice/slice.go +++ b/internal/utils/slice/slice.go @@ -1,3 +1,8 @@ +// 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 slice // ContainsString checks if a given slice of strings contains the provided string. diff --git a/internal/utils/slice/slice_test.go b/internal/utils/slice/slice_test.go index 2ac94bab8c6..8b76632e972 100644 --- a/internal/utils/slice/slice_test.go +++ b/internal/utils/slice/slice_test.go @@ -1,3 +1,8 @@ +// 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 slice import ( diff --git a/internal/xds/cache/logrwrapper.go b/internal/xds/cache/logrwrapper.go index 5c1f0732840..a21324e4ba1 100644 --- a/internal/xds/cache/logrwrapper.go +++ b/internal/xds/cache/logrwrapper.go @@ -1,3 +1,8 @@ +// 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 cache import ( diff --git a/internal/xds/cache/snapshotcache.go b/internal/xds/cache/snapshotcache.go index 82b41e29c90..255bbc591eb 100644 --- a/internal/xds/cache/snapshotcache.go +++ b/internal/xds/cache/snapshotcache.go @@ -1,5 +1,15 @@ -// Portions of this code are based on code from Contour, available at: +// 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. + +// This file contains code derived from Contour, +// https://github.com/projectcontour/contour +// from the source file // https://github.com/projectcontour/contour/blob/main/internal/xds/v3/snapshotter.go +// and is provided here subject to the following: +// Copyright Project Contour Authors +// SPDX-License-Identifier: Apache-2.0 package cache diff --git a/internal/xds/server/runner/runner.go b/internal/xds/server/runner/runner.go index 5c3a9988976..42b65bd44f1 100644 --- a/internal/xds/server/runner/runner.go +++ b/internal/xds/server/runner/runner.go @@ -1,3 +1,8 @@ +// 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 ( @@ -16,6 +21,7 @@ import ( "github.com/envoyproxy/gateway/internal/envoygateway/config" "github.com/envoyproxy/gateway/internal/message" "github.com/envoyproxy/gateway/internal/xds/cache" + xdstypes "github.com/envoyproxy/gateway/internal/xds/types" controlplane_service_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/service/cluster/v3" controlplane_service_discovery_v3 "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" controlplane_service_endpoint_v3 "github.com/envoyproxy/go-control-plane/envoy/service/endpoint/v3" @@ -112,10 +118,8 @@ func registerServer(srv controlplane_server_v3.Server, g *grpc.Server) { func (r *Runner) subscribeAndTranslate(ctx context.Context) { // Subscribe to resources - for snapshot := range r.Xds.Subscribe(ctx) { - r.Logger.Info("received a notification") - // Load all resources required for translation - for _, update := range snapshot.Updates { + message.HandleSubscription(r.Xds.Subscribe(ctx), + func(update message.Update[string, *xdstypes.ResourceVersionTable]) { key := update.Key val := update.Value @@ -129,8 +133,8 @@ func (r *Runner) subscribeAndTranslate(ctx context.Context) { if err != nil { r.Logger.Error(err, "failed to generate a snapshot") } - } - } + }, + ) r.Logger.Info("subscriber shutting down") diff --git a/internal/xds/server/runner/runner_test.go b/internal/xds/server/runner/runner_test.go index bcb62c24429..3d0a86c58de 100644 --- a/internal/xds/server/runner/runner_test.go +++ b/internal/xds/server/runner/runner_test.go @@ -1,3 +1,8 @@ +// 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 ( diff --git a/internal/xds/translator/accesslog.go b/internal/xds/translator/accesslog.go new file mode 100644 index 00000000000..82b6ef95b05 --- /dev/null +++ b/internal/xds/translator/accesslog.go @@ -0,0 +1,24 @@ +// 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 translator + +import ( + accesslog "github.com/envoyproxy/go-control-plane/envoy/config/accesslog/v3" + fileaccesslog "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/file/v3" +) + +var ( + stdoutFileAccessLog = &fileaccesslog.FileAccessLog{ + Path: "/dev/stdout", + } + + // for the case when a route does not exist to upstream, hcm logs will not be present + listenerAccessLogFilter = &accesslog.AccessLogFilter{ + FilterSpecifier: &accesslog.AccessLogFilter_ResponseFlagFilter{ + ResponseFlagFilter: &accesslog.ResponseFlagFilter{Flags: []string{"NR"}}, + }, + } +) diff --git a/internal/xds/translator/cluster.go b/internal/xds/translator/cluster.go index 2a1dff9b309..df91a5df124 100644 --- a/internal/xds/translator/cluster.go +++ b/internal/xds/translator/cluster.go @@ -1,3 +1,8 @@ +// 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 translator import ( @@ -12,19 +17,19 @@ import ( "github.com/envoyproxy/gateway/internal/ir" ) -func buildXdsCluster(httpRoute *ir.HTTPRoute) (*cluster.Cluster, error) { +func buildXdsCluster(routeName string, destinations []*ir.RouteDestination, isHTTP2 bool) (*cluster.Cluster, error) { localities := make([]*endpoint.LocalityLbEndpoints, 0, 1) locality := &endpoint.LocalityLbEndpoints{ Locality: &core.Locality{}, - LbEndpoints: buildXdsEndpoints(httpRoute.Destinations), + LbEndpoints: buildXdsEndpoints(destinations), Priority: 0, // Each locality gets the same weight 1. There is a single locality // per priority, so the weight value does not really matter, but some // load balancers need the value to be set. LoadBalancingWeight: &wrapperspb.UInt32Value{Value: 1}} localities = append(localities, locality) - clusterName := getXdsClusterName(httpRoute.Name) - return &cluster.Cluster{ + clusterName := routeName + cluster := &cluster.Cluster{ Name: clusterName, ConnectTimeout: durationpb.New(5 * time.Second), ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_STATIC}, @@ -35,7 +40,13 @@ func buildXdsCluster(httpRoute *ir.HTTPRoute) (*cluster.Cluster, error) { LocalityConfigSpecifier: &cluster.Cluster_CommonLbConfig_LocalityWeightedLbConfig_{ LocalityWeightedLbConfig: &cluster.Cluster_CommonLbConfig_LocalityWeightedLbConfig{}}}, OutlierDetection: &cluster.OutlierDetection{}, - }, nil + } + + if isHTTP2 { + cluster.Http2ProtocolOptions = &core.Http2ProtocolOptions{} + } + + return cluster, nil } diff --git a/internal/xds/translator/listener.go b/internal/xds/translator/listener.go index baca1aae39b..7d63575ddf5 100644 --- a/internal/xds/translator/listener.go +++ b/internal/xds/translator/listener.go @@ -1,12 +1,23 @@ +// 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 translator import ( "errors" + xdscore "github.com/cncf/xds/go/xds/core/v3" + matcher "github.com/cncf/xds/go/xds/type/matcher/v3" + accesslog "github.com/envoyproxy/go-control-plane/envoy/config/accesslog/v3" core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" router "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/v3" + tls_inspector "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/tls_inspector/v3" hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" + tcp "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3" + udp "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/udp/udp_proxy/v3" tls "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" "github.com/envoyproxy/go-control-plane/pkg/wellknown" "google.golang.org/protobuf/types/known/anypb" @@ -14,25 +25,63 @@ import ( "github.com/envoyproxy/gateway/internal/ir" ) -func buildXdsListener(httpListener *ir.HTTPListener) (*listener.Listener, error) { - if httpListener == nil { - return nil, errors.New("http listener is nil") +func buildXdsTCPListener(name, address string, port uint32) *listener.Listener { + accesslogAny, _ := anypb.New(stdoutFileAccessLog) + return &listener.Listener{ + Name: name, + AccessLog: []*accesslog.AccessLog{ + { + Name: wellknown.FileAccessLog, + ConfigType: &accesslog.AccessLog_TypedConfig{TypedConfig: accesslogAny}, + Filter: listenerAccessLogFilter, + }, + }, + Address: &core.Address{ + Address: &core.Address_SocketAddress{ + SocketAddress: &core.SocketAddress{ + Protocol: core.SocketAddress_TCP, + Address: address, + PortSpecifier: &core.SocketAddress_PortValue{ + PortValue: port, + }, + }, + }, + }, } +} +func addXdsHTTPFilterChain(xdsListener *listener.Listener, irListener *ir.HTTPListener) error { routerAny, err := anypb.New(&router.Router{}) if err != nil { - return nil, err + return err + } + + accesslogAny, err := anypb.New(stdoutFileAccessLog) + if err != nil { + return err } // HTTP filter configuration + var statPrefix string + if irListener.TLS != nil { + statPrefix = "https" + } else { + statPrefix = "http" + } mgr := &hcm.HttpConnectionManager{ + AccessLog: []*accesslog.AccessLog{ + { + Name: wellknown.FileAccessLog, + ConfigType: &accesslog.AccessLog_TypedConfig{TypedConfig: accesslogAny}, + }, + }, CodecType: hcm.HttpConnectionManager_AUTO, - StatPrefix: "http", + StatPrefix: statPrefix, RouteSpecifier: &hcm.HttpConnectionManager_Rds{ Rds: &hcm.Rds{ ConfigSource: makeConfigSource(), // Configure route name to be found via RDS. - RouteConfigName: getXdsRouteName(httpListener.Name), + RouteConfigName: irListener.Name, }, }, // Use only router. @@ -44,31 +93,157 @@ func buildXdsListener(httpListener *ir.HTTPListener) (*listener.Listener, error) mgrAny, err := anypb.New(mgr) if err != nil { - return nil, err + return err } - return &listener.Listener{ - Name: getXdsListenerName(httpListener.Name, httpListener.Port), - Address: &core.Address{ - Address: &core.Address_SocketAddress{ - SocketAddress: &core.SocketAddress{ - Protocol: core.SocketAddress_TCP, - Address: httpListener.Address, - PortSpecifier: &core.SocketAddress_PortValue{ - PortValue: httpListener.Port, - }, - }, + filterChain := &listener.FilterChain{ + Filters: []*listener.Filter{{ + Name: wellknown.HTTPConnectionManager, + ConfigType: &listener.Filter_TypedConfig{ + TypedConfig: mgrAny, + }, + }}, + } + + if irListener.TLS != nil { + tSocket, err := buildXdsDownstreamTLSSocket(irListener.Name, irListener.TLS) + if err != nil { + return err + } + filterChain.TransportSocket = tSocket + if err := addServerNamesMatch(xdsListener, filterChain, irListener.Hostnames); err != nil { + return err + } + + xdsListener.FilterChains = append(xdsListener.FilterChains, filterChain) + } else { + // Add the HTTP filter chain as the default filter chain + // Make sure one does not exist + if xdsListener.DefaultFilterChain != nil { + return errors.New("default filter chain already exists") + } + xdsListener.DefaultFilterChain = filterChain + } + + return nil +} + +func addServerNamesMatch(xdsListener *listener.Listener, filterChain *listener.FilterChain, hostnames []string) error { + // Dont add a filter chain match if the hostname is a wildcard character. + if len(hostnames) > 0 && hostnames[0] != "*" { + filterChain.FilterChainMatch = &listener.FilterChainMatch{ + ServerNames: hostnames, + } + + if err := addXdsTLSInspectorFilter(xdsListener); err != nil { + return err + } + } + + return nil +} + +// findXdsHTTPRouteConfigName finds the name of the route config associated with the +// http connection manager within the default filter chain and returns an empty string if +// not found. +func findXdsHTTPRouteConfigName(xdsListener *listener.Listener) string { + if xdsListener == nil || xdsListener.DefaultFilterChain == nil || xdsListener.DefaultFilterChain.Filters == nil { + return "" + } + + for _, filter := range xdsListener.DefaultFilterChain.Filters { + if filter.Name == wellknown.HTTPConnectionManager { + m := new(hcm.HttpConnectionManager) + if err := filter.GetTypedConfig().UnmarshalTo(m); err != nil { + return "" + } + rds := m.GetRds() + if rds == nil { + return "" + } + return rds.GetRouteConfigName() + } + } + return "" +} + +func addXdsTCPFilterChain(xdsListener *listener.Listener, irListener *ir.TCPListener, clusterName string) error { + if irListener == nil { + return errors.New("tcp listener is nil") + } + + statPrefix := "tcp" + if irListener.TLS != nil { + statPrefix = "passthrough" + } + + accesslogAny, err := anypb.New(stdoutFileAccessLog) + if err != nil { + return err + } + + mgr := &tcp.TcpProxy{ + AccessLog: []*accesslog.AccessLog{ + { + Name: wellknown.FileAccessLog, + ConfigType: &accesslog.AccessLog_TypedConfig{TypedConfig: accesslogAny}, }, }, - FilterChains: []*listener.FilterChain{{ - Filters: []*listener.Filter{{ - Name: wellknown.HTTPConnectionManager, - ConfigType: &listener.Filter_TypedConfig{ - TypedConfig: mgrAny, - }, - }}, + StatPrefix: statPrefix, + ClusterSpecifier: &tcp.TcpProxy_Cluster{ + Cluster: clusterName, + }, + } + mgrAny, err := anypb.New(mgr) + if err != nil { + return err + } + + filterChain := &listener.FilterChain{ + Filters: []*listener.Filter{{ + Name: wellknown.TCPProxy, + ConfigType: &listener.Filter_TypedConfig{ + TypedConfig: mgrAny, + }, }}, - }, nil + } + + if irListener.TLS != nil { + if err := addServerNamesMatch(xdsListener, filterChain, irListener.TLS.SNIs); err != nil { + return err + } + } + + xdsListener.FilterChains = append(xdsListener.FilterChains, filterChain) + + return nil +} + +// addXdsTLSInspectorFilter adds a Tls Inspector filter if it does not yet exist. +func addXdsTLSInspectorFilter(xdsListener *listener.Listener) error { + // Return early if it exists + for _, filter := range xdsListener.ListenerFilters { + if filter.Name == wellknown.TlsInspector { + return nil + } + } + + tlsInspector := &tls_inspector.TlsInspector{} + tlsInspectorAny, err := anypb.New(tlsInspector) + if err != nil { + return err + } + + filter := &listener.ListenerFilter{ + Name: wellknown.TlsInspector, + ConfigType: &listener.ListenerFilter_TypedConfig{ + TypedConfig: tlsInspectorAny, + }, + } + + xdsListener.ListenerFilters = append(xdsListener.ListenerFilters, filter) + + return nil } func buildXdsDownstreamTLSSocket(listenerName string, @@ -78,7 +253,7 @@ func buildXdsDownstreamTLSSocket(listenerName string, TlsCertificateSdsSecretConfigs: []*tls.SdsSecretConfig{{ // Generate key name for this listener. The actual key will be // delivered to Envoy via SDS. - Name: getXdsSecretName(listenerName), + Name: listenerName, SdsConfig: makeConfigSource(), }}, }, @@ -101,7 +276,7 @@ func buildXdsDownstreamTLSSecret(listenerName string, tlsConfig *ir.TLSListenerConfig) (*tls.Secret, error) { // Build the tls secret return &tls.Secret{ - Name: getXdsSecretName(listenerName), + Name: listenerName, Type: &tls.Secret_TlsCertificate{ TlsCertificate: &tls.TlsCertificate{ CertificateChain: &core.DataSource{ @@ -113,5 +288,78 @@ func buildXdsDownstreamTLSSecret(listenerName string, }, }, }, nil +} + +func buildXdsUDPListener(clusterName string, udpListener *ir.UDPListener) (*listener.Listener, error) { + if udpListener == nil { + return nil, errors.New("udp listener is nil") + } + + statPrefix := "service" + + route := &udp.Route{ + Cluster: clusterName, + } + routeAny, err := anypb.New(route) + if err != nil { + return nil, err + } + accesslogAny, _ := anypb.New(stdoutFileAccessLog) + udpProxy := &udp.UdpProxyConfig{ + StatPrefix: statPrefix, + AccessLog: []*accesslog.AccessLog{ + { + Name: wellknown.FileAccessLog, + ConfigType: &accesslog.AccessLog_TypedConfig{TypedConfig: accesslogAny}, + }, + }, + RouteSpecifier: &udp.UdpProxyConfig_Matcher{ + Matcher: &matcher.Matcher{ + OnNoMatch: &matcher.Matcher_OnMatch{ + OnMatch: &matcher.Matcher_OnMatch_Action{ + Action: &xdscore.TypedExtensionConfig{ + TypedConfig: routeAny, + }, + }, + }, + }, + }, + } + udpProxyAny, err := anypb.New(udpProxy) + if err != nil { + return nil, err + } + + filterChain := &listener.FilterChain{ + Filters: []*listener.Filter{{ + Name: "envoy.filters.udp_listener.udp_proxy", + ConfigType: &listener.Filter_TypedConfig{ + TypedConfig: udpProxyAny, + }, + }}, + } + + xdsListener := &listener.Listener{ + Name: udpListener.Name, + AccessLog: []*accesslog.AccessLog{ + { + Name: wellknown.FileAccessLog, + ConfigType: &accesslog.AccessLog_TypedConfig{TypedConfig: accesslogAny}, + }, + }, + Address: &core.Address{ + Address: &core.Address_SocketAddress{ + SocketAddress: &core.SocketAddress{ + Protocol: core.SocketAddress_UDP, + Address: udpListener.Address, + PortSpecifier: &core.SocketAddress_PortValue{ + PortValue: udpListener.Port, + }, + }, + }, + }, + FilterChains: []*listener.FilterChain{filterChain}, + } + return xdsListener, nil } diff --git a/internal/xds/translator/route.go b/internal/xds/translator/route.go index d231a606971..6ad00760bcf 100644 --- a/internal/xds/translator/route.go +++ b/internal/xds/translator/route.go @@ -1,3 +1,8 @@ +// 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 translator import ( @@ -135,20 +140,19 @@ func buildXdsStringMatcher(irMatch *ir.StringMatch) *matcher.StringMatcher { func buildXdsRouteAction(routeName string) *route.RouteAction { return &route.RouteAction{ ClusterSpecifier: &route.RouteAction_Cluster{ - Cluster: getXdsClusterName(routeName), + Cluster: routeName, }, } } func buildXdsWeightedRouteAction(httpRoute *ir.HTTPRoute) *route.RouteAction { - totalWeight := httpRoute.BackendWeights.Valid + httpRoute.BackendWeights.Invalid clusters := []*route.WeightedCluster_ClusterWeight{ { Name: "invalid-backend-cluster", Weight: &wrapperspb.UInt32Value{Value: httpRoute.BackendWeights.Invalid}, }, { - Name: getXdsClusterName(httpRoute.Name), + Name: httpRoute.Name, Weight: &wrapperspb.UInt32Value{Value: httpRoute.BackendWeights.Valid}, }, } @@ -157,8 +161,7 @@ func buildXdsWeightedRouteAction(httpRoute *ir.HTTPRoute) *route.RouteAction { ClusterNotFoundResponseCode: route.RouteAction_INTERNAL_SERVER_ERROR, ClusterSpecifier: &route.RouteAction_WeightedClusters{ WeightedClusters: &route.WeightedCluster{ - TotalWeight: &wrapperspb.UInt32Value{Value: totalWeight}, - Clusters: clusters, + Clusters: clusters, }, }, } diff --git a/internal/xds/translator/runner/runner.go b/internal/xds/translator/runner/runner.go index daa84570876..49ee70254eb 100644 --- a/internal/xds/translator/runner/runner.go +++ b/internal/xds/translator/runner/runner.go @@ -1,9 +1,15 @@ +// 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" "github.com/envoyproxy/gateway/internal/envoygateway/config" + "github.com/envoyproxy/gateway/internal/ir" "github.com/envoyproxy/gateway/internal/message" "github.com/envoyproxy/gateway/internal/xds/translator" ) @@ -36,10 +42,9 @@ func (r *Runner) Start(ctx context.Context) error { func (r *Runner) subscribeAndTranslate(ctx context.Context) { // Subscribe to resources - for snapshot := range r.XdsIR.Subscribe(ctx) { - r.Logger.Info("received a notification") - updates := snapshot.Updates - for _, update := range updates { + message.HandleSubscription(r.XdsIR.Subscribe(ctx), + func(update message.Update[string, *ir.Xds]) { + r.Logger.Info("received an update") key := update.Key val := update.Value @@ -55,7 +60,7 @@ func (r *Runner) subscribeAndTranslate(ctx context.Context) { r.Xds.Store(key, result) } } - } - } + }, + ) r.Logger.Info("subscriber shutting down") } diff --git a/internal/xds/translator/runner/runner_test.go b/internal/xds/translator/runner/runner_test.go index 36de62fab59..b7e7d8be032 100644 --- a/internal/xds/translator/runner/runner_test.go +++ b/internal/xds/translator/runner/runner_test.go @@ -1,3 +1,8 @@ +// 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 ( @@ -14,8 +19,6 @@ import ( ) func TestRunner(t *testing.T) { - // Remove once https://github.com/envoyproxy/gateway/issues/504 is completed. - t.Skip() // Setup xdsIR := new(message.XdsIR) xds := new(message.Xds) diff --git a/internal/xds/translator/testdata/in/xds-ir/http-route.yaml b/internal/xds/translator/testdata/in/xds-ir/http-route.yaml index 6a8cda1ded7..fd6c9654fbc 100644 --- a/internal/xds/translator/testdata/in/xds-ir/http-route.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/http-route.yaml @@ -5,7 +5,17 @@ http: hostnames: - "*" routes: - - name: "first-route" + - name: "first-route" + pathMatch: + name: "test" + exact: "foo/bar" + headerMatches: + - name: user + stringMatch: + exact: "jason" + queryParamMatches: + - name: "debug" + exact: "yes" destinations: - host: "1.2.3.4" port: 50000 diff --git a/internal/xds/translator/testdata/in/xds-ir/http2-route.yaml b/internal/xds/translator/testdata/in/xds-ir/http2-route.yaml new file mode 100644 index 00000000000..131d775c9bf --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/http2-route.yaml @@ -0,0 +1,22 @@ +http: +- name: "first-listener" + address: "0.0.0.0" + port: 10080 + hostnames: + - "*" + isHTTP2: true + routes: + - name: "first-route" + pathMatch: + name: "test" + exact: "foo/bar" + headerMatches: + - name: user + stringMatch: + exact: "jason" + queryParamMatches: + - name: "debug" + exact: "yes" + destinations: + - host: "1.2.3.4" + port: 50000 diff --git a/internal/xds/translator/testdata/in/xds-ir/multiple-listeners-same-port.yaml b/internal/xds/translator/testdata/in/xds-ir/multiple-listeners-same-port.yaml new file mode 100644 index 00000000000..38949ebcbd4 --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/multiple-listeners-same-port.yaml @@ -0,0 +1,66 @@ +http: +- name: "first-listener" + address: "0.0.0.0" + port: 10080 + hostnames: + - "foo.com" + tls: + serverCertificate: [99, 101, 114, 116, 45, 100, 97, 116, 97] # byte slice representation of "cert-data" + privateKey: [107, 101, 121, 45, 100, 97, 116, 97] # byte slice representation of "key-data" + routes: + - name: "first-route" + destinations: + - host: "1.2.3.4" + port: 50000 +- name: "second-listener" + address: "0.0.0.0" + port: 10080 + hostnames: + - "foo.net" + tls: + serverCertificate: [99, 101, 114, 116, 45, 100, 97, 116, 97] # byte slice representation of "cert-data" + privateKey: [107, 101, 121, 45, 100, 97, 116, 97] # byte slice representation of "key-data" + routes: + - name: "second-route" + destinations: + - host: "1.2.3.4" + port: 50000 +- name: "third-listener" + address: "0.0.0.0" + port: 10080 + hostnames: + - "example.com" + routes: + - name: "third-route" + destinations: + - host: "1.2.3.4" + port: 50000 +- name: "fourth-listener" + address: "0.0.0.0" + port: 10080 + hostnames: + - "example.net" + routes: + - name: "fourth-route" + destinations: + - host: "1.2.3.4" + port: 50000 +tcp: +- name: "fifth-listener" + address: "0.0.0.0" + port: 10080 + tls: + snis: + - bar.com + destinations: + - host: "1.2.3.4" + port: 50000 +- name: "sixth-listener" + address: "0.0.0.0" + port: 10080 + tls: + snis: + - bar.net + destinations: + - host: "1.2.3.4" + port: 50000 diff --git a/internal/xds/translator/testdata/in/xds-ir/tls-route-passthrough.yaml b/internal/xds/translator/testdata/in/xds-ir/tls-route-passthrough.yaml new file mode 100644 index 00000000000..c7f59633067 --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/tls-route-passthrough.yaml @@ -0,0 +1,12 @@ +tcp: +- name: "tls-passthrough" + address: "0.0.0.0" + port: 10080 + tls: + snis: + - foo.com + destinations: + - host: "1.2.3.4" + port: 50000 + - host: "5.6.7.8" + port: 50001 diff --git a/internal/xds/translator/testdata/in/xds-ir/udp-route.yaml b/internal/xds/translator/testdata/in/xds-ir/udp-route.yaml new file mode 100644 index 00000000000..490b4aa2121 --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/udp-route.yaml @@ -0,0 +1,9 @@ +udp: +- name: "udp-route" + address: "0.0.0.0" + port: 10080 + destinations: + - host: "1.2.3.4" + port: 50000 + - host: "5.6.7.8" + port: 50001 diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-direct-response.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-direct-response.clusters.yaml index c517dcb5690..0a8b9dd797d 100644 --- a/internal/xds/translator/testdata/out/xds-ir/http-route-direct-response.clusters.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-direct-response.clusters.yaml @@ -3,7 +3,7 @@ connectTimeout: 5s dnsLookupFamily: V4_ONLY loadAssignment: - clusterName: cluster_direct-route + clusterName: direct-route endpoints: - lbEndpoints: - endpoint: @@ -13,6 +13,6 @@ portValue: 50000 loadBalancingWeight: 1 locality: {} - name: cluster_direct-route + name: direct-route outlierDetection: {} type: STATIC diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-direct-response.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-direct-response.listeners.yaml index f1077139a82..6c555573979 100644 --- a/internal/xds/translator/testdata/out/xds-ir/http-route-direct-response.listeners.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-direct-response.listeners.yaml @@ -1,12 +1,26 @@ -- address: +- accessLog: + - filter: + responseFlagFilter: + flags: + - NR + name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout + address: socketAddress: address: 0.0.0.0 portValue: 10080 - filterChains: - - filters: + defaultFilterChain: + filters: - name: envoy.filters.network.http_connection_manager typedConfig: '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + accessLog: + - name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout httpFilters: - name: envoy.filters.http.router typedConfig: @@ -21,6 +35,6 @@ setNodeOnFirstMessageOnly: true transportApiVersion: V3 resourceApiVersion: V3 - routeConfigName: route_first-listener + routeConfigName: first-listener statPrefix: http - name: listener_first-listener_10080 + name: first-listener 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 e59392e464d..59bf816dd1b 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 @@ -1,8 +1,8 @@ -- name: route_first-listener +- name: first-listener virtualHosts: - domains: - '*' - name: route_first-listener + name: first-listener routes: - directResponse: body: diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-redirect.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-redirect.clusters.yaml index da7827f6088..3709f73545d 100644 --- a/internal/xds/translator/testdata/out/xds-ir/http-route-redirect.clusters.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-redirect.clusters.yaml @@ -3,7 +3,7 @@ connectTimeout: 5s dnsLookupFamily: V4_ONLY loadAssignment: - clusterName: cluster_redirect-route + clusterName: redirect-route endpoints: - lbEndpoints: - endpoint: @@ -13,6 +13,6 @@ portValue: 50000 loadBalancingWeight: 1 locality: {} - name: cluster_redirect-route + name: redirect-route outlierDetection: {} type: STATIC diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-redirect.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-redirect.listeners.yaml index f1077139a82..6c555573979 100644 --- a/internal/xds/translator/testdata/out/xds-ir/http-route-redirect.listeners.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-redirect.listeners.yaml @@ -1,12 +1,26 @@ -- address: +- accessLog: + - filter: + responseFlagFilter: + flags: + - NR + name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout + address: socketAddress: address: 0.0.0.0 portValue: 10080 - filterChains: - - filters: + defaultFilterChain: + filters: - name: envoy.filters.network.http_connection_manager typedConfig: '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + accessLog: + - name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout httpFilters: - name: envoy.filters.http.router typedConfig: @@ -21,6 +35,6 @@ setNodeOnFirstMessageOnly: true transportApiVersion: V3 resourceApiVersion: V3 - routeConfigName: route_first-listener + routeConfigName: first-listener statPrefix: http - name: listener_first-listener_10080 + name: first-listener diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-redirect.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-redirect.routes.yaml index 2c2f4436a9c..462febfae07 100644 --- a/internal/xds/translator/testdata/out/xds-ir/http-route-redirect.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-redirect.routes.yaml @@ -1,8 +1,8 @@ -- name: route_first-listener +- name: first-listener virtualHosts: - domains: - '*' - name: route_first-listener + name: first-listener routes: - match: prefix: / diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-request-headers.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-request-headers.clusters.yaml index ce7adf442e6..035a07ecd59 100644 --- a/internal/xds/translator/testdata/out/xds-ir/http-route-request-headers.clusters.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-request-headers.clusters.yaml @@ -3,7 +3,7 @@ connectTimeout: 5s dnsLookupFamily: V4_ONLY loadAssignment: - clusterName: cluster_request-header-route + clusterName: request-header-route endpoints: - lbEndpoints: - endpoint: @@ -13,6 +13,6 @@ portValue: 50000 loadBalancingWeight: 1 locality: {} - name: cluster_request-header-route + name: request-header-route outlierDetection: {} type: STATIC diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-request-headers.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-request-headers.listeners.yaml index f1077139a82..6c555573979 100644 --- a/internal/xds/translator/testdata/out/xds-ir/http-route-request-headers.listeners.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-request-headers.listeners.yaml @@ -1,12 +1,26 @@ -- address: +- accessLog: + - filter: + responseFlagFilter: + flags: + - NR + name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout + address: socketAddress: address: 0.0.0.0 portValue: 10080 - filterChains: - - filters: + defaultFilterChain: + filters: - name: envoy.filters.network.http_connection_manager typedConfig: '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + accessLog: + - name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout httpFilters: - name: envoy.filters.http.router typedConfig: @@ -21,6 +35,6 @@ setNodeOnFirstMessageOnly: true transportApiVersion: V3 resourceApiVersion: V3 - routeConfigName: route_first-listener + routeConfigName: first-listener statPrefix: http - name: listener_first-listener_10080 + name: first-listener diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-request-headers.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-request-headers.routes.yaml index 5d9c5f08a63..0284c21fc0d 100644 --- a/internal/xds/translator/testdata/out/xds-ir/http-route-request-headers.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-request-headers.routes.yaml @@ -1,8 +1,8 @@ -- name: route_first-listener +- name: first-listener virtualHosts: - domains: - '*' - name: route_first-listener + name: first-listener routes: - match: prefix: / @@ -31,4 +31,4 @@ - some-header5 - some-header6 route: - cluster: cluster_request-header-route + cluster: request-header-route diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-weighted-invalid-backend.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-weighted-invalid-backend.clusters.yaml index c65cb16a6a4..c10babdc70f 100644 --- a/internal/xds/translator/testdata/out/xds-ir/http-route-weighted-invalid-backend.clusters.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-weighted-invalid-backend.clusters.yaml @@ -3,7 +3,7 @@ connectTimeout: 5s dnsLookupFamily: V4_ONLY loadAssignment: - clusterName: cluster_first-route + clusterName: first-route endpoints: - lbEndpoints: - endpoint: @@ -13,6 +13,6 @@ portValue: 50000 loadBalancingWeight: 1 locality: {} - name: cluster_first-route + name: first-route outlierDetection: {} type: STATIC diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-weighted-invalid-backend.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-weighted-invalid-backend.listeners.yaml index f1077139a82..6c555573979 100644 --- a/internal/xds/translator/testdata/out/xds-ir/http-route-weighted-invalid-backend.listeners.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-weighted-invalid-backend.listeners.yaml @@ -1,12 +1,26 @@ -- address: +- accessLog: + - filter: + responseFlagFilter: + flags: + - NR + name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout + address: socketAddress: address: 0.0.0.0 portValue: 10080 - filterChains: - - filters: + defaultFilterChain: + filters: - name: envoy.filters.network.http_connection_manager typedConfig: '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + accessLog: + - name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout httpFilters: - name: envoy.filters.http.router typedConfig: @@ -21,6 +35,6 @@ setNodeOnFirstMessageOnly: true transportApiVersion: V3 resourceApiVersion: V3 - routeConfigName: route_first-listener + routeConfigName: first-listener statPrefix: http - name: listener_first-listener_10080 + name: first-listener diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-weighted-invalid-backend.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-weighted-invalid-backend.routes.yaml index 7b13acc60a9..789931e4411 100644 --- a/internal/xds/translator/testdata/out/xds-ir/http-route-weighted-invalid-backend.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-weighted-invalid-backend.routes.yaml @@ -1,8 +1,8 @@ -- name: route_first-listener +- name: first-listener virtualHosts: - domains: - '*' - name: route_first-listener + name: first-listener routes: - match: prefix: / @@ -12,6 +12,5 @@ clusters: - name: invalid-backend-cluster weight: 1 - - name: cluster_first-route + - name: first-route weight: 1 - totalWeight: 2 diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route.clusters.yaml index c65cb16a6a4..c10babdc70f 100644 --- a/internal/xds/translator/testdata/out/xds-ir/http-route.clusters.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/http-route.clusters.yaml @@ -3,7 +3,7 @@ connectTimeout: 5s dnsLookupFamily: V4_ONLY loadAssignment: - clusterName: cluster_first-route + clusterName: first-route endpoints: - lbEndpoints: - endpoint: @@ -13,6 +13,6 @@ portValue: 50000 loadBalancingWeight: 1 locality: {} - name: cluster_first-route + name: first-route outlierDetection: {} type: STATIC diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route.listeners.yaml index f1077139a82..6c555573979 100644 --- a/internal/xds/translator/testdata/out/xds-ir/http-route.listeners.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/http-route.listeners.yaml @@ -1,12 +1,26 @@ -- address: +- accessLog: + - filter: + responseFlagFilter: + flags: + - NR + name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout + address: socketAddress: address: 0.0.0.0 portValue: 10080 - filterChains: - - filters: + defaultFilterChain: + filters: - name: envoy.filters.network.http_connection_manager typedConfig: '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + accessLog: + - name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout httpFilters: - name: envoy.filters.http.router typedConfig: @@ -21,6 +35,6 @@ setNodeOnFirstMessageOnly: true transportApiVersion: V3 resourceApiVersion: V3 - routeConfigName: route_first-listener + routeConfigName: first-listener statPrefix: http - name: listener_first-listener_10080 + name: first-listener diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route.routes.yaml index a550146b227..a28539a6d49 100644 --- a/internal/xds/translator/testdata/out/xds-ir/http-route.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/http-route.routes.yaml @@ -1,10 +1,18 @@ -- name: route_first-listener +- name: first-listener virtualHosts: - domains: - '*' - name: route_first-listener + name: first-listener routes: - match: - prefix: / + headers: + - name: user + stringMatch: + exact: jason + path: foo/bar + queryParameters: + - name: debug + stringMatch: + exact: "yes" route: - cluster: cluster_first-route + cluster: first-route diff --git a/internal/xds/translator/testdata/out/xds-ir/http2-route.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/http2-route.clusters.yaml new file mode 100644 index 00000000000..3c1acd4f3a6 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/http2-route.clusters.yaml @@ -0,0 +1,19 @@ +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 5s + dnsLookupFamily: V4_ONLY + http2ProtocolOptions: {} + loadAssignment: + clusterName: first-route + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + locality: {} + name: first-route + outlierDetection: {} + type: STATIC diff --git a/internal/xds/translator/testdata/out/xds-ir/http2-route.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/http2-route.listeners.yaml new file mode 100644 index 00000000000..6c555573979 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/http2-route.listeners.yaml @@ -0,0 +1,40 @@ +- accessLog: + - filter: + responseFlagFilter: + flags: + - NR + name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout + 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 + accessLog: + - name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout + httpFilters: + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + rds: + configSource: + apiConfigSource: + apiType: DELTA_GRPC + grpcServices: + - envoyGrpc: + clusterName: xds_cluster + setNodeOnFirstMessageOnly: true + transportApiVersion: V3 + resourceApiVersion: V3 + routeConfigName: first-listener + statPrefix: http + name: first-listener diff --git a/internal/xds/translator/testdata/out/xds-ir/http2-route.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/http2-route.routes.yaml new file mode 100644 index 00000000000..a28539a6d49 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/http2-route.routes.yaml @@ -0,0 +1,18 @@ +- name: first-listener + virtualHosts: + - domains: + - '*' + name: first-listener + routes: + - match: + headers: + - name: user + stringMatch: + exact: jason + path: foo/bar + queryParameters: + - name: debug + stringMatch: + exact: "yes" + route: + cluster: first-route diff --git a/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port.clusters.yaml new file mode 100644 index 00000000000..e4e723e49f4 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port.clusters.yaml @@ -0,0 +1,102 @@ +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 5s + dnsLookupFamily: V4_ONLY + loadAssignment: + clusterName: first-route + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + locality: {} + name: first-route + outlierDetection: {} + type: STATIC +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 5s + dnsLookupFamily: V4_ONLY + loadAssignment: + clusterName: second-route + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + locality: {} + name: second-route + outlierDetection: {} + type: STATIC +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 5s + dnsLookupFamily: V4_ONLY + loadAssignment: + clusterName: third-route + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + locality: {} + name: third-route + outlierDetection: {} + type: STATIC +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 5s + dnsLookupFamily: V4_ONLY + loadAssignment: + clusterName: fourth-route + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + locality: {} + name: fourth-route + outlierDetection: {} + type: STATIC +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 5s + dnsLookupFamily: V4_ONLY + loadAssignment: + clusterName: fifth-listener + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + locality: {} + name: fifth-listener + outlierDetection: {} + type: STATIC +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 5s + dnsLookupFamily: V4_ONLY + loadAssignment: + clusterName: sixth-listener + endpoints: + - loadBalancingWeight: 1 + locality: {} + name: sixth-listener + outlierDetection: {} + type: STATIC diff --git a/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port.listeners.yaml new file mode 100644 index 00000000000..e44df68af1f --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port.listeners.yaml @@ -0,0 +1,161 @@ +- accessLog: + - filter: + responseFlagFilter: + flags: + - NR + name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout + 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 + accessLog: + - name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout + httpFilters: + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + rds: + configSource: + apiConfigSource: + apiType: DELTA_GRPC + grpcServices: + - envoyGrpc: + clusterName: xds_cluster + setNodeOnFirstMessageOnly: true + transportApiVersion: V3 + resourceApiVersion: V3 + routeConfigName: third-listener + statPrefix: http + filterChains: + - filterChainMatch: + serverNames: + - foo.com + filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + accessLog: + - name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout + httpFilters: + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + rds: + configSource: + apiConfigSource: + apiType: DELTA_GRPC + grpcServices: + - envoyGrpc: + clusterName: xds_cluster + setNodeOnFirstMessageOnly: true + transportApiVersion: V3 + resourceApiVersion: V3 + routeConfigName: first-listener + statPrefix: https + transportSocket: + name: envoy.transport_sockets.tls + typedConfig: + '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext + commonTlsContext: + tlsCertificateSdsSecretConfigs: + - name: first-listener + sdsConfig: + apiConfigSource: + apiType: DELTA_GRPC + grpcServices: + - envoyGrpc: + clusterName: xds_cluster + setNodeOnFirstMessageOnly: true + transportApiVersion: V3 + resourceApiVersion: V3 + - filterChainMatch: + serverNames: + - foo.net + filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + accessLog: + - name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout + httpFilters: + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + rds: + configSource: + apiConfigSource: + apiType: DELTA_GRPC + grpcServices: + - envoyGrpc: + clusterName: xds_cluster + setNodeOnFirstMessageOnly: true + transportApiVersion: V3 + resourceApiVersion: V3 + routeConfigName: second-listener + statPrefix: https + transportSocket: + name: envoy.transport_sockets.tls + typedConfig: + '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext + commonTlsContext: + tlsCertificateSdsSecretConfigs: + - name: second-listener + sdsConfig: + apiConfigSource: + apiType: DELTA_GRPC + grpcServices: + - envoyGrpc: + clusterName: xds_cluster + setNodeOnFirstMessageOnly: true + transportApiVersion: V3 + resourceApiVersion: V3 + - filterChainMatch: + serverNames: + - bar.com + filters: + - name: envoy.filters.network.tcp_proxy + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy + accessLog: + - name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout + cluster: fifth-listener + statPrefix: passthrough + - filterChainMatch: + serverNames: + - bar.net + filters: + - name: envoy.filters.network.tcp_proxy + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy + accessLog: + - name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout + cluster: sixth-listener + statPrefix: passthrough + listenerFilters: + - name: envoy.filters.listener.tls_inspector + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector + name: first-listener diff --git a/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port.routes.yaml new file mode 100644 index 00000000000..b9793e4baff --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port.routes.yaml @@ -0,0 +1,38 @@ +- name: first-listener + virtualHosts: + - domains: + - foo.com + name: first-listener + routes: + - match: + prefix: / + route: + cluster: first-route +- name: second-listener + virtualHosts: + - domains: + - foo.net + name: second-listener + routes: + - match: + prefix: / + route: + cluster: second-route +- name: third-listener + virtualHosts: + - domains: + - example.com + name: third-listener + routes: + - match: + prefix: / + route: + cluster: third-route + - domains: + - example.net + name: fourth-listener + routes: + - match: + prefix: / + route: + cluster: fourth-route diff --git a/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port.secrets.yaml b/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port.secrets.yaml new file mode 100644 index 00000000000..47ebba7468c --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port.secrets.yaml @@ -0,0 +1,12 @@ +- name: first-listener + tlsCertificate: + certificateChain: + inlineBytes: Y2VydC1kYXRh + privateKey: + inlineBytes: a2V5LWRhdGE= +- name: second-listener + tlsCertificate: + certificateChain: + inlineBytes: Y2VydC1kYXRh + privateKey: + inlineBytes: a2V5LWRhdGE= diff --git a/internal/xds/translator/testdata/out/xds-ir/simple-tls.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/simple-tls.clusters.yaml index c65cb16a6a4..c10babdc70f 100644 --- a/internal/xds/translator/testdata/out/xds-ir/simple-tls.clusters.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/simple-tls.clusters.yaml @@ -3,7 +3,7 @@ connectTimeout: 5s dnsLookupFamily: V4_ONLY loadAssignment: - clusterName: cluster_first-route + clusterName: first-route endpoints: - lbEndpoints: - endpoint: @@ -13,6 +13,6 @@ portValue: 50000 loadBalancingWeight: 1 locality: {} - name: cluster_first-route + name: first-route outlierDetection: {} type: STATIC diff --git a/internal/xds/translator/testdata/out/xds-ir/simple-tls.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/simple-tls.listeners.yaml index 58110f0b99d..1a3e22e469f 100644 --- a/internal/xds/translator/testdata/out/xds-ir/simple-tls.listeners.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/simple-tls.listeners.yaml @@ -1,4 +1,13 @@ -- address: +- accessLog: + - filter: + responseFlagFilter: + flags: + - NR + name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout + address: socketAddress: address: 0.0.0.0 portValue: 10080 @@ -7,6 +16,11 @@ - name: envoy.filters.network.http_connection_manager typedConfig: '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + accessLog: + - name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout httpFilters: - name: envoy.filters.http.router typedConfig: @@ -21,15 +35,15 @@ setNodeOnFirstMessageOnly: true transportApiVersion: V3 resourceApiVersion: V3 - routeConfigName: route_first-listener - statPrefix: http + routeConfigName: first-listener + statPrefix: https transportSocket: name: envoy.transport_sockets.tls typedConfig: '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext commonTlsContext: tlsCertificateSdsSecretConfigs: - - name: secret_first-listener + - name: first-listener sdsConfig: apiConfigSource: apiType: DELTA_GRPC @@ -39,4 +53,4 @@ setNodeOnFirstMessageOnly: true transportApiVersion: V3 resourceApiVersion: V3 - name: listener_first-listener_10080 + name: first-listener diff --git a/internal/xds/translator/testdata/out/xds-ir/simple-tls.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/simple-tls.routes.yaml index a550146b227..ed122e552aa 100644 --- a/internal/xds/translator/testdata/out/xds-ir/simple-tls.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/simple-tls.routes.yaml @@ -1,10 +1,10 @@ -- name: route_first-listener +- name: first-listener virtualHosts: - domains: - '*' - name: route_first-listener + name: first-listener routes: - match: prefix: / route: - cluster: cluster_first-route + cluster: first-route diff --git a/internal/xds/translator/testdata/out/xds-ir/simple-tls.secrets.yaml b/internal/xds/translator/testdata/out/xds-ir/simple-tls.secrets.yaml index 23859a66abf..9155e5c882d 100644 --- a/internal/xds/translator/testdata/out/xds-ir/simple-tls.secrets.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/simple-tls.secrets.yaml @@ -1,4 +1,4 @@ -- name: secret_first-listener +- name: first-listener tlsCertificate: certificateChain: inlineBytes: Y2VydC1kYXRh diff --git a/internal/xds/translator/testdata/out/xds-ir/tls-route-passthrough.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/tls-route-passthrough.clusters.yaml new file mode 100644 index 00000000000..48ee1e2ba40 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/tls-route-passthrough.clusters.yaml @@ -0,0 +1,23 @@ +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 5s + dnsLookupFamily: V4_ONLY + loadAssignment: + clusterName: tls-passthrough + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + - endpoint: + address: + socketAddress: + address: 5.6.7.8 + portValue: 50001 + loadBalancingWeight: 1 + locality: {} + name: tls-passthrough + outlierDetection: {} + type: STATIC diff --git a/internal/xds/translator/testdata/out/xds-ir/tls-route-passthrough.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/tls-route-passthrough.listeners.yaml new file mode 100644 index 00000000000..2186e91d327 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/tls-route-passthrough.listeners.yaml @@ -0,0 +1,33 @@ +- accessLog: + - filter: + responseFlagFilter: + flags: + - NR + name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout + address: + socketAddress: + address: 0.0.0.0 + portValue: 10080 + filterChains: + - filterChainMatch: + serverNames: + - foo.com + filters: + - name: envoy.filters.network.tcp_proxy + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy + accessLog: + - name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout + cluster: tls-passthrough + statPrefix: passthrough + listenerFilters: + - name: envoy.filters.listener.tls_inspector + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector + name: tls-passthrough diff --git a/internal/xds/translator/testdata/out/xds-ir/tls-route-passthrough.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/tls-route-passthrough.routes.yaml new file mode 100644 index 00000000000..fe51488c706 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/tls-route-passthrough.routes.yaml @@ -0,0 +1 @@ +[] diff --git a/internal/xds/translator/testdata/out/xds-ir/udp-route.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/udp-route.clusters.yaml new file mode 100644 index 00000000000..3201aa8be50 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/udp-route.clusters.yaml @@ -0,0 +1,23 @@ +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 5s + dnsLookupFamily: V4_ONLY + loadAssignment: + clusterName: udp-route + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + - endpoint: + address: + socketAddress: + address: 5.6.7.8 + portValue: 50001 + loadBalancingWeight: 1 + locality: {} + name: udp-route + outlierDetection: {} + type: STATIC diff --git a/internal/xds/translator/testdata/out/xds-ir/udp-route.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/udp-route.listeners.yaml new file mode 100644 index 00000000000..d0def54c67b --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/udp-route.listeners.yaml @@ -0,0 +1,28 @@ +- accessLog: + - name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout + address: + socketAddress: + address: 0.0.0.0 + portValue: 10080 + protocol: UDP + filterChains: + - filters: + - name: envoy.filters.udp_listener.udp_proxy + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.udp.udp_proxy.v3.UdpProxyConfig + accessLog: + - name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout + matcher: + onNoMatch: + action: + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.udp.udp_proxy.v3.Route + cluster: udp-route + statPrefix: service + name: udp-route diff --git a/internal/xds/translator/testdata/out/xds-ir/udp-route.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/udp-route.routes.yaml new file mode 100644 index 00000000000..fe51488c706 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/udp-route.routes.yaml @@ -0,0 +1 @@ +[] diff --git a/internal/xds/translator/translator.go b/internal/xds/translator/translator.go index 3c6bf26147c..eeaf4a192e1 100644 --- a/internal/xds/translator/translator.go +++ b/internal/xds/translator/translator.go @@ -1,10 +1,15 @@ +// 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 translator import ( "errors" - "fmt" core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" resource "github.com/envoyproxy/go-control-plane/pkg/resource/v3" "github.com/tetratelabs/multierror" @@ -22,21 +27,46 @@ func Translate(ir *ir.Xds) (*types.ResourceVersionTable, error) { tCtx := new(types.ResourceVersionTable) for _, httpListener := range ir.HTTP { - // 1:1 between IR HTTPListener and xDS Listener - xdsListener, err := buildXdsListener(httpListener) - if err != nil { - return nil, multierror.Append(err, errors.New("error building xds listener")) + addFilterChain := true + var xdsRouteCfg *route.RouteConfiguration + + // Search for an existing listener, if it does not exist, create one. + xdsListener := findXdsListener(tCtx, httpListener.Address, httpListener.Port, core.SocketAddress_TCP) + if xdsListener == nil { + xdsListener = buildXdsTCPListener(httpListener.Name, httpListener.Address, httpListener.Port) + tCtx.AddXdsResource(resource.ListenerType, xdsListener) + } else if httpListener.TLS == nil { + // Find the route config associated with this listener that + // maps to the default filter chain for http traffic + routeName := findXdsHTTPRouteConfigName(xdsListener) + if routeName != "" { + // If an existing listener exists, dont create a new filter chain + // for HTTP traffic, match on the Domains field within VirtualHosts + // within the same RouteConfiguration instead + addFilterChain = false + xdsRouteCfg = findXdsRouteConfig(tCtx, routeName) + if xdsRouteCfg == nil { + return nil, errors.New("unable to find xds route config") + } + } } - // 1:1 between IR TLSListenerConfig and xDS Secret - if httpListener.TLS != nil { - // Build downstream TLS details. - tSocket, err := buildXdsDownstreamTLSSocket(httpListener.Name, httpListener.TLS) - if err != nil { - return nil, multierror.Append(err, errors.New("error building xds listener tls socket")) + if addFilterChain { + if err := addXdsHTTPFilterChain(xdsListener, httpListener); err != nil { + return nil, err } - xdsListener.FilterChains[0].TransportSocket = tSocket + } + + // Create a route config if we have not found one yet + if xdsRouteCfg == nil { + xdsRouteCfg = &route.RouteConfiguration{ + Name: httpListener.Name, + } + tCtx.AddXdsResource(resource.RouteType, xdsRouteCfg) + } + // 1:1 between IR TLSListenerConfig and xDS Secret + if httpListener.TLS != nil { secret, err := buildXdsDownstreamTLSSecret(httpListener.Name, httpListener.TLS) if err != nil { return nil, multierror.Append(err, errors.New("error building xds listener tls secret")) @@ -46,9 +76,8 @@ func Translate(ir *ir.Xds) (*types.ResourceVersionTable, error) { // Allocate virtual host for this httpListener. // 1:1 between IR HTTPListener and xDS VirtualHost - routeName := getXdsRouteName(httpListener.Name) vHost := &route.VirtualHost{ - Name: routeName, + Name: httpListener.Name, Domains: httpListener.Hostnames, } @@ -64,7 +93,7 @@ func Translate(ir *ir.Xds) (*types.ResourceVersionTable, error) { if len(httpRoute.Destinations) == 0 && httpRoute.BackendWeights.Invalid > 0 { continue } - xdsCluster, err := buildXdsCluster(httpRoute) + xdsCluster, err := buildXdsCluster(httpRoute.Name, httpRoute.Destinations, httpListener.IsHTTP2) if err != nil { return nil, multierror.Append(err, errors.New("error building xds cluster")) } @@ -72,32 +101,81 @@ func Translate(ir *ir.Xds) (*types.ResourceVersionTable, error) { } - xdsRouteCfg := &route.RouteConfiguration{ - Name: routeName, - } xdsRouteCfg.VirtualHosts = append(xdsRouteCfg.VirtualHosts, vHost) + } - tCtx.AddXdsResource(resource.ListenerType, xdsListener) - tCtx.AddXdsResource(resource.RouteType, xdsRouteCfg) + for _, tcpListener := range ir.TCP { + // 1:1 between IR TCPListener and xDS Cluster + xdsCluster, err := buildXdsCluster(tcpListener.Name, tcpListener.Destinations, false /*isHTTP2 */) + if err != nil { + return nil, multierror.Append(err, errors.New("error building xds cluster")) + } + tCtx.AddXdsResource(resource.ClusterType, xdsCluster) + + // Search for an existing listener, if it does not exist, create one. + xdsListener := findXdsListener(tCtx, tcpListener.Address, tcpListener.Port, core.SocketAddress_TCP) + if xdsListener == nil { + xdsListener = buildXdsTCPListener(tcpListener.Name, tcpListener.Address, tcpListener.Port) + tCtx.AddXdsResource(resource.ListenerType, xdsListener) + } + + if err := addXdsTCPFilterChain(xdsListener, tcpListener, xdsCluster.Name); err != nil { + return nil, err + } } + for _, udpListener := range ir.UDP { + // 1:1 between IR UDPListener and xDS Cluster + xdsCluster, err := buildXdsCluster(udpListener.Name, udpListener.Destinations, false /*isHTTP2 */) + if err != nil { + return nil, multierror.Append(err, errors.New("error building xds cluster")) + } + tCtx.AddXdsResource(resource.ClusterType, xdsCluster) + + // There won't be multiple UDP listeners on the same port since it's already been checked at the gateway api + // translator + xdsListener, err := buildXdsUDPListener(xdsCluster.Name, udpListener) + if err != nil { + return nil, multierror.Append(err, errors.New("error building xds cluster")) + } + tCtx.AddXdsResource(resource.ListenerType, xdsListener) + } return tCtx, nil } -func getXdsRouteName(listenerName string) string { - return fmt.Sprintf("route_%s", listenerName) -} +// findXdsListener finds a xds listener with the same address, port and protocol, and returns nil if there is no match. +func findXdsListener(tCtx *types.ResourceVersionTable, address string, port uint32, + protocol core.SocketAddress_Protocol) *listener.Listener { + if tCtx == nil || tCtx.XdsResources == nil || tCtx.XdsResources[resource.ListenerType] == nil { + return nil + } -func getXdsListenerName(listenerName string, listenerPort uint32) string { - return fmt.Sprintf("listener_%s_%d", listenerName, listenerPort) -} + for _, r := range tCtx.XdsResources[resource.ListenerType] { + listener := r.(*listener.Listener) + addr := listener.GetAddress() + if addr.GetSocketAddress().GetPortValue() == port && addr.GetSocketAddress().Address == address && addr. + GetSocketAddress().Protocol == protocol { + return listener + } + } -func getXdsSecretName(listenerName string) string { - return fmt.Sprintf("secret_%s", listenerName) + return nil } -func getXdsClusterName(routeName string) string { - return fmt.Sprintf("cluster_%s", routeName) +// findXdsRouteConfig finds an xds route with the name and returns nil if there is no match. +func findXdsRouteConfig(tCtx *types.ResourceVersionTable, name string) *route.RouteConfiguration { + if tCtx == nil || tCtx.XdsResources == nil || tCtx.XdsResources[resource.RouteType] == nil { + return nil + } + + for _, r := range tCtx.XdsResources[resource.RouteType] { + route := r.(*route.RouteConfiguration) + if route.Name == name { + return route + } + } + + return nil } // Point to xds cluster. diff --git a/internal/xds/translator/translator_test.go b/internal/xds/translator/translator_test.go index 90cea496704..5591363f815 100644 --- a/internal/xds/translator/translator_test.go +++ b/internal/xds/translator/translator_test.go @@ -1,3 +1,8 @@ +// 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 translator import ( @@ -50,6 +55,19 @@ func TestTranslate(t *testing.T) { name: "simple-tls", requireSecrets: true, }, + { + name: "tls-route-passthrough", + }, + { + name: "multiple-listeners-same-port", + requireSecrets: true, + }, + { + name: "udp-route", + }, + { + name: "http2-route", + }, } for _, tc := range testCases { diff --git a/internal/xds/types/resourceversiontable.go b/internal/xds/types/resourceversiontable.go index 9aed4c728d5..ebf323ab8f1 100644 --- a/internal/xds/types/resourceversiontable.go +++ b/internal/xds/types/resourceversiontable.go @@ -1,3 +1,8 @@ +// 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 types import ( diff --git a/internal/xds/types/resourceversiontable_test.go b/internal/xds/types/resourceversiontable_test.go index e78402f526b..2cb17be407f 100644 --- a/internal/xds/types/resourceversiontable_test.go +++ b/internal/xds/types/resourceversiontable_test.go @@ -1,3 +1,8 @@ +// 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 types import ( diff --git a/kustomization.yaml b/kustomization.yaml deleted file mode 100644 index a99c6d192d9..00000000000 --- a/kustomization.yaml +++ /dev/null @@ -1,3 +0,0 @@ -resources: - - release-artifacts/envoy-gateway.yaml - - release-artifacts/infra-manager-rbac.yaml diff --git a/changelogs/0.1.0.yaml b/release-notes/v0.1.0.yaml similarity index 100% rename from changelogs/0.1.0.yaml rename to release-notes/v0.1.0.yaml diff --git a/changelogs/0.2.0-rc1.yaml b/release-notes/v0.2.0-rc1.yaml similarity index 100% rename from changelogs/0.2.0-rc1.yaml rename to release-notes/v0.2.0-rc1.yaml diff --git a/changelogs/0.2.0-rc2.yaml b/release-notes/v0.2.0-rc2.yaml similarity index 100% rename from changelogs/0.2.0-rc2.yaml rename to release-notes/v0.2.0-rc2.yaml diff --git a/release-notes/v0.2.0.yaml b/release-notes/v0.2.0.yaml new file mode 100644 index 00000000000..cecac2f7693 --- /dev/null +++ b/release-notes/v0.2.0.yaml @@ -0,0 +1,52 @@ +date: October 19, 2022 + +changes: + - area: documentation + change: | + 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. + - area: api + change: | + Added the EnvoyGateway API type for configuring Envoy Gateway. + Added the EnvoyProxy API type for configuring managed Envoys. + - area: ci-tooling-testing + change: | + 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. + - area: ir + change: | + Added xds and infra IRs to decouple user-facing APIs from Envoy Gateway. + Added IR validation. + - area: translator + change: | + 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. + - area: message-service + change: | + 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. + - area: infra-manager + change: | + Added Kubernetes Infra Manager to manage Envoy infrastructure running in a Kubernetes cluster. + Added support for managing a separate Envoy infrastructure per Gateway. + - area: providers + change: | + 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. + - area: xds + change: | + 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/test/conformance/conformance_test.go b/test/conformance/conformance_test.go index e69f5586b71..09f576105b6 100644 --- a/test/conformance/conformance_test.go +++ b/test/conformance/conformance_test.go @@ -1,9 +1,15 @@ //go:build conformance // +build conformance +// 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 conformance import ( + "flag" "testing" "github.com/stretchr/testify/require" @@ -15,7 +21,11 @@ import ( "sigs.k8s.io/gateway-api/conformance/utils/suite" ) +var useUniquePorts = flag.Bool("use-unique-ports", true, "whether to use unique ports") + func TestGatewayAPIConformance(t *testing.T) { + flag.Parse() + cfg, err := config.GetConfig() require.NoError(t, err) @@ -24,18 +34,24 @@ func TestGatewayAPIConformance(t *testing.T) { require.NoError(t, v1alpha2.AddToScheme(client.Scheme())) + validUniqueListenerPorts := []v1alpha2.PortNumber{ + v1alpha2.PortNumber(int32(80)), + v1alpha2.PortNumber(int32(81)), + v1alpha2.PortNumber(int32(82)), + v1alpha2.PortNumber(int32(83)), + } + + if !*useUniquePorts { + validUniqueListenerPorts = []v1alpha2.PortNumber{} + } + cSuite := suite.New(suite.Options{ - Client: client, - GatewayClassName: *flags.GatewayClassName, - Debug: *flags.ShowDebug, - CleanupBaseResources: *flags.CleanupBaseResources, - ValidUniqueListenerPorts: []v1alpha2.PortNumber{ - v1alpha2.PortNumber(int32(80)), - v1alpha2.PortNumber(int32(81)), - v1alpha2.PortNumber(int32(82)), - v1alpha2.PortNumber(int32(83)), - v1alpha2.PortNumber(int32(84)), - }, + Client: client, + GatewayClassName: *flags.GatewayClassName, + Debug: *flags.ShowDebug, + CleanupBaseResources: *flags.CleanupBaseResources, + ValidUniqueListenerPorts: validUniqueListenerPorts, + SupportedFeatures: []suite.SupportedFeature{suite.SupportHTTPRouteQueryParamMatching, suite.SupportReferenceGrant}, }) cSuite.Setup(t) egTests := []suite.ConformanceTest{ @@ -47,9 +63,16 @@ func TestGatewayAPIConformance(t *testing.T) { tests.HTTPRouteCrossNamespace, tests.HTTPRouteHeaderMatching, tests.HTTPRouteMatchingAcrossRoutes, + tests.HTTPRouteHostnameIntersection, + tests.HTTPRouteListenerHostnameMatching, tests.HTTPRouteInvalidNonExistentBackendRef, tests.HTTPRouteInvalidBackendRefUnknownKind, tests.HTTPRouteInvalidCrossNamespaceBackendRef, + tests.GatewaySecretReferenceGrantAllInNamespace, + tests.GatewaySecretReferenceGrantSpecific, + // Uncomment when https://github.com/envoyproxy/gateway/issues/539 is fixed. + /*tests.GatewaySecretMissingReferenceGrant, + tests.GatewaySecretInvalidReferenceGrant,*/ } cSuite.Run(t, egTests) diff --git a/tools/boilerplate/boilerplate.generatego.txt b/tools/boilerplate/boilerplate.generatego.txt new file mode 100644 index 00000000000..3f9e2deb710 --- /dev/null +++ b/tools/boilerplate/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/tools/boilerplate/boilerplate.go.txt b/tools/boilerplate/boilerplate.go.txt new file mode 100644 index 00000000000..3f9e2deb710 --- /dev/null +++ b/tools/boilerplate/boilerplate.go.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/tools/boilerplate/boilerplate.py b/tools/boilerplate/boilerplate.py new file mode 100755 index 00000000000..4741440808f --- /dev/null +++ b/tools/boilerplate/boilerplate.py @@ -0,0 +1,257 @@ +#!/usr/bin/env python + +# Copyright 2015 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This file is copied from https://github.com/kubernetes/kubernetes/blob/04c2b1fbdc1289c9a72eda87cf7072346e60d241/hack/boilerplate/boilerplate.py + +from __future__ import print_function + +import argparse +import datetime +import difflib +import glob +import os +import re +import sys + +parser = argparse.ArgumentParser() +parser.add_argument( + "filenames", + help="list of files to check, all files if unspecified", + nargs='*') + +rootdir = os.path.dirname(__file__) + "/../../" +rootdir = os.path.abspath(rootdir) +parser.add_argument( + "--rootdir", default=rootdir, help="root directory to examine") + +default_boilerplate_dir = os.path.join(rootdir, "tools/boilerplate") +parser.add_argument( + "--boilerplate-dir", default=default_boilerplate_dir) + +parser.add_argument( + "-v", "--verbose", + help="give verbose output regarding why a file does not pass", + action="store_true") + +args = parser.parse_args() + +verbose_out = sys.stderr if args.verbose else open("/dev/null", "w") + + +def get_refs(): + refs = {} + + for path in glob.glob(os.path.join(args.boilerplate_dir, "boilerplate.*.txt")): + extension = os.path.basename(path).split(".")[1] + + ref_file = open(path, 'r') + ref = ref_file.read().splitlines() + ref_file.close() + refs[extension] = ref + + return refs + + +def is_generated_file(filename, data, regexs): + for d in skipped_ungenerated_files: + if d in filename: + return False + + p = regexs["generated"] + return p.search(data) + + +def file_passes(filename, refs, regexs): + try: + f = open(filename, 'r') + except Exception as exc: + print("Unable to open %s: %s" % (filename, exc), file=verbose_out) + return False + + data = f.read() + f.close() + + # determine if the file is automatically generated + generated = is_generated_file(filename, data, regexs) + + basename = os.path.basename(filename) + extension = file_extension(filename) + if generated: + if extension == "go": + extension = "generatego" + elif extension == "bzl": + extension = "generatebzl" + + if extension != "": + ref = refs[extension] + else: + ref = refs[basename] + + # remove extra content from the top of files + if extension == "go" or extension == "generatego": + p = regexs["go_build_constraints"] + (data, found) = p.subn("", data, 1) + elif extension in ["sh", "py"]: + p = regexs["shebang"] + (data, found) = p.subn("", data, 1) + + data = data.splitlines() + + # if our test file is smaller than the reference it surely fails! + if len(ref) > len(data): + print('File %s smaller than reference (%d < %d)' % + (filename, len(data), len(ref)), + file=verbose_out) + return False + + # trim our file to the same number of lines as the reference file + data = data[:len(ref)] + + p = regexs["year"] + for d in data: + if p.search(d): + if generated: + print('File %s has the YEAR field, but it should not be in generated file' % + filename, file=verbose_out) + else: + print('File %s has the YEAR field, but missing the year of date' % + filename, file=verbose_out) + return False + + if not generated: + # Replace all occurrences of the regex "2014|2015|2016|2017|2018" with "YEAR" + p = regexs["date"] + for i, d in enumerate(data): + (data[i], found) = p.subn('YEAR', d) + if found != 0: + break + + # if we don't match the reference at this point, fail + if ref != data: + print("Header in %s does not match reference, diff:" % + filename, file=verbose_out) + if args.verbose: + print(file=verbose_out) + for line in difflib.unified_diff(ref, data, 'reference', filename, lineterm=''): + print(line, file=verbose_out) + print(file=verbose_out) + return False + + return True + + +def file_extension(filename): + return os.path.splitext(filename)[1].split(".")[-1].lower() + + +skipped_dirs = [ + 'cluster/env.sh', + '.git', + '_gopath', + 'hack/boilerplate/test', + '_output', + 'staging/src/k8s.io/kubectl/pkg/generated/bindata.go', + 'test/e2e/generated/bindata.go', + 'third_party', + 'vendor', + '.venv', +] + +# list all the files contain 'DO NOT EDIT', but are not generated +skipped_ungenerated_files = [ + 'hack/lib/swagger.sh', 'tools/boilerplate/boilerplate.py'] + + +def normalize_files(files): + newfiles = [] + for pathname in files: + if any(x in pathname for x in skipped_dirs): + continue + newfiles.append(pathname) + for i, pathname in enumerate(newfiles): + if not os.path.isabs(pathname): + newfiles[i] = os.path.join(args.rootdir, pathname) + return newfiles + + +def get_files(extensions): + files = [] + if len(args.filenames) > 0: + files = args.filenames + else: + for root, dirs, walkfiles in os.walk(args.rootdir): + # don't visit certain dirs. This is just a performance improvement + # as we would prune these later in normalize_files(). But doing it + # cuts down the amount of filesystem walking we do and cuts down + # the size of the file list + for d in skipped_dirs: + if d in dirs: + dirs.remove(d) + + for name in walkfiles: + pathname = os.path.join(root, name) + files.append(pathname) + + files = normalize_files(files) + outfiles = [] + for pathname in files: + basename = os.path.basename(pathname) + extension = file_extension(pathname) + if extension in extensions or basename in extensions: + outfiles.append(pathname) + return outfiles + + +def get_dates(): + years = datetime.datetime.now().year + return '(%s)' % '|'.join((str(year) for year in range(2014, years+1))) + + +def get_regexs(): + regexs = {} + # Search for "YEAR" which exists in the boilerplate, but shouldn't in the real thing + regexs["year"] = re.compile('YEAR') + # get_dates return 2014, 2015, 2016, 2017, or 2018 until the current year as a regex like: "(2014|2015|2016|2017|2018)"; + # company holder names can be anything + regexs["date"] = re.compile(get_dates()) + # strip the following build constraints/tags: + # //go:build + # // +build \n\n + regexs["go_build_constraints"] = re.compile( + r"^(//(go:build| \+build).*\n)+\n", re.MULTILINE) + # strip #!.* from scripts + regexs["shebang"] = re.compile(r"^(#!.*\n)\n*", re.MULTILINE) + # Search for generated files + regexs["generated"] = re.compile('DO NOT EDIT') + return regexs + + +def main(): + regexs = get_regexs() + refs = get_refs() + filenames = get_files(refs.keys()) + + for filename in filenames: + if not file_passes(filename, refs, regexs): + print(filename, file=sys.stdout) + + print("Verified %d file headers match boilerplate" % (len(filenames),), file=sys.stderr) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/boilerplate/verify-boilerplate.sh b/tools/boilerplate/verify-boilerplate.sh new file mode 100755 index 00000000000..ab0c7d798f6 --- /dev/null +++ b/tools/boilerplate/verify-boilerplate.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# Copyright 2014 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -o errexit +set -o nounset +set -o pipefail + +SCRIPT_ROOT=$(dirname "${BASH_SOURCE}")/../.. + +boilerDir="${SCRIPT_ROOT}/tools/boilerplate" +boiler="${boilerDir}/boilerplate.py" + +files_need_boilerplate=($(${boiler} "$@" -v)) + +echo $SCRIPT_ROOT +echo $boilerDir + +# Run boilerplate check +if [[ ${#files_need_boilerplate[@]} -gt 0 ]]; then + for file in "${files_need_boilerplate[@]}"; do + echo "Boilerplate header is wrong for: ${file}" + done + + exit 1 +fi diff --git a/tools/hack/release-manifests.sh b/tools/hack/release-manifests.sh deleted file mode 100755 index 0f501676785..00000000000 --- a/tools/hack/release-manifests.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bash - -set -o errexit -set -o nounset -set -o pipefail - -readonly KUSTOMIZE=${KUSTOMIZE:-tools/bin/kustomize} -readonly GATEWAY_API_VERSION="$1" -readonly TAG="$2" - -mkdir -p release-artifacts/ - -# Wrap sed to deal with GNU and BSD sed flags. -run::sed() { - if sed --version &1 | grep -q GNU; then - # GNU sed - sed -i "$@" - else - # assume BSD sed - sed -i '' "$@" - fi -} - -# Download the supported Gateway API CRDs that will be supported by the release. -curl -sLo release-artifacts/gatewayapi-crds.yaml https://github.com/kubernetes-sigs/gateway-api/releases/download/"${GATEWAY_API_VERSION}"/experimental-install.yaml - -echo "Added:" release-artifacts/gatewayapi-crds.yaml - -# Generate the envoy gateway installation manifest supported by the release. -${KUSTOMIZE} build internal/provider/kubernetes/config/default > release-artifacts/envoy-gateway.yaml -${KUSTOMIZE} build internal/infrastructure/kubernetes/config/rbac > release-artifacts/infra-manager-rbac.yaml -${KUSTOMIZE} build > release-artifacts/install.yaml - -echo "Generated:" release-artifacts/install.yaml - -# Update the image in the Envoy Gateway deployment manifest. -[[ -n "${TAG}" ]] && run::sed \ - "-es|image: envoyproxy/gateway-dev:.*$|image: envoyproxy/gateway:${TAG}|" \ - "release-artifacts/install.yaml" - -echo "Updated the envoy gateway image:" release-artifacts/install.yaml diff --git a/tools/linter/codespell/.codespell.skip b/tools/linter/codespell/.codespell.skip index 0f7ff043e14..319d3a78dfe 100644 --- a/tools/linter/codespell/.codespell.skip +++ b/tools/linter/codespell/.codespell.skip @@ -8,6 +8,7 @@ *.jpg *.ico *.svg +./docs/html/* go.mod go.sum bin diff --git a/tools/make/common.mk b/tools/make/common.mk index fbc6c6fe111..4f84aed0c50 100644 --- a/tools/make/common.mk +++ b/tools/make/common.mk @@ -20,6 +20,8 @@ ROOT_PACKAGE=github.com/envoyproxy/gateway +RELEASE_VERSION=$(shell cat VERSION) + # Set Root Directory Path ifeq ($(origin ROOT_DIR),undefined) ROOT_DIR := $(abspath $(shell pwd -P)) diff --git a/tools/make/docs.mk b/tools/make/docs.mk index 17baf3444d2..c2c9b14897e 100644 --- a/tools/make/docs.mk +++ b/tools/make/docs.mk @@ -1,15 +1,48 @@ -DOCS_DIR := docs DOCS_OUTPUT_DIR := docs/html +RELEASE_VERSIONS ?= $(foreach v,$(wildcard ${ROOT_DIR}/docs/*),$(notdir ${v})) + +##@ Docs .PHONY: docs -docs: $(tools/sphinx-build) $(tools/goversion) - env BUILD_VERSION=$(shell $(tools/goversion)) $(shell go run ./cmd/envoy-gateway versions --env) $(tools/sphinx-build) -j auto -b html $(DOCS_DIR) $(DOCS_OUTPUT_DIR) +docs: docs.clean $(tools/sphinx-build) ## Generate Envoy Gateway Docs Sources + mkdir -p $(DOCS_OUTPUT_DIR) + cp docs/index.html $(DOCS_OUTPUT_DIR)/index.html + @for VERSION in $(RELEASE_VERSIONS); do \ + env BUILD_VERSION=$$VERSION $(shell go run ./cmd/envoy-gateway versions --env) $(tools/sphinx-build) -j auto -b html docs/$$VERSION $(DOCS_OUTPUT_DIR)/$$VERSION; \ + done -.PHONY: docs.clean -docs.clean: ## Clean the built docs - @$(call log, "Cleaning all built docs") - rm -rf $(DOCS_OUTPUT_DIR) +.PHONY: docs-release +docs-release: docs-release-prepare docs-release-gen docs ## Generate Envoy Gateway Release Docs + +.PHONY: docs-serve +docs-serve: ## Start Envoy Gateway Site Locally + python3 -m http.server -d $(DOCS_OUTPUT_DIR) .PHONY: clean clean: ## Remove all files that are created during builds. clean: docs.clean + +.PHONY: docs.clean +docs.clean: + @$(call log, "Cleaning all built docs") + rm -rf $(DOCS_OUTPUT_DIR) + +.PHONY: docs-release-prepare +docs-release-prepare: + mkdir -p $(OUTPUT_DIR) + @echo "\033[36m===========> Updated Release Version: $(TAG)\033[0m" + $(eval LAST_VERSION := $(shell cat VERSION)) + cat docs/index.html | sed "s;$(LAST_VERSION);$(TAG);g" > $(OUTPUT_DIR)/index.html + mv $(OUTPUT_DIR)/index.html docs/index.html + echo $(TAG) > VERSION + +.PHONY: docs-release-gen +docs-release-gen: + @echo "\033[36m===========> Added Release Doc: docs/$(TAG)\033[0m" + cp -r docs/latest docs/$(TAG) + @for DOC in $(shell ls docs/latest/user); do \ + cp docs/$(TAG)/user/$$DOC $(OUTPUT_DIR)/$$DOC ; \ + cat $(OUTPUT_DIR)/$$DOC | sed "s;latest;$(TAG);g" > $(OUTPUT_DIR)/$(TAG)-$$DOC ; \ + mv $(OUTPUT_DIR)/$(TAG)-$$DOC docs/$(TAG)/user/$$DOC ; \ + echo "\033[36m===========> Updated: docs/$(TAG)/user/$$DOC\033[0m" ; \ + done diff --git a/tools/make/golang.mk b/tools/make/golang.mk index e4aaf1cce8b..9bb4a558466 100644 --- a/tools/make/golang.mk +++ b/tools/make/golang.mk @@ -2,6 +2,13 @@ # # All make targets related to golang are defined in this file. +VERSION_PACKAGE := github.com/envoyproxy/gateway/internal/cmd/version + +GO_LDFLAGS += -X $(VERSION_PACKAGE).EnvoyGatewayVersion=$(shell cat VERSION) \ + -X $(VERSION_PACKAGE).GitCommitID=$(GIT_COMMIT) + +GIT_COMMIT:=$(shell git rev-parse HEAD) + GOPATH := $(shell go env GOPATH) ifeq ($(origin GOBIN), undefined) GOBIN := $(GOPATH)/bin @@ -12,7 +19,7 @@ GO_VERSION = $(shell grep -oE "^go [[:digit:]]*\.[[:digit:]]*" go.mod | cut -d' # Build the target binary in target platform. # The pattern of build.% is `build.{Platform}.{Command}`. # If we want to build envoy-gateway in linux amd64 platform, -# just execute make build.linux_amd64.envoy-gateway. +# just execute make go.build.linux_amd64.envoy-gateway. .PHONY: go.build.% go.build.%: $(eval COMMAND := $(word 2,$(subst ., ,$*))) @@ -20,7 +27,7 @@ go.build.%: $(eval OS := $(word 1,$(subst _, ,$(PLATFORM)))) $(eval ARCH := $(word 2,$(subst _, ,$(PLATFORM)))) @$(call log, "Building binary $(COMMAND) with commit $(REV) for $(OS) $(ARCH)") - CGO_ENABLED=0 GOOS=$(OS) GOARCH=$(ARCH) go build -o $(OUTPUT_DIR)/$(OS)/$(ARCH)/$(COMMAND) $(ROOT_PACKAGE)/cmd/$(COMMAND) + CGO_ENABLED=0 GOOS=$(OS) GOARCH=$(ARCH) go build -o $(OUTPUT_DIR)/$(OS)/$(ARCH)/$(COMMAND) -ldflags "$(GO_LDFLAGS)" $(ROOT_PACKAGE)/cmd/$(COMMAND) # Build the envoy-gateway binaries in the hosted platforms. .PHONY: go.build diff --git a/tools/make/kube.mk b/tools/make/kube.mk index c879c88c3c5..1c1b58b8c9f 100644 --- a/tools/make/kube.mk +++ b/tools/make/kube.mk @@ -4,6 +4,10 @@ ENVTEST_K8S_VERSION ?= 1.24.1 # For more details, see https://gateway-api.sigs.k8s.io/guides/getting-started/#installing-gateway-api GATEWAY_API_VERSION ?= $(shell go list -m -f '{{.Version}}' sigs.k8s.io/gateway-api) +GATEWAY_RELEASE_URL ?= https://github.com/kubernetes-sigs/gateway-api/releases/download/${GATEWAY_API_VERSION}/experimental-install.yaml + +CONFORMANCE_UNIQUE_PORTS ?= true + # Set Kubernetes Resources Directory Path ifeq ($(origin KUBE_PROVIDER_DIR),undefined) KUBE_PROVIDER_DIR := $(ROOT_DIR)/internal/provider/kubernetes/config @@ -15,6 +19,8 @@ KUBE_INFRA_DIR := $(ROOT_DIR)/internal/infrastructure/kubernetes/config endif ##@ Kubernetes Development +YEAR := $(shell date +%Y) +CONTROLLERGEN_OBJECT_FLAGS := object:headerFile="$(ROOT_DIR)/tools/boilerplate/boilerplate.generatego.txt",year=$(YEAR) .PHONY: manifests manifests: $(tools/controller-gen) ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. @@ -22,7 +28,8 @@ manifests: $(tools/controller-gen) ## Generate WebhookConfiguration, ClusterRole .PHONY: generate generate: $(tools/controller-gen) ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. - $(tools/controller-gen) object paths="./..." +# Note that the paths can't just be "./..." with the header file, or the tool will panic on run. Sorry. + $(tools/controller-gen) $(CONTROLLERGEN_OBJECT_FLAGS) paths="{$(ROOT_DIR)/api/config/...,$(ROOT_DIR)/internal/ir/...}" .PHONY: kube-test kube-test: manifests generate $(tools/setup-envtest) ## Run Kubernetes provider tests. @@ -31,55 +38,34 @@ kube-test: manifests generate $(tools/setup-envtest) ## Run Kubernetes provider ##@ Kubernetes Deployment ifndef ignore-not-found - ignore-not-found = false + ignore-not-found = true endif -.PHONY: kube-install -kube-install: manifests $(tools/kustomize) ## Install Envoy Gateway CRDs into the Kubernetes cluster specified in ~/.kube/config. - mkdir -pv $(OUTPUT_DIR)/manifests/provider - cp -r $(KUBE_PROVIDER_DIR) $(OUTPUT_DIR)/manifests/provider - mkdir -pv $(OUTPUT_DIR)/manifests/infra - cp -r $(KUBE_INFRA_DIR) $(OUTPUT_DIR)/manifests/infra - $(tools/kustomize) build $(OUTPUT_DIR)/manifests/provider/config/crd | kubectl apply -f - - kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/${GATEWAY_API_VERSION}/experimental-install.yaml - -.PHONY: kube-uninstall -kube-uninstall: manifests $(tools/kustomize) ## Uninstall Envoy Gateway CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. - kubectl delete -f https://github.com/kubernetes-sigs/gateway-api/releases/download/${GATEWAY_API_VERSION}/experimental-install.yaml - .PHONY: kube-deploy -kube-deploy: kube-install ## Install Envoy Gateway controller into the Kubernetes cluster specified in ~/.kube/config. - cd $(OUTPUT_DIR)/manifests/provider/config/envoy-gateway && $(ROOT_DIR)/$(tools/kustomize) edit set image envoyproxy/gateway-dev=$(IMAGE):$(TAG) - $(tools/kustomize) build $(OUTPUT_DIR)/manifests/provider/config/default | kubectl apply -f - - $(tools/kustomize) build $(OUTPUT_DIR)/manifests/infra/config/rbac | kubectl apply -f - +kube-deploy: manifests $(tools/kustomize) generate-manifests ## Install Envoy Gateway into the Kubernetes cluster specified in ~/.kube/config. + kubectl apply -f $(OUTPUT_DIR)/install.yaml .PHONY: kube-undeploy -kube-undeploy: kube-uninstall ## Uninstall the Envoy Gateway controller into the Kubernetes cluster specified in ~/.kube/config. - $(tools/kustomize) build $(OUTPUT_DIR)/manifests/provider/config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f - - rm -rf $(OUTPUT_DIR)/manifests/provider - rm -rf $(OUTPUT_DIR)/manifests/infra +kube-undeploy: manifests $(tools/kustomize) ## Uninstall the Envoy Gateway into the Kubernetes cluster specified in ~/.kube/config. + kubectl delete --ignore-not-found=$(ignore-not-found) -f $(OUTPUT_DIR)/install.yaml .PHONY: kube-demo kube-demo: ## Deploy a demo backend service, gatewayclass, gateway and httproute resource and test the configuration. - kubectl apply -f examples/kubernetes/httpbin.yaml - kubectl apply -f examples/kubernetes/gatewayclass.yaml - kubectl apply -f examples/kubernetes/gateway.yaml - kubectl apply -f examples/kubernetes/httproute.yaml + kubectl apply -f examples/kubernetes/quickstart.yaml + $(eval ENVOY_SERVICE := $(shell 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}')) @echo "\nPort forward to the Envoy service using the command below" - @echo "kubectl -n envoy-gateway-system port-forward service/envoy-default-eg 8888:8080 &" + @echo 'kubectl -n envoy-gateway-system port-forward service/$(ENVOY_SERVICE) 8888:8080 &' @echo "\nCurl the app through Envoy proxy using the command below" @echo "curl --verbose --header \"Host: www.example.com\" http://localhost:8888/get\n" .PHONY: kube-demo-undeploy kube-demo-undeploy: ## Uninstall the Kubernetes resources installed from the `make kube-demo` command. - kubectl delete -f examples/kubernetes/httproute.yaml - kubectl delete -f examples/kubernetes/gateway.yaml - kubectl delete -f examples/kubernetes/gatewayclass.yaml - kubectl delete -f examples/kubernetes/httpbin.yaml + kubectl delete -f examples/kubernetes/quickstart.yaml --ignore-not-found=$(ignore-not-found) -.PHONY: run-kube-local -run-kube-local: build kube-install ## Run Envoy Gateway locally. - tools/hack/run-kube-local.sh +# Uncomment when https://github.com/envoyproxy/gateway/issues/256 is fixed. +#.PHONY: run-kube-local +#run-kube-local: build kube-install ## Run Envoy Gateway locally. +# tools/hack/run-kube-local.sh .PHONY: conformance conformance: create-cluster kube-install-image kube-deploy run-conformance delete-cluster ## Create a kind cluster, deploy EG into it, run Gateway API conformance, and clean up. @@ -97,12 +83,35 @@ run-conformance: ## Run Gateway API conformance. kubectl wait --timeout=5m -n gateway-system deployment/gateway-api-admission-server --for=condition=Available kubectl wait --timeout=5m -n envoy-gateway-system deployment/envoy-gateway --for=condition=Available kubectl apply -f internal/provider/kubernetes/config/samples/gatewayclass.yaml - go test -v -tags conformance ./test/conformance --gateway-class=envoy-gateway --debug=true + go test -v -tags conformance ./test/conformance --gateway-class=envoy-gateway --debug=true --use-unique-ports=$(CONFORMANCE_UNIQUE_PORTS) .PHONY: delete-cluster delete-cluster: $(tools/kind) ## Delete kind cluster. $(tools/kind) delete cluster --name envoy-gateway -.PHONY: release-manifests -release-manifests: $(tools/kustomize) ## Generate Kubernetes release manifests. - tools/hack/release-manifests.sh $(GATEWAY_API_VERSION) $(TAG) +.PHONY: generate-manifests +generate-manifests: $(tools/kustomize) ## Generate Kubernetes release manifests. + @echo "\033[36m===========> Generating kubernetes manifests\033[0m" + mkdir -p $(OUTPUT_DIR)/ + curl -sLo $(OUTPUT_DIR)/gatewayapi-crds.yaml ${GATEWAY_RELEASE_URL} + @echo "\033[36m===========> Added: $(OUTPUT_DIR)/gatewayapi-crds.yaml\033[0m" + mkdir -pv $(OUTPUT_DIR)/manifests/provider + cp -r $(KUBE_PROVIDER_DIR) $(OUTPUT_DIR)/manifests/provider + mkdir -pv $(OUTPUT_DIR)/manifests/infra + cp -r $(KUBE_INFRA_DIR) $(OUTPUT_DIR)/manifests/infra + cd $(OUTPUT_DIR)/manifests/provider/config/envoy-gateway && $(ROOT_DIR)/$(tools/kustomize) edit set image envoyproxy/gateway-dev=$(IMAGE):$(TAG) + $(tools/kustomize) build $(OUTPUT_DIR)/manifests/provider/config/default > $(OUTPUT_DIR)/envoy-gateway.yaml + $(tools/kustomize) build $(OUTPUT_DIR)/manifests/infra/config/rbac > $(OUTPUT_DIR)/infra-manager-rbac.yaml + touch $(OUTPUT_DIR)/kustomization.yaml + cd $(OUTPUT_DIR) && $(ROOT_DIR)/$(tools/kustomize) edit add resource ./envoy-gateway.yaml + cd $(OUTPUT_DIR) && $(ROOT_DIR)/$(tools/kustomize) edit add resource ./infra-manager-rbac.yaml + cd $(OUTPUT_DIR) && $(ROOT_DIR)/$(tools/kustomize) edit add resource ./gatewayapi-crds.yaml + $(tools/kustomize) build $(OUTPUT_DIR) > $(OUTPUT_DIR)/install.yaml + @echo "\033[36m===========> Added: $(OUTPUT_DIR)/install.yaml\033[0m" + cp examples/kubernetes/quickstart.yaml $(OUTPUT_DIR)/quickstart.yaml + @echo "\033[36m===========> Added: $(OUTPUT_DIR)/quickstart.yaml\033[0m" + +.PHONY: generate-artifacts +generate-artifacts: generate-manifests ## Generate release artifacts. + cp -r $(ROOT_DIR)/release-notes/$(TAG).yaml $(OUTPUT_DIR)/release-notes.yaml + @echo "\033[36m===========> Added: $(OUTPUT_DIR)/release-notes.yaml\033[0m" diff --git a/tools/make/lint.mk b/tools/make/lint.mk index ba00b6a1296..465a2181a9b 100644 --- a/tools/make/lint.mk +++ b/tools/make/lint.mk @@ -58,12 +58,13 @@ lint.whitenoise: $(tools/whitenoise) @echo Running WhiteNoise linter ... $(tools/whitenoise) -# GitHub has shellcheck pre-installed + .PHONY: lint.shellcheck lint: lint.shellcheck -lint.shellcheck: +lint-deps: $(tools/shellcheck) +lint.shellcheck: $(tools/shellcheck) @echo Running Shellcheck linter ... - @shellcheck tools/hack/*.sh + $(tools/shellcheck) tools/hack/*.sh .PHONY: gen-check gen-check: generate manifests @@ -71,3 +72,7 @@ gen-check: generate manifests echo "\nERROR: Some files need to be updated, please run 'make generate' and 'make manifests' to include any changed files to your PR\n"; \ git diff --exit-code; \ fi + +.PHONY: licensecheck +licensecheck: ## Check license headers are present. + tools/boilerplate/verify-boilerplate.sh diff --git a/tools/make/tools.mk b/tools/make/tools.mk index 2d52dceeaf4..d945caf79e8 100644 --- a/tools/make/tools.mk +++ b/tools/make/tools.mk @@ -14,7 +14,6 @@ $(tools.bindir)/%: $(tools.srcdir)/%.sh # tools/controller-gen = $(tools.bindir)/controller-gen tools/golangci-lint = $(tools.bindir)/golangci-lint -tools/goversion = $(tools.bindir)/goversion tools/kustomize = $(tools.bindir)/kustomize tools/kind = $(tools.bindir)/kind tools/setup-envtest = $(tools.bindir)/setup-envtest @@ -33,3 +32,23 @@ $(tools.bindir)/%.d/venv: $(tools.srcdir)/%/requirements.txt $@/bin/pip3 install -r $< || (rm -rf $@; exit 1) $(tools.bindir)/%: $(tools.bindir)/%.d/venv ln -sf $*.d/venv/bin/$* $@ + +ifneq ($(GOOS),windows) +# Shellcheck +# ========== +# +tools/shellcheck = $(tools.bindir)/shellcheck +SHELLCHECK_VERSION=0.8.0 +SHELLCHECK_ARCH=$(shell uname -m) +# shellcheck uses the same binary on Intel and Apple Silicon Mac. +ifeq ($(GOOS),darwin) +SHELLCHECK_ARCH=x86_64 +endif +SHELLCHECK_TXZ = https://github.com/koalaman/shellcheck/releases/download/v$(SHELLCHECK_VERSION)/shellcheck-v$(SHELLCHECK_VERSION).$(GOOS).$(SHELLCHECK_ARCH).tar.xz +tools/bin/$(notdir $(SHELLCHECK_TXZ)): + mkdir -p $(@D) + curl -sfL $(SHELLCHECK_TXZ) -o $@ +%/bin/shellcheck: %/bin/$(notdir $(SHELLCHECK_TXZ)) + mkdir -p $(@D) + tar -C $(@D) -Jxmf $< --strip-components=1 shellcheck-v$(SHELLCHECK_VERSION)/shellcheck +endif diff --git a/tools/src/controller-gen/pin.go b/tools/src/controller-gen/pin.go index 0eb2187c6e5..5140b3786b5 100644 --- a/tools/src/controller-gen/pin.go +++ b/tools/src/controller-gen/pin.go @@ -1,3 +1,8 @@ +// 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 diff --git a/tools/src/golangci-lint/pin.go b/tools/src/golangci-lint/pin.go index d250d0323ed..1500aa4c7b9 100644 --- a/tools/src/golangci-lint/pin.go +++ b/tools/src/golangci-lint/pin.go @@ -1,3 +1,8 @@ +// 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 diff --git a/tools/src/goversion/go.mod b/tools/src/goversion/go.mod deleted file mode 100644 index 68a8691075e..00000000000 --- a/tools/src/goversion/go.mod +++ /dev/null @@ -1,13 +0,0 @@ -module local - -go 1.19 - -require github.com/emissary-ingress/goversion v0.0.0-20220825220041-6870ba273e76 - -require ( - github.com/inconshreveable/mousetrap v1.0.0 // indirect - github.com/spf13/cobra v1.5.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - golang.org/x/mod v0.5.1 // indirect - golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 // indirect -) diff --git a/tools/src/goversion/go.sum b/tools/src/goversion/go.sum deleted file mode 100644 index b1054153db3..00000000000 --- a/tools/src/goversion/go.sum +++ /dev/null @@ -1,16 +0,0 @@ -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/emissary-ingress/goversion v0.0.0-20220825220041-6870ba273e76 h1:r5LJ+pvXBngyIBaL9u10pXNfd332eD5JfjR5duY8TEk= -github.com/emissary-ingress/goversion v0.0.0-20220825220041-6870ba273e76/go.mod h1:O3FGqM30w7Xc2n6cZg7mdZa6o+u1AsxEfcdnqKNFDic= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= -github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= -golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/tools/src/goversion/pin.go b/tools/src/goversion/pin.go deleted file mode 100644 index 662fbb00bd2..00000000000 --- a/tools/src/goversion/pin.go +++ /dev/null @@ -1,6 +0,0 @@ -//go:build pin -// +build pin - -package ignore - -import "github.com/emissary-ingress/goversion" diff --git a/tools/src/kind/pin.go b/tools/src/kind/pin.go index 20964c929c6..5180e168384 100644 --- a/tools/src/kind/pin.go +++ b/tools/src/kind/pin.go @@ -1,3 +1,8 @@ +// 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 diff --git a/tools/src/kustomize/pin.go b/tools/src/kustomize/pin.go index 71820da506a..f871f2f9b93 100644 --- a/tools/src/kustomize/pin.go +++ b/tools/src/kustomize/pin.go @@ -1,6 +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/kustomize/kustomize/v3" +import "sigs.k8s.io/kustomize/kustomize/v3" \ No newline at end of file diff --git a/tools/src/setup-envtest/pin.go b/tools/src/setup-envtest/pin.go index c1aef52f066..69d708e7ea9 100644 --- a/tools/src/setup-envtest/pin.go +++ b/tools/src/setup-envtest/pin.go @@ -1,3 +1,8 @@ +// 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 diff --git a/tools/src/sphinx-build/requirements.txt b/tools/src/sphinx-build/requirements.txt index baa05d40d5c..de4d2c42f4e 100644 --- a/tools/src/sphinx-build/requirements.txt +++ b/tools/src/sphinx-build/requirements.txt @@ -1 +1,2 @@ Sphinx==5.1.1 +myst-parser==0.18.1 \ No newline at end of file