From 946a64f5b6442a5ffe042a82cbef8fc74940c0bd Mon Sep 17 00:00:00 2001 From: Zijun Wang Date: Thu, 23 May 2024 16:37:18 -0700 Subject: [PATCH 1/6] TLS_PASSTHROUGH document --- .github/workflows/publish-doc.yaml | 17 +- .gitignore | 2 +- docs/api-types/target-group-policy.md | 22 +- docs/api-types/tls-route.md | 62 ++ docs/guides/tls-passthrough.md | 257 +++++ docs/images/tlsroute-multi-cluster.png | Bin 0 -> 34832 bytes .../examples/my-gateway-tls-passthrough.yaml | 15 + .../nginx-server-tls-passthrough.yaml | 33 + files/examples/rate-tlsroute-bluegreen.yaml | 20 + files/examples/tls-rate1.yaml | 36 + files/examples/tls-rate2-export.yaml | 6 + files/examples/tls-rate2-import.yaml | 9 + .../examples/tls-rate2-targetgrouppolicy.yaml | 12 + files/examples/tls-rate2.yaml | 36 + files/examples/tlsroute-nginx.yaml | 15 + .../gateway.networking.k8s.io_tlsroutes.yaml | 894 ++++++++++++++++++ mkdocs.yml | 3 +- .../client/gomock_reflect_318529362/prog.go | 64 -- 18 files changed, 1422 insertions(+), 81 deletions(-) create mode 100644 docs/api-types/tls-route.md create mode 100644 docs/guides/tls-passthrough.md create mode 100644 docs/images/tlsroute-multi-cluster.png create mode 100644 files/examples/my-gateway-tls-passthrough.yaml create mode 100644 files/examples/nginx-server-tls-passthrough.yaml create mode 100644 files/examples/rate-tlsroute-bluegreen.yaml create mode 100644 files/examples/tls-rate1.yaml create mode 100644 files/examples/tls-rate2-export.yaml create mode 100644 files/examples/tls-rate2-import.yaml create mode 100644 files/examples/tls-rate2-targetgrouppolicy.yaml create mode 100644 files/examples/tls-rate2.yaml create mode 100644 files/examples/tlsroute-nginx.yaml create mode 100644 helm/crds/gateway.networking.k8s.io_tlsroutes.yaml delete mode 100644 mocks/controller-runtime/client/gomock_reflect_318529362/prog.go diff --git a/.github/workflows/publish-doc.yaml b/.github/workflows/publish-doc.yaml index 77c3d70f..4d32a4d6 100644 --- a/.github/workflows/publish-doc.yaml +++ b/.github/workflows/publish-doc.yaml @@ -4,6 +4,7 @@ on: push: branches: - main + - 'release-v*.*.*' jobs: publish-docs: runs-on: ubuntu-latest @@ -24,10 +25,14 @@ jobs: run: | python -m pip install --upgrade pip pip install mkdocs-material mike - - name: Build + - name: Deploy to Mike run: | - mike deploy 1.0.5 latest --update-aliases --push - mike set-default latest --allow-empty --push - - - + if [[ ${{ github.ref }} == refs/heads/main ]]; then + # Deploy to the mike doc version `dev` and update the `latest` alias for the main branch new git commits + mike deploy dev latest --update-aliases --push + elif [[ ${{ github.ref }} == refs/heads/release-v* ]]; then + # Deploy to the mike doc version `vx.x.x` for the new git branches `release-vx.x.x` + branch_name=${{ github.ref }} + version=${branch_name##refs/heads/release-} + mike deploy $version --push + fi diff --git a/.gitignore b/.gitignore index 28cbe8f4..0c239275 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,7 @@ go.work* # gomock generated prog.go pkg/aws/services/gomock_reflect_* - +mocks/controller-runtime/client/gomock_reflect_* pkg/**/prog.* # Image build tarballed bundles diff --git a/docs/api-types/target-group-policy.md b/docs/api-types/target-group-policy.md index 5601ecd6..93acfc0c 100644 --- a/docs/api-types/target-group-policy.md +++ b/docs/api-types/target-group-policy.md @@ -3,35 +3,39 @@ ## Introduction By default, AWS Gateway API Controller assumes plaintext HTTP/1 traffic for backend Kubernetes resources. -TargetGroupPolicy is a CRD that can be attached to a Service, which allows the users to define protocol and -health check configurations of those backend resources. +TargetGroupPolicy is a CRD that can be attached to Service or ServiceExport, which allows the users to define protocol, protocol version and +health check configurations of those backend resources. When attaching a policy to a resource, the following restrictions apply: -- A policy can be only attached to `Service` resources. -- The attached resource can only be `backendRef` of `HTTPRoute` and `GRPCRoute`. +- A policy can be attached to `Service` that being `backendRef` of `HTTPRoute`, `GRPCRoute` and `TLSRoute`. +- A policy can be attached to `ServiceExport`. - The attached resource should exist in the same namespace as the policy resource. The policy will not take effect if: - - The resource does not exist - The resource is not referenced by any route - The resource is referenced by a route of unsupported type +- The ProtocolVersion is non-empty if the TargetGroupPolicy protocol is TCP + +Please check the TargetGroupPolicy API Reference for more details. [TargetGroupPolicy API Reference](../api-reference.md#application-networking.k8s.aws/v1alpha1.TargetGroupPolicy) + These restrictions are not forced; for example, users may create a policy that targets a service that is not created yet. However, the policy will not take effect unless the target is valid. + + ### Limitations and Considerations -- Attaching TargetGroupPolicy to a resource that is already referenced by a route will result in a replacement +- Attaching TargetGroupPolicy to a Service that is already referenced by a route will result in a replacement of VPC Lattice TargetGroup resource, except for health check updates. +- Attaching TargetGroupPolicy to a ServiceExport will result in a replacement of VPC Lattice TargetGroup resource, except for health check updates. - Removing TargetGroupPolicy of a resource will roll back protocol configuration to default setting. (HTTP1/HTTP plaintext) ## Example Configuration -This will enable TLS traffic between the gateway and Kubernetes service, with customized health check configuration. - -Note that the TLS traffic is always terminated at the gateway, so it will be re-encrypted in this case. The gateway does not perform any certificate validations to the certificate on targets. +This will enable HTTPS traffic between the gateway and Kubernetes service, with customized health check configuration. ``` apiVersion: application-networking.k8s.aws/v1alpha1 diff --git a/docs/api-types/tls-route.md b/docs/api-types/tls-route.md new file mode 100644 index 00000000..0f0fd77f --- /dev/null +++ b/docs/api-types/tls-route.md @@ -0,0 +1,62 @@ +# TLSRoute API Reference + +## Introduction + +With integration of the Gateway API, AWS Gateway API Controller supports `TLSRoute`. +This allows you to define and manage end-to-end TLS encrypted traffic routing to your Kubernetes clusters. + +### TLSRoute Key Features & Limitations + +**Features**: + +- **Routing Traffic**: Enables routing end-to-end TLS encrypted traffic from your client workload to server workload. + + +**Limitations**: + +- **Listener Protocol**: The `TLSRoute` sectionName must refer to an TLS protocol listener with mode: Passthrough in the parent `Gateway`. + +- `TLSRoute` only supports to have one rule. +- `TLSRoute` don't support `matches` field in the rule. +- The `hostnames` field with exactly one host name is required. This domain name is used as a vpc lattice's Service Name Indication (SNI) match. + + +## Example Configuration + +Here is a sample configuration that demonstrates how to set up a `TLSRoute` resource to route end-to-end TLS encrypted traffic to a nginx service: + +```yaml +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: TLSRoute +metadata: + name: nginx-tls-route +spec: + hostnames: + - nginx-test.my-test.com + parentRefs: + - name: my-hotel-tls-passthrough + sectionName: tls + rules: + - backendRefs: + - name: nginx-tls + kind: Service + port: 443 +``` + +In this example: + +- The `TLSRoute` is named ` nginx-tls-route` and is associated with a parent gateway named `my-hotel-tls-passthrough` that has + a listener section named `tls`: +``` + - name: tls + protocol: TLS + port: 443 + tls: + mode: Passthrough +``` +- The `TLSRoute` is configured to route traffic to a k8s service named `nginx-tls` on port 443. +- The `hostnames` field is set to `nginx-test.my-test.com`. The customer must use this domain name to send traffic to the nginx service. + +This `TLSRoute` documentation provides a detailed introduction, feature set, and a basic example of how to configure +and use the resource within AWS Gateway API Controller project. For in-depth details and specifications, you can refer to the +official [Gateway API documentation](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.TLSRoute). \ No newline at end of file diff --git a/docs/guides/tls-passthrough.md b/docs/guides/tls-passthrough.md new file mode 100644 index 00000000..ef9abd3f --- /dev/null +++ b/docs/guides/tls-passthrough.md @@ -0,0 +1,257 @@ +# TLS Passthrough Support + +[Kubernetes Gateway API](https://gateway-api.sigs.k8s.io/guides/tls/) lays out the general guidelines on how to configure TLS passthrough. Here are examples on how to use them against AWS Gateway Api controller and VPC Lattice. + +## Install Gateway API TLSRoute CRD + +The TLSRoute CRD already included in the helm chart and deployment.yaml, If you are using these 2 methods to install the controller no extra steps are needed. +If you want to install the CRD manually by yourself: +``` +# Install CRD +kubectl apply -f config/crds/bases/gateway.networking.k8s.io_tlsroutes.yaml +# Verfiy TLSRoute CRD +kubectl get crd tlsroutes.gateway.networking.k8s.io +NAME CREATED AT +tlsroutes.gateway.networking.k8s.io 2024-03-07T23:16:22Z +``` + +## Setup TLS Passthrough Connectivity in a single cluster + +### 1. Configure TLS Passthrough Listener on Gateway + +``` +kubectl apply -f files/examples/gateway-tls-passthrough.yaml +``` + +``` +# tls listener config snips: +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: Gateway +metadata: + name: my-hotel-tls-passthrough +spec: + gatewayClassName: amazon-vpc-lattice + listeners: + ... + - name: tls + protocol: TLS + port: 443 + tls: + mode: Passthrough + ... +``` + +### 2. Configure TLSRoute + +``` +# Suppose in the below example, we use the "parking" service as the client pod to test the TLS passthrough traffic. +kubectl apply -f files/examples/parking.yaml + +# Configure nginx backend service (This nginx image includes a self-signed certificate) +kubectl apply -f files/example/nginx-server-tls-passthrough.yaml + +# configure nginx tls route +kubectl apply -f files/examples/tlsroute-nginx.yaml + +``` + +### 3. Verify the controller has reconciled nginx-tls route + +Make sure the TLSRoute has the `application-networking.k8s.aws/lattice-assigned-domain-name` annotation and status `Accepted: True` +``` +kubectl get tlsroute nginx-tls -o yaml +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: TLSRoute +metadata: + annotations: + application-networking.k8s.aws/lattice-assigned-domain-name: nginx-tls-default-0af995120af2711bc.7d67968.vpc-lattice-svcs.us-west-2.on.aws + ... + name: nginx-tls + namespace: default + ... + +status: + parents: + - conditions: + - lastTransitionTime: ..... + message: "" + observedGeneration: 1 + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: ..... + message: "" + observedGeneration: 1 + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: application-networking.k8s.aws/gateway-api-controller + +``` + +### 4. Verify TLS Passthrough Traffic + +``` +kubectl get deployment nginx-tls +NAME READY UP-TO-DATE AVAILABLE AGE +nginx-tls 2/2 2 2 1d + +kubectl exec deployments/parking -- curl -kv https://nginx-test.my-test.com --resolve nginx-test.my-test.com:443:169.254.171.0 + +* Trying 169.254.171.0:443... +* Connected to nginx-test.my-test.com (169.254.171.0) port 443 (#0) +* ALPN, offering h2 +* ALPN, offering http/1.1 +* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH +* successfully set certificate verify locations: +* CAfile: /etc/pki/tls/certs/ca-bundle.crt +* CApath: none +* TLSv1.2 (OUT), TLS header, Certificate Status (22): +* TLSv1.2 (OUT), TLS handshake, Client hello (1): +* TLSv1.2 (IN), TLS handshake, Server hello (2): +* TLSv1.2 (IN), TLS handshake, Certificate (11): +* TLSv1.2 (IN), TLS handshake, Server key exchange (12): +* TLSv1.2 (IN), TLS handshake, Server finished (14): +* TLSv1.2 (OUT), TLS handshake, Client key exchange (16): +* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1): +* TLSv1.2 (OUT), TLS handshake, Finished (20): +* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1): +* TLSv1.2 (IN), TLS handshake, Finished (20): +* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384 +* ALPN, server accepted to use h2 +* Server certificate: +* subject: C=US; ST=wa; L=seattle; O=aws; OU=lattice; CN=liwen.ssl-test.com; emailAddress=liwenwu@amazon.com +* start date: Mar 5 21:26:24 2024 GMT +# use customer defined name +curl -k -v https://nginx-test.my-test.com --resolve nginx-test.my-test.com:443:169.254.171.32 +* Added nginx-test.my-test.com:443:169.254.171.32 to DNS cache +* Hostname nginx-test.my-test.com was found in DNS cache +* Trying 169.254.171.0:443... +* Connected to nginx-test.my-test.com (169.254.171.0) port 443 (#0) +* ALPN, offering h2 +* ALPN, offering http/1.1 +* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH +* successfully set certificate verify locations: +* CAfile: /etc/pki/tls/certs/ca-bundle.crt +* CApath: none +* TLSv1.2 (OUT), TLS header, Certificate Status (22): +* TLSv1.2 (OUT), TLS handshake, Client hello (1): +* TLSv1.2 (IN), TLS handshake, Server hello (2): +* TLSv1.2 (IN), TLS handshake, Certificate (11): +* TLSv1.2 (IN), TLS handshake, Server key exchange (12): +* TLSv1.2 (IN), TLS handshake, Server finished (14): +* TLSv1.2 (OUT), TLS handshake, Client key exchange (16): +* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1): +* TLSv1.2 (OUT), TLS handshake, Finished (20): +* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1): +* TLSv1.2 (IN), TLS handshake, Finished (20): +* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384 +* ALPN, server accepted to use h2 +* Server certificate: +* subject: C=US; ST=wa; L=seattle; O=aws; OU=lattice; CN=liwen.ssl-test.com; emailAddress=liwenwu@amazon.com + +.... + +

Welcome to nginx!

+

If you see this page, the nginx web server is successfully installed and +working. Further configuration is required.

+.... + +``` + +## Setup TLS Passthrough Connectivity spanning multiple clusters + + +![tlsoute multi cluster](../images/tlsroute-multi-cluster.png) + +### 1. In this example we still use the "parking" Kubernetes service as the client pod to test the cross cluster TLS passthrough traffic. +``` +kubectl apply -f files/examples/parking.yaml +``` + +### 2. In cluster-1, create `tls-rate1` Kubernetes Service: +``` +kubectl apply -f files/examples/tls-rate1.yaml +``` + +### 3. Configure ServieExport with TargetGroupPolicy `protocol:TCP` in cluster-2 + +``` +# Create tls-rate2 Kubernetes Service in cluster-2 +kubectl apply -f files/examples/tls-rate2.yaml +# Create serviceexport in cluster-2 +kubectl apply -f files/examples/tls-rate2-export.yaml +# Create targetgroup policy to configure TCP protocol for tls-rate2 in cluster-2 +kubectl apply -f files/examples/tls-rate2-targetgrouppolicy.yaml +``` + +``` +# Snips of serviceexport config +apiVersion: application-networking.k8s.aws/v1alpha1 +kind: ServiceExport +metadata: + name: tls-rate-2 + annotations: + application-networking.k8s.aws/federation: "amazon-vpc-lattice" +# Snips of targetgroup policy config +apiVersion: application-networking.k8s.aws/v1alpha1 +kind: TargetGroupPolicy +metadata: + name: tls-rate2 +spec: + targetRef: + group: "application-networking.k8s.aws" + kind: ServiceExport + name: tls-rate2 + protocol: TCP +``` + +### 4. Configure ServiceImport in cluster1 + +``` +kubectl apply -f files/examples/tls-rate2-import.yaml +``` + +### 5. Configure TLSRoute for bluegreen deployment + +``` +kubectl apply -f files/examples/rate-tlsroute-bluegreen.yaml + +# snips of TLSRoute span multiple Kubernetes Clusters +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: TLSRoute +metadata: + name: tls-rate +spec: + hostnames: + - tls-rate.my-test.com + parentRefs: + - name: my-hotel-tls + sectionName: tls + rules: + - backendRefs: + - name: tls-rate1 <---------- to Kubernetes Cluster-1 + kind: Service + port: 443 + weight: 10 + - name: tls-rate2 <---------- to Kubernetes Cluster-2 + kind: ServiceImport + port: 443 + weight: 90 +``` +### 6. Verify cross-cluster TLS passthrough traffic + +Expected to receive the weighted traffic route to tls-rate1 service(10%) and tls-rate2 service(90%), if you curl the `tls-rate.my-test.com` from the client pod multiple times: +``` +kubectl exec deploy/parking -- sh -c 'for ((i=1; i<=30; i++)); do curl -k https://tls-rate.my-test.com --resolve tls-rate.my-test.com:443:169.254.171.0 2>/dev/null; done' + +Requsting to TLS Pod(tls-rate2-7f8b9cc97b-fgqk6): tls-rate2 handler pod <----> k8s service in cluster-2 +Requsting to TLS Pod(tls-rate2-7f8b9cc97b-fgqk6): tls-rate2 handler pod +Requsting to TLS Pod(tls-rate2-7f8b9cc97b-fgqk6): tls-rate2 handler pod +Requsting to TLS Pod(tls-rate2-7f8b9cc97b-fgqk6): tls-rate2 handler pod +Requsting to TLS Pod(tls-rate1-98cc7fd87a-642zw): tls-rate1 handler pod <----> k8s service in cluster-1 +Requsting to TLS Pod(tls-rate2-7f8b9cc97b-fgqk6): tls-rate2 handler pod +Requsting to TLS Pod(tls-rate2-7f8b9cc97b-fgqk6): tls-rate2 handler pod +Requsting to TLS Pod(tls-rate2-7f8b9cc97b-fgqk6): tls-rate2 handler pod +Requsting to TLS Pod(tls-rate1-98cc7fd87a-642zw): tls-rate1 handler pod +``` diff --git a/docs/images/tlsroute-multi-cluster.png b/docs/images/tlsroute-multi-cluster.png new file mode 100644 index 0000000000000000000000000000000000000000..ba7b6c28ec0a9ea73b8a148b240b5d38dbaec0cd GIT binary patch literal 34832 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 Q!2ti%m9>?s6s$u32NZS)NB{r; literal 0 HcmV?d00001 diff --git a/files/examples/my-gateway-tls-passthrough.yaml b/files/examples/my-gateway-tls-passthrough.yaml new file mode 100644 index 00000000..79cff561 --- /dev/null +++ b/files/examples/my-gateway-tls-passthrough.yaml @@ -0,0 +1,15 @@ +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: Gateway +metadata: + name: my-hotel-tls-passthrough +spec: + gatewayClassName: amazon-vpc-lattice + listeners: + - name: http + protocol: HTTP + port: 80 + - name: tls + protocol: TLS + port: 443 + tls: + mode: Passthrough \ No newline at end of file diff --git a/files/examples/nginx-server-tls-passthrough.yaml b/files/examples/nginx-server-tls-passthrough.yaml new file mode 100644 index 00000000..4833d8cb --- /dev/null +++ b/files/examples/nginx-server-tls-passthrough.yaml @@ -0,0 +1,33 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-tls +spec: + selector: + matchLabels: + app: nginx-tls + replicas: 2 + template: + metadata: + labels: + app: nginx-tls + spec: + containers: + - name: nginx-tls + image: public.ecr.aws/x2j8p8w7/lattice-test-server:latest + ports: + - containerPort: 443 + +--- +apiVersion: v1 +kind: Service +metadata: + name: nginx-tls +spec: + selector: + app: nginx-tls + ports: + - protocol: TCP + port: 443 + targetPort: 443 + diff --git a/files/examples/rate-tlsroute-bluegreen.yaml b/files/examples/rate-tlsroute-bluegreen.yaml new file mode 100644 index 00000000..9a7a02eb --- /dev/null +++ b/files/examples/rate-tlsroute-bluegreen.yaml @@ -0,0 +1,20 @@ +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: TLSRoute +metadata: + name: rate-tls-passthrough +spec: + hostnames: + - tls-rate.my-test.com + parentRefs: + - name: my-hotel-tls-passthrough + sectionName: tls + rules: + - backendRefs: + - name: tls-rate1 + kind: Service + port: 443 + weight: 10 + - name: tls-rate2 + kind: ServiceImport + port: 443 + weight: 90 \ No newline at end of file diff --git a/files/examples/tls-rate1.yaml b/files/examples/tls-rate1.yaml new file mode 100644 index 00000000..677600ed --- /dev/null +++ b/files/examples/tls-rate1.yaml @@ -0,0 +1,36 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: tls-rate1 + labels: + app: tls-rate1 +spec: + replicas: 2 + selector: + matchLabels: + app: tls-rate1 + template: + metadata: + labels: + app: tls-rate1 + spec: + containers: + - name: tls-rate1 + image: public.ecr.aws/x2j8p8w7/https-server:latest + env: + - name: PodName + value: "tls-rate1 handler pod" + + +--- +apiVersion: v1 +kind: Service +metadata: + name: tls-rate1 +spec: + selector: + app: tls-rate1 + ports: + - protocol: TCP + port: 443 + targetPort: 443 \ No newline at end of file diff --git a/files/examples/tls-rate2-export.yaml b/files/examples/tls-rate2-export.yaml new file mode 100644 index 00000000..352944fc --- /dev/null +++ b/files/examples/tls-rate2-export.yaml @@ -0,0 +1,6 @@ +apiVersion: application-networking.k8s.aws/v1alpha1 +kind: ServiceExport +metadata: + name: tls-rate2 + annotations: + application-networking.k8s.aws/federation: "amazon-vpc-lattice" \ No newline at end of file diff --git a/files/examples/tls-rate2-import.yaml b/files/examples/tls-rate2-import.yaml new file mode 100644 index 00000000..3faf33e4 --- /dev/null +++ b/files/examples/tls-rate2-import.yaml @@ -0,0 +1,9 @@ +apiVersion: application-networking.k8s.aws/v1alpha1 +kind: ServiceImport +metadata: + name: tls-rate2 +spec: + type: ClusterSetIP + ports: + - port: 443 + protocol: TCP \ No newline at end of file diff --git a/files/examples/tls-rate2-targetgrouppolicy.yaml b/files/examples/tls-rate2-targetgrouppolicy.yaml new file mode 100644 index 00000000..72338ce9 --- /dev/null +++ b/files/examples/tls-rate2-targetgrouppolicy.yaml @@ -0,0 +1,12 @@ +apiVersion: application-networking.k8s.aws/v1alpha1 +kind: TargetGroupPolicy +metadata: + name: tls-rate2 +spec: + targetRef: + group: "application-networking.k8s.aws" + kind: ServiceExport + name: tls-rate2 + protocol: TCP + healthCheck: + enabled: false \ No newline at end of file diff --git a/files/examples/tls-rate2.yaml b/files/examples/tls-rate2.yaml new file mode 100644 index 00000000..c41dffa9 --- /dev/null +++ b/files/examples/tls-rate2.yaml @@ -0,0 +1,36 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: tls-rate2 + labels: + app: tls-rate2 +spec: + replicas: 2 + selector: + matchLabels: + app: tls-rate2 + template: + metadata: + labels: + app: tls-rate2 + spec: + containers: + - name: tls-rate2 + image: public.ecr.aws/x2j8p8w7/https-server:latest + env: + - name: PodName + value: "tls-rate2 handler pod" + + +--- +apiVersion: v1 +kind: Service +metadata: + name: tls-rate2 +spec: + selector: + app: tls-rate2 + ports: + - protocol: TCP + port: 443 + targetPort: 443 \ No newline at end of file diff --git a/files/examples/tlsroute-nginx.yaml b/files/examples/tlsroute-nginx.yaml new file mode 100644 index 00000000..ff63874f --- /dev/null +++ b/files/examples/tlsroute-nginx.yaml @@ -0,0 +1,15 @@ +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: TLSRoute +metadata: + name: nginx-tls-route +spec: + hostnames: + - nginx-test.my-test.com + parentRefs: + - name: my-hotel-tls-passthrough + sectionName: tls + rules: + - backendRefs: + - name: nginx-tls + kind: Service + port: 443 \ No newline at end of file diff --git a/helm/crds/gateway.networking.k8s.io_tlsroutes.yaml b/helm/crds/gateway.networking.k8s.io_tlsroutes.yaml new file mode 100644 index 00000000..781db047 --- /dev/null +++ b/helm/crds/gateway.networking.k8s.io_tlsroutes.yaml @@ -0,0 +1,894 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/2997 + gateway.networking.k8s.io/bundle-version: v1.2.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. + + + 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: + + + 1. IPs are not allowed in SNI names per RFC 6066. + 2. A hostname may be prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + + + If a hostname is specified by both the Listener and TLSRoute, there + must be at least one intersecting hostname for the TLSRoute to be + attached to the Listener. For example: + + + * A Listener with `test.example.com` as the hostname matches TLSRoutes + that have either not specified any hostnames, or have specified at + least one of `test.example.com` or `*.example.com`. + * A Listener with `*.example.com` as the hostname matches TLSRoutes + that have either not specified any hostnames or have specified at least + one hostname that matches the Listener hostname. For example, + `test.example.com` and `*.example.com` would both match. On the other + hand, `example.com` and `test.example.net` would not match. + + + If both the Listener and TLSRoute have specified hostnames, any + TLSRoute hostnames that do not match the Listener hostname MUST be + ignored. For example, if a Listener specified `*.example.com`, and the + TLSRoute specified `test.example.com` and `test.example.net`, + `test.example.net` must not be considered for a match. + + + If both the Listener and TLSRoute have specified hostnames, and none + match with the criteria above, then the TLSRoute is not accepted. The + implementation must raise an 'Accepted' Condition with a status of + `False` in the corresponding RouteParentStatus. + + + Support: Core + items: + description: |- + Hostname is the fully qualified domain name of a network host. This matches + the RFC 1123 definition of a hostname with 2 notable exceptions: + + + 1. IPs are not allowed. + 2. A hostname may be prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + + + Hostname can be "precise" which is a domain name without the terminating + dot of a network host (e.g. "foo.example.com") or "wildcard", which is a + domain name prefixed with a single wildcard label (e.g. `*.example.com`). + + + Note that as per RFC1035 and RFC1123, a *label* must consist of lower case + alphanumeric characters or '-', and must start and end with an alphanumeric + character. No other punctuation is allowed. + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + maxItems: 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. For Services, that means the Service must either be in the same + namespace for a "producer" route, or the mesh implementation must support + and allow "consumer" routes for the referenced Service. ReferenceGrant is + not applicable for governing ParentRefs to Services - it is not possible to + create a "producer" route for a Service in a different namespace from the + Route. + + + There are two kinds of parent resources with "Core" support: + + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + + This API may be extended in the future to support additional kinds of parent + resources. + + + ParentRefs must be _distinct_. This means either that: + + + * They select different objects. If this is the case, then parentRef + entries are distinct. In terms of fields, this means that the + multi-part key defined by `group`, `kind`, `namespace`, and `name` must + be unique across all parentRef entries in the Route. + * They do not select different objects, but for each optional field used, + each ParentRef that selects the same object must set the same set of + optional fields to different values. If one ParentRef sets a + combination of optional fields, all must set the same combination. + + + Some examples: + + + * If one ParentRef sets `sectionName`, all ParentRefs referencing the + same object must also set `sectionName`. + * If one ParentRef sets `port`, all ParentRefs referencing the same + object must also set `port`. + * If one ParentRef sets `sectionName` and `port`, all ParentRefs + referencing the same object must also set `sectionName` and `port`. + + + It is possible to separately reference multiple distinct objects that may + be collapsed by an implementation. For example, some implementations may + choose to merge compatible Gateway Listeners together. If that is the + case, the list of routes attached to those resources should also be + merged. + + + Note that for ParentRefs that cross namespace boundaries, there are specific + rules. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example, + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable other kinds of cross-namespace reference. + + + + ParentRefs from a Route to a Service in the same namespace are "producer" + routes, which apply default routing rules to inbound connections from + any namespace to the Service. + + + ParentRefs from a Route to a Service in a different namespace are + "consumer" routes, and these routing rules are only applied to outbound + connections originating from the same namespace as the Route, for which + the intended destination of the connections are a Service targeted as a + ParentRef of the Route. + + + + + + + items: + description: |- + ParentReference identifies an API object (usually a Gateway) that can be considered + a parent of this resource (usually a route). There are two kinds of parent resources + with "Core" support: + + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + + This API may be extended in the future to support additional kinds of parent + resources. + + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + + There are two kinds of parent resources with "Core" support: + + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + + + ParentRefs from a Route to a Service in the same namespace are "producer" + routes, which apply default routing rules to inbound connections from + any namespace to the Service. + + + ParentRefs from a Route to a Service in a different namespace are + "consumer" routes, and these routing rules are only applied to outbound + connections originating from the same namespace as the Route, for which + the intended destination of the connections are a Service targeted as a + ParentRef of the Route. + + + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + + + When the parent resource is a Service, this targets a specific port in the + Service spec. When both Port (experimental) and SectionName are specified, + the name and port of the selected port must match both specified values. + + + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + maxItems: 32 + type: array + x-kubernetes-validations: + - message: sectionName or port must be specified when parentRefs includes + 2 or more references to the same parent + rule: 'self.all(p1, self.all(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '''') && (!has(p2.__namespace__) || p2.__namespace__ + == '''')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__)) ? ((!has(p1.sectionName) + || p1.sectionName == '''') == (!has(p2.sectionName) || p2.sectionName + == '''') && (!has(p1.port) || p1.port == 0) == (!has(p2.port) + || p2.port == 0)): true))' + - message: sectionName or port must be unique when parentRefs includes + 2 or more references to the same parent + rule: self.all(p1, self.exists_one(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '') && (!has(p2.__namespace__) || p2.__namespace__ + == '')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) && (((!has(p1.sectionName) + || p1.sectionName == '') && (!has(p2.sectionName) || p2.sectionName + == '')) || ( has(p1.sectionName) && has(p2.sectionName) && p1.sectionName + == p2.sectionName)) && (((!has(p1.port) || p1.port == 0) && (!has(p2.port) + || p2.port == 0)) || (has(p1.port) && has(p2.port) && p1.port + == p2.port)))) + 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. + + + Support: Core for Kubernetes Service + + + Support: Extended for Kubernetes ServiceImport + + + Support: Implementation-specific for any other resource + + + Support for weight: Extended + items: + description: |- + BackendRef defines how a Route should forward a request to a Kubernetes + resource. + + + Note that when a namespace different than the local namespace is specified, a + ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + + + + + When the BackendRef points to a Kubernetes Service, implementations SHOULD + honor the appProtocol field if it is set for the target Service Port. + + + Implementations supporting appProtocol SHOULD recognize the Kubernetes + Standard Application Protocols defined in KEP-3726. + + + If a Service appProtocol isn't specified, an implementation MAY infer the + backend protocol through its own means. Implementations MAY infer the + protocol from the Route type referring to the backend Service. + + + If a Route is not able to send traffic to the backend using the specified + protocol then the backend is considered invalid. Implementations MUST set the + "ResolvedRefs" condition to "False" with the "UnsupportedProtocol" reason. + + + + + + Note that when the BackendTLSPolicy object is enabled by the implementation, + there are some extra rules about validity to consider here. See the fields + where this struct is used for more information about the exact behavior. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + + Defaults to "Service" when not specified. + + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + + Support: Core (Services with a type other than ExternalName) + + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + weight: + default: 1 + description: |- + Weight specifies the proportion of requests forwarded to the referenced + backend. This is computed as weight/(sum of all weights in this + BackendRefs list). For non-zero values, there may be some epsilon from + the exact proportion defined here depending on the precision an + implementation supports. Weight is not a percentage and the sum of + weights does not need to equal 100. + + + If only one backend is specified and it has a weight greater than 0, 100% + of the traffic is forwarded to that backend. If weight is set to 0, no + traffic should be forwarded for this entry. If unspecified, weight + defaults to 1. + + + Support for this field varies based on the context where used. + format: int32 + maximum: 1000000 + minimum: 0 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind == ''Service'') + ? has(self.port) : true' + maxItems: 16 + minItems: 1 + type: array + 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. + + + Note that parent references that cannot be resolved by an implementation + of this API will not be added to this list. Implementations of this API + can only populate Route status for the Gateways/parent resources they are + responsible for. + + + A maximum of 32 Gateways will be represented in this list. An empty list + means the route has not been attached to any Gateway. + items: + description: |- + RouteParentStatus describes the status of a route with respect to an + associated Parent. + properties: + conditions: + description: |- + Conditions describes the status of the route with respect to the Gateway. + Note that the route's availability is also subject to the Gateway's own + status conditions and listener status. + + + If the Route's ParentRef specifies an existing Gateway that supports + Routes of this kind AND that Gateway's controller has sufficient access, + then that Gateway's controller MUST set the "Accepted" condition on the + Route, to indicate whether the route has been accepted or rejected by the + Gateway, and why. + + + A Route MUST be considered "Accepted" if at least one of the Route's + rules is implemented by the Gateway. + + + There are a number of cases where the "Accepted" condition may not be set + due to lack of controller visibility, that includes when: + + + * The Route refers to a 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.\n---\nThis struct + is intended for direct use as an array at the field path + .status.conditions. For example,\n\n\n\ttype FooStatus + struct{\n\t // Represents the observations of a foo's + current state.\n\t // Known .status.conditions.type are: + \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // + +listType=map\n\t // +listMapKey=type\n\t Conditions + []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" + patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + 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. + + + Example: "example.net/gateway-controller". + + + The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are + valid Kubernetes names + (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + + + Controllers MUST populate this field when writing status. Controllers should ensure that + entries to status populated with their ControllerName are cleaned up when they are no + longer necessary. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + parentRef: + description: |- + ParentRef corresponds with a ParentRef in the spec that this + RouteParentStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + + There are two kinds of parent resources with "Core" support: + + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + + + ParentRefs from a Route to a Service in the same namespace are "producer" + routes, which apply default routing rules to inbound connections from + any namespace to the Service. + + + ParentRefs from a Route to a Service in a different namespace are + "consumer" routes, and these routing rules are only applied to outbound + connections originating from the same namespace as the Route, for which + the intended destination of the connections are a Service targeted as a + ParentRef of the Route. + + + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + + + When the parent resource is a Service, this targets a specific port in the + Service spec. When both Port (experimental) and SectionName are specified, + the name and port of the selected port must match both specified values. + + + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + required: + - 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: null + storedVersions: null \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 59a9bed1..ae45612a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -16,9 +16,10 @@ nav: - Getting Started: guides/getstarted.md - Cross-Account Sharing: guides/ram-sharing.md - Advanced Configurations: guides/advanced-configurations.md - - TLS: guides/https.md + - HTTPS: guides/https.md - Custom Domain Name: guides/custom-domain-name.md - GRPC: guides/grpc.md + - TLS Passthrough: guides/tls-passthrough.md - Pod Readiness Gates: guides/pod-readiness-gates.md - Configuration: guides/environment.md - API Specification: api-reference.md diff --git a/mocks/controller-runtime/client/gomock_reflect_318529362/prog.go b/mocks/controller-runtime/client/gomock_reflect_318529362/prog.go deleted file mode 100644 index a780b9eb..00000000 --- a/mocks/controller-runtime/client/gomock_reflect_318529362/prog.go +++ /dev/null @@ -1,64 +0,0 @@ -package main - -import ( - "encoding/gob" - "flag" - "fmt" - "os" - "path" - "reflect" - - "github.com/golang/mock/mockgen/model" - - pkg_ "sigs.k8s.io/controller-runtime/pkg/client" -) - -var output = flag.String("output", "", "The output file name, or empty to use stdout.") - -func main() { - flag.Parse() - - its := []struct { - sym string - typ reflect.Type - }{ - - {"Client", reflect.TypeOf((*pkg_.Client)(nil)).Elem()}, - } - pkg := &model.Package{ - // NOTE: This behaves contrary to documented behaviour if the - // package name is not the final component of the import path. - // The reflect package doesn't expose the package name, though. - Name: path.Base("sigs.k8s.io/controller-runtime/pkg/client"), - } - - for _, it := range its { - intf, err := model.InterfaceFromInterfaceType(it.typ) - if err != nil { - fmt.Fprintf(os.Stderr, "Reflection: %v\n", err) - os.Exit(1) - } - intf.Name = it.sym - pkg.Interfaces = append(pkg.Interfaces, intf) - } - - outfile := os.Stdout - if len(*output) != 0 { - var err error - outfile, err = os.Create(*output) - if err != nil { - fmt.Fprintf(os.Stderr, "failed to open output file %q", *output) - } - defer func() { - if err := outfile.Close(); err != nil { - fmt.Fprintf(os.Stderr, "failed to close output file %q", *output) - os.Exit(1) - } - }() - } - - if err := gob.NewEncoder(outfile).Encode(pkg); err != nil { - fmt.Fprintf(os.Stderr, "gob encode: %v\n", err) - os.Exit(1) - } -} From b1441470b677a102f6a13965e30cab8da02452ea Mon Sep 17 00:00:00 2001 From: Zijun Wang Date: Tue, 11 Jun 2024 09:55:04 -0700 Subject: [PATCH 2/6] Minor Change --- .github/workflows/publish-doc.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/publish-doc.yaml b/.github/workflows/publish-doc.yaml index 4d32a4d6..0b49086f 100644 --- a/.github/workflows/publish-doc.yaml +++ b/.github/workflows/publish-doc.yaml @@ -30,6 +30,7 @@ jobs: if [[ ${{ github.ref }} == refs/heads/main ]]; then # Deploy to the mike doc version `dev` and update the `latest` alias for the main branch new git commits mike deploy dev latest --update-aliases --push + mike set-default latest elif [[ ${{ github.ref }} == refs/heads/release-v* ]]; then # Deploy to the mike doc version `vx.x.x` for the new git branches `release-vx.x.x` branch_name=${{ github.ref }} From 112b6b0cc3e79df166a410b8580786d65f2fb83c Mon Sep 17 00:00:00 2001 From: Zijun Wang Date: Tue, 11 Jun 2024 10:27:21 -0700 Subject: [PATCH 3/6] Minor Change --- docs/api-types/target-group-policy.md | 6 +++--- docs/api-types/tls-route.md | 26 +++++++++++--------------- docs/guides/tls-passthrough.md | 4 ++-- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/docs/api-types/target-group-policy.md b/docs/api-types/target-group-policy.md index 93acfc0c..9a7b976a 100644 --- a/docs/api-types/target-group-policy.md +++ b/docs/api-types/target-group-policy.md @@ -4,7 +4,7 @@ By default, AWS Gateway API Controller assumes plaintext HTTP/1 traffic for backend Kubernetes resources. TargetGroupPolicy is a CRD that can be attached to Service or ServiceExport, which allows the users to define protocol, protocol version and -health check configurations of those backend resources. +health check configurations of those backend resources. When attaching a policy to a resource, the following restrictions apply: @@ -28,9 +28,9 @@ However, the policy will not take effect unless the target is valid. ### Limitations and Considerations -- Attaching TargetGroupPolicy to a Service that is already referenced by a route will result in a replacement +- Attaching TargetGroupPolicy to an existing Service that is already referenced by a route will result in a replacement of VPC Lattice TargetGroup resource, except for health check updates. -- Attaching TargetGroupPolicy to a ServiceExport will result in a replacement of VPC Lattice TargetGroup resource, except for health check updates. +- Attaching TargetGroupPolicy to an existing ServiceExport will result in a replacement of VPC Lattice TargetGroup resource, except for health check updates. - Removing TargetGroupPolicy of a resource will roll back protocol configuration to default setting. (HTTP1/HTTP plaintext) ## Example Configuration diff --git a/docs/api-types/tls-route.md b/docs/api-types/tls-route.md index 0f0fd77f..35cf5c6c 100644 --- a/docs/api-types/tls-route.md +++ b/docs/api-types/tls-route.md @@ -5,20 +5,12 @@ With integration of the Gateway API, AWS Gateway API Controller supports `TLSRoute`. This allows you to define and manage end-to-end TLS encrypted traffic routing to your Kubernetes clusters. -### TLSRoute Key Features & Limitations - -**Features**: - -- **Routing Traffic**: Enables routing end-to-end TLS encrypted traffic from your client workload to server workload. - - -**Limitations**: - -- **Listener Protocol**: The `TLSRoute` sectionName must refer to an TLS protocol listener with mode: Passthrough in the parent `Gateway`. +### Considerations +- `TLSRoute` sectionName must refer to an `TLS` protocol listener with `mode: Passthrough` in the parentRefs `Gateway`. - `TLSRoute` only supports to have one rule. -- `TLSRoute` don't support `matches` field in the rule. -- The `hostnames` field with exactly one host name is required. This domain name is used as a vpc lattice's Service Name Indication (SNI) match. +- `TLSRoute` don't support any rule matching condition. +- The `hostnames` field with exactly one host name is required. This domain name is used as a vpc lattice's Service Name Indication (SNI) match to route the traffic to the correct backend service. ## Example Configuration @@ -57,6 +49,10 @@ In this example: - The `TLSRoute` is configured to route traffic to a k8s service named `nginx-tls` on port 443. - The `hostnames` field is set to `nginx-test.my-test.com`. The customer must use this domain name to send traffic to the nginx service. -This `TLSRoute` documentation provides a detailed introduction, feature set, and a basic example of how to configure -and use the resource within AWS Gateway API Controller project. For in-depth details and specifications, you can refer to the -official [Gateway API documentation](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.TLSRoute). \ No newline at end of file + +For the detailed tls passthrough traffic connectivity setup, please refer the user guide [here](../guides/tls-passthrough.md). + +For the detailed Gateway API `TLSRoute` resource specifications, you can refer to the +Kubernetes official [documentation](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1alpha2.TLSRoute). + +For the VPC Lattice tls passthrough Listener configuration details, you can refer to the VPC Lattice [documentation](https://docs.aws.amazon.com/vpc-lattice/latest/ug/tls-listeners.html). \ No newline at end of file diff --git a/docs/guides/tls-passthrough.md b/docs/guides/tls-passthrough.md index ef9abd3f..0a06bf16 100644 --- a/docs/guides/tls-passthrough.md +++ b/docs/guides/tls-passthrough.md @@ -4,8 +4,8 @@ ## Install Gateway API TLSRoute CRD -The TLSRoute CRD already included in the helm chart and deployment.yaml, If you are using these 2 methods to install the controller no extra steps are needed. -If you want to install the CRD manually by yourself: +The TLSRoute CRD already included in the helm chart and deployment.yaml, if you are using these 2 methods to install the controller no extra steps are needed. +If you want to install the TLSRoute CRD manually by yourself: ``` # Install CRD kubectl apply -f config/crds/bases/gateway.networking.k8s.io_tlsroutes.yaml From 642eed9c5ad75ba8d85b268ed18feae5eea875f5 Mon Sep 17 00:00:00 2001 From: Zijun Wang Date: Wed, 12 Jun 2024 11:40:01 -0700 Subject: [PATCH 4/6] Address PR comments, minor change --- docs/api-types/tls-route.md | 2 +- docs/guides/tls-passthrough.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-types/tls-route.md b/docs/api-types/tls-route.md index 35cf5c6c..a4cb2791 100644 --- a/docs/api-types/tls-route.md +++ b/docs/api-types/tls-route.md @@ -9,7 +9,7 @@ This allows you to define and manage end-to-end TLS encrypted traffic routing to - `TLSRoute` sectionName must refer to an `TLS` protocol listener with `mode: Passthrough` in the parentRefs `Gateway`. - `TLSRoute` only supports to have one rule. -- `TLSRoute` don't support any rule matching condition. +- `TLSRoute` doesn't support any rule matching condition. - The `hostnames` field with exactly one host name is required. This domain name is used as a vpc lattice's Service Name Indication (SNI) match to route the traffic to the correct backend service. diff --git a/docs/guides/tls-passthrough.md b/docs/guides/tls-passthrough.md index 0a06bf16..a9746abd 100644 --- a/docs/guides/tls-passthrough.md +++ b/docs/guides/tls-passthrough.md @@ -4,7 +4,7 @@ ## Install Gateway API TLSRoute CRD -The TLSRoute CRD already included in the helm chart and deployment.yaml, if you are using these 2 methods to install the controller no extra steps are needed. +The TLSRoute CRD is already included in the helm chart and deployment.yaml, if you are using these 2 methods to install the controller no extra steps needed. If you want to install the TLSRoute CRD manually by yourself: ``` # Install CRD From 15509b95618067034786d6f325460ee804d1495c Mon Sep 17 00:00:00 2001 From: Zijun Wang Date: Wed, 12 Jun 2024 12:44:29 -0700 Subject: [PATCH 5/6] Minor change --- docs/contributing/developer.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/contributing/developer.md b/docs/contributing/developer.md index a6d8e496..7dd3e20c 100644 --- a/docs/contributing/developer.md +++ b/docs/contributing/developer.md @@ -58,6 +58,7 @@ And install additional CRDs for the controller: ```bash kubectl apply -f config/crds/bases/externaldns.k8s.io_dnsendpoints.yaml +kubectl apply -f config/crds/bases/gateway.networking.k8s.io_tlsroutes.yaml kubectl apply -f config/crds/bases/application-networking.k8s.aws_serviceexports.yaml kubectl apply -f config/crds/bases/application-networking.k8s.aws_serviceimports.yaml kubectl apply -f config/crds/bases/application-networking.k8s.aws_targetgrouppolicies.yaml From 1dbd98cdb78c1c023686b0deeb8dc7950e1c4f93 Mon Sep 17 00:00:00 2001 From: Zijun Wang Date: Fri, 14 Jun 2024 11:14:20 -0700 Subject: [PATCH 6/6] Address PR comments --- docs/api-types/tls-route.md | 8 +++--- docs/guides/tls-passthrough.md | 49 +++++----------------------------- 2 files changed, 11 insertions(+), 46 deletions(-) diff --git a/docs/api-types/tls-route.md b/docs/api-types/tls-route.md index a4cb2791..50569b15 100644 --- a/docs/api-types/tls-route.md +++ b/docs/api-types/tls-route.md @@ -7,10 +7,10 @@ This allows you to define and manage end-to-end TLS encrypted traffic routing to ### Considerations -- `TLSRoute` sectionName must refer to an `TLS` protocol listener with `mode: Passthrough` in the parentRefs `Gateway`. +- `TLSRoute` sectionName must refer to a `TLS` protocol listener with `mode: Passthrough` in the parentRefs `Gateway`. - `TLSRoute` only supports to have one rule. -- `TLSRoute` doesn't support any rule matching condition. -- The `hostnames` field with exactly one host name is required. This domain name is used as a vpc lattice's Service Name Indication (SNI) match to route the traffic to the correct backend service. +- `TLSRoute` does not support any rule matching condition. +- The `hostnames` field with exactly one host name is required. ## Example Configuration @@ -47,7 +47,7 @@ In this example: mode: Passthrough ``` - The `TLSRoute` is configured to route traffic to a k8s service named `nginx-tls` on port 443. -- The `hostnames` field is set to `nginx-test.my-test.com`. The customer must use this domain name to send traffic to the nginx service. +- The `hostnames` field is set to `nginx-test.my-test.com`. The customer must use this hostname to send traffic to the nginx service. For the detailed tls passthrough traffic connectivity setup, please refer the user guide [here](../guides/tls-passthrough.md). diff --git a/docs/guides/tls-passthrough.md b/docs/guides/tls-passthrough.md index a9746abd..b771d476 100644 --- a/docs/guides/tls-passthrough.md +++ b/docs/guides/tls-passthrough.md @@ -20,7 +20,7 @@ tlsroutes.gateway.networking.k8s.io 2024-03-07T23:16:22Z ### 1. Configure TLS Passthrough Listener on Gateway ``` -kubectl apply -f files/examples/gateway-tls-passthrough.yaml +kubectl apply -f files/examples/my-gateway-tls-passthrough.yaml ``` ``` @@ -96,44 +96,12 @@ kubectl get deployment nginx-tls NAME READY UP-TO-DATE AVAILABLE AGE nginx-tls 2/2 2 2 1d +# Use the specified TLSRoute hostname to send traffic to the beackend nginx service kubectl exec deployments/parking -- curl -kv https://nginx-test.my-test.com --resolve nginx-test.my-test.com:443:169.254.171.0 * Trying 169.254.171.0:443... * Connected to nginx-test.my-test.com (169.254.171.0) port 443 (#0) -* ALPN, offering h2 -* ALPN, offering http/1.1 -* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH -* successfully set certificate verify locations: -* CAfile: /etc/pki/tls/certs/ca-bundle.crt -* CApath: none -* TLSv1.2 (OUT), TLS header, Certificate Status (22): -* TLSv1.2 (OUT), TLS handshake, Client hello (1): -* TLSv1.2 (IN), TLS handshake, Server hello (2): -* TLSv1.2 (IN), TLS handshake, Certificate (11): -* TLSv1.2 (IN), TLS handshake, Server key exchange (12): -* TLSv1.2 (IN), TLS handshake, Server finished (14): -* TLSv1.2 (OUT), TLS handshake, Client key exchange (16): -* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1): -* TLSv1.2 (OUT), TLS handshake, Finished (20): -* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1): -* TLSv1.2 (IN), TLS handshake, Finished (20): -* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384 -* ALPN, server accepted to use h2 -* Server certificate: -* subject: C=US; ST=wa; L=seattle; O=aws; OU=lattice; CN=liwen.ssl-test.com; emailAddress=liwenwu@amazon.com -* start date: Mar 5 21:26:24 2024 GMT -# use customer defined name -curl -k -v https://nginx-test.my-test.com --resolve nginx-test.my-test.com:443:169.254.171.32 -* Added nginx-test.my-test.com:443:169.254.171.32 to DNS cache -* Hostname nginx-test.my-test.com was found in DNS cache -* Trying 169.254.171.0:443... -* Connected to nginx-test.my-test.com (169.254.171.0) port 443 (#0) -* ALPN, offering h2 -* ALPN, offering http/1.1 -* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH -* successfully set certificate verify locations: -* CAfile: /etc/pki/tls/certs/ca-bundle.crt -* CApath: none +.... * TLSv1.2 (OUT), TLS header, Certificate Status (22): * TLSv1.2 (OUT), TLS handshake, Client hello (1): * TLSv1.2 (IN), TLS handshake, Server hello (2): @@ -142,14 +110,11 @@ curl -k -v https://nginx-test.my-test.com --resolve nginx-test.my-test.com:443:1 * TLSv1.2 (IN), TLS handshake, Server finished (14): * TLSv1.2 (OUT), TLS handshake, Client key exchange (16): * TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1): -* TLSv1.2 (OUT), TLS handshake, Finished (20): +* TLSv1.2 (OUT), TLS handshake, Finished (20): * TLSv1.2 (IN), TLS change cipher, Change cipher spec (1): -* TLSv1.2 (IN), TLS handshake, Finished (20): +* TLSv1.2 (IN), TLS handshake, Finished (20): <---------- TLS Handshake from client pod to the backend `nginx-tls` pod successfully, no tls termination in the middle * SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384 * ALPN, server accepted to use h2 -* Server certificate: -* subject: C=US; ST=wa; L=seattle; O=aws; OU=lattice; CN=liwen.ssl-test.com; emailAddress=liwenwu@amazon.com - ....

Welcome to nginx!

@@ -174,7 +139,7 @@ kubectl apply -f files/examples/parking.yaml kubectl apply -f files/examples/tls-rate1.yaml ``` -### 3. Configure ServieExport with TargetGroupPolicy `protocol:TCP` in cluster-2 +### 3. Configure ServiceExport with TargetGroupPolicy `protocol:TCP` in cluster-2 ``` # Create tls-rate2 Kubernetes Service in cluster-2 @@ -212,7 +177,7 @@ spec: kubectl apply -f files/examples/tls-rate2-import.yaml ``` -### 5. Configure TLSRoute for bluegreen deployment +### 5. Configure TLSRoute for blue/green deployment ``` kubectl apply -f files/examples/rate-tlsroute-bluegreen.yaml