diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0f4771a3f5e5..5fceea67877e 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -36,14 +36,14 @@ jobs: - uses: ./tools/github-actions/setup-deps - name: Initialize CodeQL - uses: github/codeql-action/init@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12 + uses: github/codeql-action/init@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13 with: languages: ${{ matrix.language }} - name: Autobuild - uses: github/codeql-action/autobuild@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12 + uses: github/codeql-action/autobuild@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12 + uses: github/codeql-action/analyze@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 987f7c2b62df..018bb5c0dd74 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -40,6 +40,6 @@ jobs: retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12 + uses: github/codeql-action/upload-sarif@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13 with: sarif_file: results.sarif diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index 9e4a1460f8e7..f34bd237a88f 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -25,7 +25,7 @@ jobs: IMAGE=envoy-proxy/gateway-dev TAG=${{ github.sha }} make image - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@5681af892cd0f4997658e2bacc62bd0a894cf564 # v0.27.0 + uses: aquasecurity/trivy-action@915b19bbe73b92a6cf82a1bc12b087c9a19a5fe2 # v0.28.0 with: image-ref: envoy-proxy/gateway-dev:${{ github.sha }} exit-code: '1' diff --git a/api/v1alpha1/backendtrafficpolicy_types.go b/api/v1alpha1/backendtrafficpolicy_types.go index 4e6118e70353..4183c12830f9 100644 --- a/api/v1alpha1/backendtrafficpolicy_types.go +++ b/api/v1alpha1/backendtrafficpolicy_types.go @@ -74,7 +74,6 @@ type BackendTrafficPolicySpec struct { // If multiple configurations are specified, the first one to match wins. // // +optional - // +notImplementedHide ResponseOverride []*ResponseOverride `json:"responseOverride,omitempty"` } diff --git a/api/v1alpha1/envoyproxy_types.go b/api/v1alpha1/envoyproxy_types.go index d7a2a73abe81..4bf7920f6241 100644 --- a/api/v1alpha1/envoyproxy_types.go +++ b/api/v1alpha1/envoyproxy_types.go @@ -124,6 +124,8 @@ type EnvoyProxySpec struct { // // - envoy.filters.http.ratelimit // + // - envoy.filters.http.custom_response + // // - envoy.filters.http.router // // Note: "envoy.filters.http.router" cannot be reordered, it's always the last filter in the chain. @@ -174,7 +176,7 @@ type FilterPosition struct { } // EnvoyFilter defines the type of Envoy HTTP filter. -// +kubebuilder:validation:Enum=envoy.filters.http.health_check;envoy.filters.http.fault;envoy.filters.http.cors;envoy.filters.http.ext_authz;envoy.filters.http.basic_auth;envoy.filters.http.oauth2;envoy.filters.http.jwt_authn;envoy.filters.http.stateful_session;envoy.filters.http.ext_proc;envoy.filters.http.wasm;envoy.filters.http.rbac;envoy.filters.http.local_ratelimit;envoy.filters.http.ratelimit +// +kubebuilder:validation:Enum=envoy.filters.http.health_check;envoy.filters.http.fault;envoy.filters.http.cors;envoy.filters.http.ext_authz;envoy.filters.http.basic_auth;envoy.filters.http.oauth2;envoy.filters.http.jwt_authn;envoy.filters.http.stateful_session;envoy.filters.http.ext_proc;envoy.filters.http.wasm;envoy.filters.http.rbac;envoy.filters.http.local_ratelimit;envoy.filters.http.ratelimit;envoy.filters.http.custom_response type EnvoyFilter string const ( @@ -217,6 +219,9 @@ const ( // EnvoyFilterRateLimit defines the Envoy HTTP rate limit filter. EnvoyFilterRateLimit EnvoyFilter = "envoy.filters.http.ratelimit" + // EnvoyFilterCustomResponse defines the Envoy HTTP custom response filter. + EnvoyFilterCustomResponse EnvoyFilter = "envoy.filters.http.custom_response" + // EnvoyFilterRouter defines the Envoy HTTP router filter. EnvoyFilterRouter EnvoyFilter = "envoy.filters.http.router" ) diff --git a/api/v1alpha1/oidc_types.go b/api/v1alpha1/oidc_types.go index dcc036157720..dfe7a4604f4f 100644 --- a/api/v1alpha1/oidc_types.go +++ b/api/v1alpha1/oidc_types.go @@ -119,7 +119,6 @@ type OIDCProvider struct { // Other settings for the connection to the OIDC Provider can be specified in the BackendSettings resource. // // +optional - // +notImplementedHide BackendCluster `json:",inline"` // The OIDC Provider's [issuer identifier](https://openid.net/specs/openid-connect-discovery-1_0.html#IssuerDiscovery). diff --git a/api/v1alpha1/shared_types.go b/api/v1alpha1/shared_types.go index fe795c833db0..617151e296a6 100644 --- a/api/v1alpha1/shared_types.go +++ b/api/v1alpha1/shared_types.go @@ -627,33 +627,48 @@ type ResponseOverride struct { // CustomResponseMatch defines the configuration for matching a user response to return a custom one. type CustomResponseMatch struct { // Status code to match on. The match evaluates to true if any of the matches are successful. - StatusCode []StatusCodeMatch `json:"statusCode"` + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=50 + StatusCodes []StatusCodeMatch `json:"statusCodes"` } // StatusCodeValueType defines the types of values for the status code match supported by Envoy Gateway. // +kubebuilder:validation:Enum=Value;Range type StatusCodeValueType string +const ( + // StatusCodeValueTypeValue defines the "Value" status code match type. + StatusCodeValueTypeValue StatusCodeValueType = "Value" + + // StatusCodeValueTypeRange defines the "Range" status code match type. + StatusCodeValueTypeRange StatusCodeValueType = "Range" +) + +// StatusCodeMatch defines the configuration for matching a status code. +// +kubebuilder:validation:XValidation:message="value must be set for type Value",rule="(!has(self.type) || self.type == 'Value')? has(self.value) : true" +// +kubebuilder:validation:XValidation:message="range must be set for type Range",rule="(has(self.type) && self.type == 'Range')? has(self.range) : true" type StatusCodeMatch struct { // Type is the type of value. + // Valid values are Value and Range, default is Value. // // +kubebuilder:default=Value + // +kubebuilder:validation:Enum=Value;Range // +unionDiscriminator Type *StatusCodeValueType `json:"type"` // Value contains the value of the status code. // // +optional - Value *string `json:"value,omitempty"` - // ValueRef contains the contents of the body - // specified as a local object reference. - // Only a reference to ConfigMap is supported. + Value *int `json:"value,omitempty"` + + // Range contains the range of status codes. // // +optional Range *StatusCodeRange `json:"range,omitempty"` } // StatusCodeRange defines the configuration for define a range of status codes. +// +kubebuilder:validation:XValidation: message="end must be greater than start",rule="self.end > self.start" type StatusCodeRange struct { // Start of the range, including the start value. Start int `json:"start"` @@ -669,19 +684,31 @@ type CustomResponse struct { ContentType *string `json:"contentType,omitempty"` // Body of the Custom Response - // - // +optional - Body *CustomResponseBody `json:"body,omitempty"` + Body CustomResponseBody `json:"body"` } // ResponseValueType defines the types of values for the response body supported by Envoy Gateway. // +kubebuilder:validation:Enum=Inline;ValueRef type ResponseValueType string +const ( + // ResponseValueTypeInline defines the "Inline" response body type. + ResponseValueTypeInline ResponseValueType = "Inline" + + // ResponseValueTypeValueRef defines the "ValueRef" response body type. + ResponseValueTypeValueRef ResponseValueType = "ValueRef" +) + // CustomResponseBody +// +kubebuilder:validation:XValidation:message="inline must be set for type Inline",rule="(!has(self.type) || self.type == 'Inline')? has(self.inline) : true" +// +kubebuilder:validation:XValidation:message="valueRef must be set for type ValueRef",rule="(has(self.type) && self.type == 'ValueRef')? has(self.valueRef) : true" +// +kubebuilder:validation:XValidation:message="only ConfigMap is supported for ValueRef",rule="has(self.valueRef) ? self.valueRef.kind == 'ConfigMap' : true" type CustomResponseBody struct { // Type is the type of method to use to read the body value. + // Valid values are Inline and ValueRef, default is Inline. // + // +kubebuilder:default=Inline + // +kubebuilder:validation:Enum=Inline;ValueRef // +unionDiscriminator Type *ResponseValueType `json:"type"` @@ -689,10 +716,14 @@ type CustomResponseBody struct { // // +optional Inline *string `json:"inline,omitempty"` + // ValueRef contains the contents of the body // specified as a local object reference. // Only a reference to ConfigMap is supported. // + // The value of key `response.body` in the ConfigMap will be used as the response body. + // If the key is not found, the first value in the ConfigMap will be used. + // // +optional ValueRef *gwapiv1.LocalObjectReference `json:"valueRef,omitempty"` } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 696c99259fb2..c225d65d39e6 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -1148,11 +1148,7 @@ func (in *CustomResponse) DeepCopyInto(out *CustomResponse) { *out = new(string) **out = **in } - if in.Body != nil { - in, out := &in.Body, &out.Body - *out = new(CustomResponseBody) - (*in).DeepCopyInto(*out) - } + in.Body.DeepCopyInto(&out.Body) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomResponse. @@ -1198,8 +1194,8 @@ func (in *CustomResponseBody) DeepCopy() *CustomResponseBody { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CustomResponseMatch) DeepCopyInto(out *CustomResponseMatch) { *out = *in - if in.StatusCode != nil { - in, out := &in.StatusCode, &out.StatusCode + if in.StatusCodes != nil { + in, out := &in.StatusCodes, &out.StatusCodes *out = make([]StatusCodeMatch, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) @@ -5166,7 +5162,7 @@ func (in *StatusCodeMatch) DeepCopyInto(out *StatusCodeMatch) { } if in.Value != nil { in, out := &in.Value, &out.Value - *out = new(string) + *out = new(int) **out = **in } if in.Range != nil { diff --git a/charts/gateway-addons-helm/Chart.lock b/charts/gateway-addons-helm/Chart.lock index 4b6f92ac77cc..228a952fdc1d 100644 --- a/charts/gateway-addons-helm/Chart.lock +++ b/charts/gateway-addons-helm/Chart.lock @@ -16,6 +16,6 @@ dependencies: version: 1.3.1 - name: opentelemetry-collector repository: https://open-telemetry.github.io/opentelemetry-helm-charts - version: 0.73.1 -digest: sha256:4c16df8d7efc27aff566fa5dfd2eba6527adbf3fc8e94e7e3ccfc0cee7836f1c -generated: "2024-06-20T11:46:59.148579+08:00" + version: 0.108.0 +digest: sha256:ea6663bb1358123b96b69d2c5b0b8c20650a43dc39b24c482f0560201fd2cc3a +generated: "2024-10-19T12:59:47.251089661+02:00" diff --git a/charts/gateway-addons-helm/Chart.yaml b/charts/gateway-addons-helm/Chart.yaml index 84ac6228f624..2571ccec51e2 100644 --- a/charts/gateway-addons-helm/Chart.yaml +++ b/charts/gateway-addons-helm/Chart.yaml @@ -47,5 +47,5 @@ dependencies: condition: tempo.enabled - name: opentelemetry-collector repository: https://open-telemetry.github.io/opentelemetry-helm-charts - version: 0.73.1 + version: 0.108.0 condition: opentelemetry-collector.enabled diff --git a/charts/gateway-addons-helm/README.md b/charts/gateway-addons-helm/README.md index ccbd26b983d9..a52af3e2d14b 100644 --- a/charts/gateway-addons-helm/README.md +++ b/charts/gateway-addons-helm/README.md @@ -25,7 +25,7 @@ An Add-ons Helm chart for Envoy Gateway | https://grafana.github.io/helm-charts | grafana | 8.0.0 | | https://grafana.github.io/helm-charts | loki | 4.8.0 | | https://grafana.github.io/helm-charts | tempo | 1.3.1 | -| https://open-telemetry.github.io/opentelemetry-helm-charts | opentelemetry-collector | 0.73.1 | +| https://open-telemetry.github.io/opentelemetry-helm-charts | opentelemetry-collector | 0.108.0 | | https://prometheus-community.github.io/helm-charts | prometheus | 25.21.0 | ## Usage @@ -103,7 +103,7 @@ To uninstall the chart: | loki.singleBinary.replicas | int | `1` | | | loki.test.enabled | bool | `false` | | | loki.write.replicas | int | `0` | | -| opentelemetry-collector.config.exporters.logging.verbosity | string | `"detailed"` | | +| opentelemetry-collector.config.exporters.debug.verbosity | string | `"detailed"` | | | opentelemetry-collector.config.exporters.loki.endpoint | string | `"http://loki.monitoring.svc:3100/loki/api/v1/push"` | | | opentelemetry-collector.config.exporters.otlp.endpoint | string | `"tempo.monitoring.svc:4317"` | | | opentelemetry-collector.config.exporters.otlp.tls.insecure | bool | `true` | | @@ -112,6 +112,7 @@ To uninstall the chart: | opentelemetry-collector.config.processors.attributes.actions[0].action | string | `"insert"` | | | opentelemetry-collector.config.processors.attributes.actions[0].key | string | `"loki.attribute.labels"` | | | opentelemetry-collector.config.processors.attributes.actions[0].value | string | `"k8s.pod.name, k8s.namespace.name"` | | +| opentelemetry-collector.config.receivers.datadog.endpoint | string | `"${env:MY_POD_IP}:8126"` | | | opentelemetry-collector.config.receivers.otlp.protocols.grpc.endpoint | string | `"${env:MY_POD_IP}:4317"` | | | opentelemetry-collector.config.receivers.otlp.protocols.http.endpoint | string | `"${env:MY_POD_IP}:4318"` | | | opentelemetry-collector.config.receivers.zipkin.endpoint | string | `"${env:MY_POD_IP}:9411"` | | @@ -120,12 +121,15 @@ To uninstall the chart: | opentelemetry-collector.config.service.pipelines.logs.processors[0] | string | `"attributes"` | | | opentelemetry-collector.config.service.pipelines.logs.receivers[0] | string | `"otlp"` | | | opentelemetry-collector.config.service.pipelines.metrics.exporters[0] | string | `"prometheus"` | | -| opentelemetry-collector.config.service.pipelines.metrics.receivers[0] | string | `"otlp"` | | +| opentelemetry-collector.config.service.pipelines.metrics.receivers[0] | string | `"datadog"` | | +| opentelemetry-collector.config.service.pipelines.metrics.receivers[1] | string | `"otlp"` | | | opentelemetry-collector.config.service.pipelines.traces.exporters[0] | string | `"otlp"` | | -| opentelemetry-collector.config.service.pipelines.traces.receivers[0] | string | `"otlp"` | | -| opentelemetry-collector.config.service.pipelines.traces.receivers[1] | string | `"zipkin"` | | +| opentelemetry-collector.config.service.pipelines.traces.receivers[0] | string | `"datadog"` | | +| opentelemetry-collector.config.service.pipelines.traces.receivers[1] | string | `"otlp"` | | +| opentelemetry-collector.config.service.pipelines.traces.receivers[2] | string | `"zipkin"` | | | opentelemetry-collector.enabled | bool | `false` | | | opentelemetry-collector.fullnameOverride | string | `"otel-collector"` | | +| opentelemetry-collector.image.repository | string | `"otel/opentelemetry-collector-contrib"` | | | opentelemetry-collector.mode | string | `"deployment"` | | | prometheus.alertmanager.enabled | bool | `false` | | | prometheus.enabled | bool | `true` | | diff --git a/charts/gateway-addons-helm/values.yaml b/charts/gateway-addons-helm/values.yaml index 55a02b682558..d3fb043ddd43 100644 --- a/charts/gateway-addons-helm/values.yaml +++ b/charts/gateway-addons-helm/values.yaml @@ -181,11 +181,13 @@ opentelemetry-collector: enabled: false fullnameOverride: otel-collector mode: deployment + image: + repository: "otel/opentelemetry-collector-contrib" config: exporters: prometheus: endpoint: 0.0.0.0:19001 - logging: + debug: verbosity: detailed loki: endpoint: "http://loki.monitoring.svc:3100/loki/api/v1/push" @@ -207,6 +209,8 @@ opentelemetry-collector: # Loki will convert this to k8s_pod_name label. value: k8s.pod.name, k8s.namespace.name receivers: + datadog: + endpoint: ${env:MY_POD_IP}:8126 zipkin: endpoint: ${env:MY_POD_IP}:9411 otlp: @@ -223,6 +227,7 @@ opentelemetry-collector: exporters: - prometheus receivers: + - datadog - otlp logs: exporters: @@ -235,5 +240,6 @@ opentelemetry-collector: exporters: - otlp receivers: + - datadog - otlp - zipkin diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml index 7b2e937312db..f9fb0f329dd6 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml @@ -940,16 +940,15 @@ spec: match: description: Match configuration. properties: - statusCode: + statusCodes: description: Status code to match on. The match evaluates to true if any of the matches are successful. items: + description: StatusCodeMatch defines the configuration + for matching a status code. properties: range: - description: |- - ValueRef contains the contents of the body - specified as a local object reference. - Only a reference to ConfigMap is supported. + description: Range contains the range of status codes. properties: end: description: End of the range, including the end @@ -963,23 +962,41 @@ spec: - end - start type: object + x-kubernetes-validations: + - message: end must be greater than start + rule: self.end > self.start type: + allOf: + - enum: + - Value + - Range + - enum: + - Value + - Range default: Value - description: Type is the type of value. - enum: - - Value - - Range + description: |- + Type is the type of value. + Valid values are Value and Range, default is Value. type: string value: description: Value contains the value of the status code. - type: string + type: integer required: - type type: object + x-kubernetes-validations: + - message: value must be set for type Value + rule: '(!has(self.type) || self.type == ''Value'')? + has(self.value) : true' + - message: range must be set for type Range + rule: '(has(self.type) && self.type == ''Range'')? has(self.range) + : true' + maxItems: 50 + minItems: 1 type: array required: - - statusCode + - statusCodes type: object response: description: Response configuration. @@ -992,17 +1009,26 @@ spec: string. type: string type: - description: Type is the type of method to use to read - the body value. - enum: - - Inline - - ValueRef + allOf: + - enum: + - Inline + - ValueRef + - enum: + - Inline + - ValueRef + default: Inline + description: |- + Type is the type of method to use to read the body value. + Valid values are Inline and ValueRef, default is Inline. type: string valueRef: description: |- ValueRef contains the contents of the body specified as a local object reference. Only a reference to ConfigMap is supported. + + The value of key `response.body` in the ConfigMap will be used as the response body. + If the key is not found, the first value in the ConfigMap will be used. properties: group: description: |- @@ -1031,10 +1057,22 @@ spec: required: - type type: object + x-kubernetes-validations: + - message: inline must be set for type Inline + rule: '(!has(self.type) || self.type == ''Inline'')? has(self.inline) + : true' + - message: valueRef must be set for type ValueRef + rule: '(has(self.type) && self.type == ''ValueRef'')? + has(self.valueRef) : true' + - message: only ConfigMap is supported for ValueRef + rule: 'has(self.valueRef) ? self.valueRef.kind == ''ConfigMap'' + : true' contentType: description: Content Type of the response. This will be set in the Content-Type header. type: string + required: + - body type: object required: - match diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml index 5b0130f2736c..0733ed112b6a 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml @@ -305,6 +305,8 @@ spec: - envoy.filters.http.ratelimit + - envoy.filters.http.custom_response + - envoy.filters.http.router Note: "envoy.filters.http.router" cannot be reordered, it's always the last filter in the chain. @@ -330,6 +332,7 @@ spec: - envoy.filters.http.rbac - envoy.filters.http.local_ratelimit - envoy.filters.http.ratelimit + - envoy.filters.http.custom_response type: string before: description: |- @@ -349,6 +352,7 @@ spec: - envoy.filters.http.rbac - envoy.filters.http.local_ratelimit - envoy.filters.http.ratelimit + - envoy.filters.http.custom_response type: string name: description: Name of the filter. @@ -366,6 +370,7 @@ spec: - envoy.filters.http.rbac - envoy.filters.http.local_ratelimit - envoy.filters.http.ratelimit + - envoy.filters.http.custom_response type: string required: - name diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_httproutefilters.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_httproutefilters.yaml index 8a75fec42116..672cfb59df8c 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_httproutefilters.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_httproutefilters.yaml @@ -60,17 +60,26 @@ spec: description: Inline contains the value as an inline string. type: string type: - description: Type is the type of method to use to read the - body value. - enum: - - Inline - - ValueRef + allOf: + - enum: + - Inline + - ValueRef + - enum: + - Inline + - ValueRef + default: Inline + description: |- + Type is the type of method to use to read the body value. + Valid values are Inline and ValueRef, default is Inline. type: string valueRef: description: |- ValueRef contains the contents of the body specified as a local object reference. Only a reference to ConfigMap is supported. + + The value of key `response.body` in the ConfigMap will be used as the response body. + If the key is not found, the first value in the ConfigMap will be used. properties: group: description: |- @@ -99,6 +108,16 @@ spec: required: - type type: object + x-kubernetes-validations: + - message: inline must be set for type Inline + rule: '(!has(self.type) || self.type == ''Inline'')? has(self.inline) + : true' + - message: valueRef must be set for type ValueRef + rule: '(has(self.type) && self.type == ''ValueRef'')? has(self.valueRef) + : true' + - message: only ConfigMap is supported for ValueRef + rule: 'has(self.valueRef) ? self.valueRef.kind == ''ConfigMap'' + : true' contentType: description: Content Type of the response. This will be set in the Content-Type header. diff --git a/examples/extension-server/go.mod b/examples/extension-server/go.mod index 92af0438105b..25eb15516ef7 100644 --- a/examples/extension-server/go.mod +++ b/examples/extension-server/go.mod @@ -5,7 +5,7 @@ go 1.23.1 require ( github.com/envoyproxy/gateway v1.0.2 github.com/envoyproxy/go-control-plane v0.13.1 - github.com/urfave/cli/v2 v2.27.4 + github.com/urfave/cli/v2 v2.27.5 google.golang.org/grpc v1.67.1 google.golang.org/protobuf v1.35.1 k8s.io/apimachinery v0.31.1 @@ -17,7 +17,7 @@ require ( cel.dev/expr v0.16.0 // indirect github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-logr/logr v1.4.2 // indirect diff --git a/examples/extension-server/go.sum b/examples/extension-server/go.sum index 1df719e00af0..29bfba9e9f4f 100644 --- a/examples/extension-server/go.sum +++ b/examples/extension-server/go.sum @@ -4,8 +4,8 @@ github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMr github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20 h1:N+3sFI5GUjRKBi+i0TxYVST9h4Ie192jJWpHvthBBgg= github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= -github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -64,8 +64,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8= -github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= +github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= diff --git a/go.mod b/go.mod index ce9b809b7d81..e8b99417e7a9 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/miekg/dns v1.1.62 github.com/ohler55/ojg v1.24.1 - github.com/prometheus/client_golang v1.20.4 + github.com/prometheus/client_golang v1.20.5 github.com/prometheus/common v0.60.0 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 @@ -285,7 +285,7 @@ require ( golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.24.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 7f3f258926c3..bd764134785d 100644 --- a/go.sum +++ b/go.sum @@ -689,8 +689,8 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= -github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= -github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= diff --git a/internal/gatewayapi/backendtrafficpolicy.go b/internal/gatewayapi/backendtrafficpolicy.go index b8f289a9df0e..89b6804a2ba2 100644 --- a/internal/gatewayapi/backendtrafficpolicy.go +++ b/internal/gatewayapi/backendtrafficpolicy.go @@ -10,9 +10,11 @@ import ( "fmt" "math" "sort" + "strconv" "strings" perr "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" @@ -34,6 +36,7 @@ func (t *Translator) ProcessBackendTrafficPolicies(backendTrafficPolicies []*egv gateways []*GatewayContext, routes []RouteContext, xdsIR resource.XdsIRMap, + configMaps []*corev1.ConfigMap, ) []*egv1a1.BackendTrafficPolicy { res := []*egv1a1.BackendTrafficPolicy{} @@ -127,7 +130,7 @@ func (t *Translator) ProcessBackendTrafficPolicies(backendTrafficPolicies []*egv } // Set conditions for translation error if it got any - if err := t.translateBackendTrafficPolicyForRoute(policy, route, xdsIR); err != nil { + if err := t.translateBackendTrafficPolicyForRoute(policy, route, xdsIR, configMaps); err != nil { status.SetTranslationErrorForPolicyAncestors(&policy.Status, ancestorRefs, t.GatewayControllerName, @@ -181,7 +184,7 @@ func (t *Translator) ProcessBackendTrafficPolicies(backendTrafficPolicies []*egv } // Set conditions for translation error if it got any - if err := t.translateBackendTrafficPolicyForGateway(policy, currTarget, gateway, xdsIR); err != nil { + if err := t.translateBackendTrafficPolicyForGateway(policy, currTarget, gateway, xdsIR, configMaps); err != nil { status.SetTranslationErrorForPolicyAncestors(&policy.Status, ancestorRefs, t.GatewayControllerName, @@ -281,7 +284,12 @@ func resolveBTPolicyRouteTargetRef(policy *egv1a1.BackendTrafficPolicy, target g return route.RouteContext, nil } -func (t *Translator) translateBackendTrafficPolicyForRoute(policy *egv1a1.BackendTrafficPolicy, route RouteContext, xdsIR resource.XdsIRMap) error { +func (t *Translator) translateBackendTrafficPolicyForRoute( + policy *egv1a1.BackendTrafficPolicy, + route RouteContext, + xdsIR resource.XdsIRMap, + configMaps []*corev1.ConfigMap, +) error { var ( rl *ir.RateLimit lb *ir.LoadBalancer @@ -295,6 +303,7 @@ func (t *Translator) translateBackendTrafficPolicyForRoute(policy *egv1a1.Backen bc *ir.BackendConnection ds *ir.DNS h2 *ir.HTTP2Settings + ro *ir.ResponseOverride err, errs error ) @@ -340,6 +349,11 @@ func (t *Translator) translateBackendTrafficPolicyForRoute(policy *egv1a1.Backen errs = errors.Join(errs, err) } + if ro, err = buildResponseOverride(policy, configMaps); err != nil { + err = perr.WithMessage(err, "ResponseOverride") + errs = errors.Join(errs, err) + } + ds = translateDNS(policy.Spec.ClusterSettings) // Apply IR to all relevant routes @@ -402,6 +416,7 @@ func (t *Translator) translateBackendTrafficPolicyForRoute(policy *egv1a1.Backen HTTP2: h2, DNS: ds, Timeout: to, + ResponseOverride: ro, } // Update the Host field in HealthCheck, now that we have access to the Route Hostname. @@ -418,7 +433,13 @@ func (t *Translator) translateBackendTrafficPolicyForRoute(policy *egv1a1.Backen return errs } -func (t *Translator) translateBackendTrafficPolicyForGateway(policy *egv1a1.BackendTrafficPolicy, target gwapiv1a2.LocalPolicyTargetReferenceWithSectionName, gateway *GatewayContext, xdsIR resource.XdsIRMap) error { +func (t *Translator) translateBackendTrafficPolicyForGateway( + policy *egv1a1.BackendTrafficPolicy, + target gwapiv1a2.LocalPolicyTargetReferenceWithSectionName, + gateway *GatewayContext, + xdsIR resource.XdsIRMap, + configMaps []*corev1.ConfigMap, +) error { var ( rl *ir.RateLimit lb *ir.LoadBalancer @@ -431,6 +452,7 @@ func (t *Translator) translateBackendTrafficPolicyForGateway(policy *egv1a1.Back rt *ir.Retry ds *ir.DNS h2 *ir.HTTP2Settings + ro *ir.ResponseOverride err, errs error ) @@ -469,6 +491,10 @@ func (t *Translator) translateBackendTrafficPolicyForGateway(policy *egv1a1.Back err = perr.WithMessage(err, "HTTP2") errs = errors.Join(errs, err) } + if ro, err = buildResponseOverride(policy, configMaps); err != nil { + err = perr.WithMessage(err, "ResponseOverride") + errs = errors.Join(errs, err) + } ds = translateDNS(policy.Spec.ClusterSettings) @@ -542,16 +568,17 @@ func (t *Translator) translateBackendTrafficPolicyForGateway(policy *egv1a1.Back } r.Traffic = &ir.TrafficFeatures{ - RateLimit: rl, - LoadBalancer: lb, - ProxyProtocol: pp, - HealthCheck: hc, - CircuitBreaker: cb, - FaultInjection: fi, - TCPKeepalive: ka, - Retry: rt, - HTTP2: h2, - DNS: ds, + RateLimit: rl, + LoadBalancer: lb, + ProxyProtocol: pp, + HealthCheck: hc, + CircuitBreaker: cb, + FaultInjection: fi, + TCPKeepalive: ka, + Retry: rt, + HTTP2: h2, + DNS: ds, + ResponseOverride: ro, } // Update the Host field in HealthCheck, now that we have access to the Route Hostname. @@ -836,3 +863,81 @@ func makeIrTriggerSet(in []egv1a1.TriggerEnum) []ir.TriggerEnum { } return irTriggers } + +func buildResponseOverride(policy *egv1a1.BackendTrafficPolicy, configMaps []*corev1.ConfigMap) (*ir.ResponseOverride, error) { + if len(policy.Spec.ResponseOverride) == 0 { + return nil, nil + } + + rules := make([]ir.ResponseOverrideRule, 0, len(policy.Spec.ResponseOverride)) + for index, ro := range policy.Spec.ResponseOverride { + match := ir.CustomResponseMatch{ + StatusCodes: make([]ir.StatusCodeMatch, 0, len(ro.Match.StatusCodes)), + } + + for _, code := range ro.Match.StatusCodes { + if code.Type != nil && *code.Type == egv1a1.StatusCodeValueTypeRange { + match.StatusCodes = append(match.StatusCodes, ir.StatusCodeMatch{ + Range: &ir.StatusCodeRange{ + Start: code.Range.Start, + End: code.Range.End, + }, + }) + } else { + match.StatusCodes = append(match.StatusCodes, ir.StatusCodeMatch{ + Value: code.Value, + }) + } + } + + response := ir.CustomResponse{ + ContentType: ro.Response.ContentType, + } + + if ro.Response.Body.Type != nil && *ro.Response.Body.Type == egv1a1.ResponseValueTypeValueRef { + foundCM := false + for _, cm := range configMaps { + if cm.Namespace == policy.Namespace && cm.Name == string(ro.Response.Body.ValueRef.Name) { + body, dataOk := cm.Data["response.body"] + switch { + case dataOk: + response.Body = body + case len(cm.Data) > 0: // Fallback to the first key if response.body is not found + for _, value := range cm.Data { + body = value + break + } + response.Body = body + default: + return nil, fmt.Errorf("can't find the key response.body in the referenced configmap %s", ro.Response.Body.ValueRef.Name) + } + + foundCM = true + break + } + } + if !foundCM { + return nil, fmt.Errorf("can't find the referenced configmap %s", ro.Response.Body.ValueRef.Name) + } + } else { + response.Body = *ro.Response.Body.Inline + } + + rules = append(rules, ir.ResponseOverrideRule{ + Name: defaultResponseOverrideRuleName(policy, index), + Match: match, + Response: response, + }) + } + return &ir.ResponseOverride{ + Name: irConfigName(policy), + Rules: rules, + }, nil +} + +func defaultResponseOverrideRuleName(policy *egv1a1.BackendTrafficPolicy, index int) string { + return fmt.Sprintf( + "%s/responseoverride/rule/%s", + irConfigName(policy), + strconv.Itoa(index)) +} diff --git a/internal/gatewayapi/helpers_v1alpha2.go b/internal/gatewayapi/helpers_v1alpha2.go deleted file mode 100644 index 3b1dffde66f1..000000000000 --- a/internal/gatewayapi/helpers_v1alpha2.go +++ /dev/null @@ -1,45 +0,0 @@ -// 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 ( - gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" - gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" -) - -// TODO: [gwapiv1a2-gwapiv1] -// This file can be removed once all routes graduates to gwapiv1. - -// UpgradeBackendRef converts gwapiv1a2.BackendRef to gwapiv1.BackendRef -func UpgradeBackendRef(old gwapiv1a2.BackendRef) gwapiv1.BackendRef { - upgraded := gwapiv1.BackendRef{} - - 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 = old.Name - - if old.Port != nil { - upgraded.Port = PortNumPtr(int32(*old.Port)) - } - - return upgraded -} diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override-invalid-valueref.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override-invalid-valueref.in.yaml new file mode 100644 index 000000000000..e44a8473d5c9 --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override-invalid-valueref.in.yaml @@ -0,0 +1,141 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: default + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All + - apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: default + name: gateway-2 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All +grpcRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: GRPCRoute + metadata: + namespace: default + name: grpcroute-1 + spec: + parentRefs: + - namespace: default + name: gateway-1 + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 +httpRoutes: + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: default + name: gateway-2 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 +configMaps: + - apiVersion: v1 + kind: ConfigMap + metadata: + name: response-override-config + namespace: default + data: {} +backendTrafficPolicies: + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + namespace: default + name: policy-for-gateway + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + responseOverride: + - match: + statusCodes: + - type: Value + value: 404 + response: + contentType: text/plain + body: + type: Inline + inline: "gateway-1 Not Found" + - match: + statusCodes: + - type: Value + value: 500 + - type: Range + range: + start: 501 + end: 511 + response: + contentType: application/json + body: + type: ValueRef + valueRef: + group: "" + kind: ConfigMap + name: response-override-config + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + namespace: default + name: policy-for-route + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + responseOverride: + - match: + statusCodes: + - value: 404 + response: + contentType: text/plain + body: + inline: "httproute-1 Not Found" + - match: + statusCodes: + - value: 500 + - type: Range + range: + start: 501 + end: 511 + response: + contentType: application/json + body: + type: ValueRef + valueRef: + group: "" + kind: ConfigMap + name: response-override-config-1 diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override-invalid-valueref.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override-invalid-valueref.out.yaml new file mode 100644 index 000000000000..c1542d9caec2 --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override-invalid-valueref.out.yaml @@ -0,0 +1,371 @@ +backendTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + creationTimestamp: null + name: policy-for-route + namespace: default + spec: + responseOverride: + - match: + statusCodes: + - type: null + value: 404 + response: + body: + inline: httproute-1 Not Found + type: null + contentType: text/plain + - match: + statusCodes: + - type: null + value: 500 + - range: + end: 511 + start: 501 + type: Range + response: + body: + type: ValueRef + valueRef: + group: "" + kind: ConfigMap + name: response-override-config-1 + contentType: application/json + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-2 + namespace: default + sectionName: http + conditions: + - lastTransitionTime: null + message: 'ResponseOverride: can''t find the referenced configmap response-override-config-1.' + reason: Invalid + status: "False" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + creationTimestamp: null + name: policy-for-gateway + namespace: default + spec: + responseOverride: + - match: + statusCodes: + - type: Value + value: 404 + response: + body: + inline: gateway-1 Not Found + type: Inline + contentType: text/plain + - match: + statusCodes: + - type: Value + value: 500 + - range: + end: 511 + start: 501 + type: Range + response: + body: + type: ValueRef + valueRef: + group: "" + kind: ConfigMap + name: response-override-config + contentType: application/json + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: default + conditions: + - lastTransitionTime: null + message: 'ResponseOverride: can''t find the key response.body in the referenced + configmap response-override-config.' + reason: Invalid + status: "False" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-1 + namespace: default + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-2 + namespace: default + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +grpcRoutes: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: GRPCRoute + metadata: + creationTimestamp: null + name: grpcroute-1 + namespace: default + spec: + parentRefs: + - name: gateway-1 + namespace: default + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: default + sectionName: http +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-1 + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-2 + namespace: default + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: / + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-2 + namespace: default + sectionName: http +infraIR: + default/gateway-1: + proxy: + listeners: + - address: null + name: default/gateway-1/http + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: default + name: default/gateway-1 + default/gateway-2: + proxy: + listeners: + - address: null + name: default/gateway-2/http + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-2 + gateway.envoyproxy.io/owning-gateway-namespace: default + name: default/gateway-2 +xdsIR: + default/gateway-1: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: true + metadata: + kind: Gateway + name: gateway-1 + namespace: default + sectionName: http + name: default/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + name: grpcroute/default/grpcroute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: GRPC + weight: 1 + directResponse: + statusCode: 500 + hostname: '*' + isHTTP2: true + metadata: + kind: GRPCRoute + name: grpcroute-1 + namespace: default + name: grpcroute/default/grpcroute-1/rule/0/match/-1/* + default/gateway-2: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + metadata: + kind: Gateway + name: gateway-2 + namespace: default + sectionName: http + name: default/gateway-2/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + directResponse: + statusCode: 500 + hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: / diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override.in.yaml new file mode 100644 index 000000000000..51dd9fd71142 --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override.in.yaml @@ -0,0 +1,145 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: default + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All + - apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: default + name: gateway-2 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All +grpcRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: GRPCRoute + metadata: + namespace: default + name: grpcroute-1 + spec: + parentRefs: + - namespace: default + name: gateway-1 + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 +httpRoutes: + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: default + name: gateway-2 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 +configMaps: + - apiVersion: v1 + kind: ConfigMap + metadata: + name: response-override-config + namespace: default + data: + response.body: | + { + "error": "Internal Server Error" + } +backendTrafficPolicies: + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + namespace: default + name: policy-for-gateway + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + responseOverride: + - match: + statusCodes: + - type: Value + value: 404 + response: + contentType: text/plain + body: + type: Inline + inline: "gateway-1 Not Found" + - match: + statusCodes: + - type: Value + value: 500 + - type: Range + range: + start: 501 + end: 511 + response: + contentType: application/json + body: + type: ValueRef + valueRef: + group: "" + kind: ConfigMap + name: response-override-config + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + namespace: default + name: policy-for-route + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + responseOverride: + - match: + statusCodes: + - value: 404 + response: + contentType: text/plain + body: + inline: "httproute-1 Not Found" + - match: + statusCodes: + - value: 500 + - type: Range + range: + start: 501 + end: 511 + response: + contentType: application/json + body: + type: ValueRef + valueRef: + group: "" + kind: ConfigMap + name: response-override-config diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override.out.yaml new file mode 100644 index 000000000000..568a57af4849 --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override.out.yaml @@ -0,0 +1,414 @@ +backendTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + creationTimestamp: null + name: policy-for-route + namespace: default + spec: + responseOverride: + - match: + statusCodes: + - type: null + value: 404 + response: + body: + inline: httproute-1 Not Found + type: null + contentType: text/plain + - match: + statusCodes: + - type: null + value: 500 + - range: + end: 511 + start: 501 + type: Range + response: + body: + type: ValueRef + valueRef: + group: "" + kind: ConfigMap + name: response-override-config + contentType: application/json + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-2 + namespace: default + sectionName: http + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + creationTimestamp: null + name: policy-for-gateway + namespace: default + spec: + responseOverride: + - match: + statusCodes: + - type: Value + value: 404 + response: + body: + inline: gateway-1 Not Found + type: Inline + contentType: text/plain + - match: + statusCodes: + - type: Value + value: 500 + - range: + end: 511 + start: 501 + type: Range + response: + body: + type: ValueRef + valueRef: + group: "" + kind: ConfigMap + name: response-override-config + contentType: application/json + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: default + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-1 + namespace: default + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-2 + namespace: default + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +grpcRoutes: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: GRPCRoute + metadata: + creationTimestamp: null + name: grpcroute-1 + namespace: default + spec: + parentRefs: + - name: gateway-1 + namespace: default + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: default + sectionName: http +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-1 + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-2 + namespace: default + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: / + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-2 + namespace: default + sectionName: http +infraIR: + default/gateway-1: + proxy: + listeners: + - address: null + name: default/gateway-1/http + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: default + name: default/gateway-1 + default/gateway-2: + proxy: + listeners: + - address: null + name: default/gateway-2/http + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-2 + gateway.envoyproxy.io/owning-gateway-namespace: default + name: default/gateway-2 +xdsIR: + default/gateway-1: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: true + metadata: + kind: Gateway + name: gateway-1 + namespace: default + sectionName: http + name: default/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + name: grpcroute/default/grpcroute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: GRPC + weight: 1 + hostname: '*' + isHTTP2: true + metadata: + kind: GRPCRoute + name: grpcroute-1 + namespace: default + name: grpcroute/default/grpcroute-1/rule/0/match/-1/* + traffic: + responseOverride: + name: backendtrafficpolicy/default/policy-for-gateway + rules: + - match: + statusCodes: + - value: 404 + name: backendtrafficpolicy/default/policy-for-gateway/responseoverride/rule/0 + response: + body: gateway-1 Not Found + contentType: text/plain + - match: + statusCodes: + - value: 500 + - range: + end: 511 + start: 501 + name: backendtrafficpolicy/default/policy-for-gateway/responseoverride/rule/1 + response: + body: | + { + "error": "Internal Server Error" + } + contentType: application/json + default/gateway-2: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + metadata: + kind: Gateway + name: gateway-2 + namespace: default + sectionName: http + name: default/gateway-2/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: / + traffic: + responseOverride: + name: backendtrafficpolicy/default/policy-for-route + rules: + - match: + statusCodes: + - value: 404 + name: backendtrafficpolicy/default/policy-for-route/responseoverride/rule/0 + response: + body: httproute-1 Not Found + contentType: text/plain + - match: + statusCodes: + - value: 500 + - range: + end: 511 + start: 501 + name: backendtrafficpolicy/default/policy-for-route/responseoverride/rule/1 + response: + body: | + { + "error": "Internal Server Error" + } + contentType: application/json diff --git a/internal/gatewayapi/translator.go b/internal/gatewayapi/translator.go index 0e6d683d855c..0f518b71033b 100644 --- a/internal/gatewayapi/translator.go +++ b/internal/gatewayapi/translator.go @@ -211,7 +211,7 @@ func (t *Translator) Translate(resources *resource.Resources) (*TranslateResult, // Process BackendTrafficPolicies backendTrafficPolicies := t.ProcessBackendTrafficPolicies( - resources.BackendTrafficPolicies, gateways, routes, xdsIR) + resources.BackendTrafficPolicies, gateways, routes, xdsIR, resources.ConfigMaps) // Process SecurityPolicies securityPolicies := t.ProcessSecurityPolicies( diff --git a/internal/ir/xds.go b/internal/ir/xds.go index fdcace324f5f..cb5021f4c9fe 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -477,6 +477,63 @@ type HTTP2Settings struct { ResetStreamOnError *bool `json:"resetStreamOnError,omitempty" yaml:"resetStreamOnError,omitempty"` } +// ResponseOverride defines the configuration to override specific responses with a custom one. +// +k8s:deepcopy-gen=true +type ResponseOverride struct { + // Name is a unique name for a ResponseOverride configuration. + // The xds translator only generates one CustomResponse filter for each unique name. + Name string `json:"name" yaml:"name"` + + // Rules contains the list of rules to override responses. + Rules []ResponseOverrideRule `json:"rules,omitempty"` +} + +// ResponseOverrideRule defines the configuration for overriding a response. +// +k8s:deepcopy-gen=true +type ResponseOverrideRule struct { + // Name is a generated name for the rule. + Name string `json:"name"` + // Match configuration. + Match CustomResponseMatch `json:"match"` + // Response configuration. + Response CustomResponse `json:"response"` +} + +// CustomResponseMatch defines the configuration for matching a user response to return a custom one. +// +k8s:deepcopy-gen=true +type CustomResponseMatch struct { + // Status code to match on. The match evaluates to true if any of the matches are successful. + StatusCodes []StatusCodeMatch `json:"statusCodes"` +} + +// StatusCodeMatch defines the configuration for matching a status code. +// +k8s:deepcopy-gen=true +type StatusCodeMatch struct { + // Value contains the value of the status code. + Value *int `json:"value,omitempty"` + + // Range contains a range of status codes. + Range *StatusCodeRange `json:"range,omitempty"` +} + +// StatusCodeRange defines the configuration for define a range of status codes. +type StatusCodeRange struct { + // Start of the range, including the start value. + Start int `json:"start"` + // End of the range, including the end value. + End int `json:"end"` +} + +// CustomResponse defines the configuration for returning a custom response. +// +k8s:deepcopy-gen=true +type CustomResponse struct { + // Content Type of the response. This will be set in the Content-Type header. + ContentType *string `json:"contentType,omitempty"` + + // Body of the Custom Response + Body string `json:"body"` +} + // HealthCheckSettings provides HealthCheck configuration on the HTTP/HTTPS listener. // +k8s:deepcopy-gen=true type HealthCheckSettings egv1a1.HealthCheckSettings @@ -657,6 +714,8 @@ type TrafficFeatures struct { HTTP2 *HTTP2Settings `json:"http2,omitempty" yaml:"http2,omitempty"` // DNS is used to configure how DNS resolution is handled by the Envoy Proxy cluster DNS *DNS `json:"dns,omitempty" yaml:"dns,omitempty"` + // ResponseOverride defines the schema for overriding the response. + ResponseOverride *ResponseOverride `json:"responseOverride,omitempty" yaml:"responseOverride,omitempty"` } func (b *TrafficFeatures) Validate() error { diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index 3c0c1135f44b..791b6d5dd68a 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -602,6 +602,48 @@ func (in *CoreListenerDetails) DeepCopy() *CoreListenerDetails { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CustomResponse) DeepCopyInto(out *CustomResponse) { + *out = *in + if in.ContentType != nil { + in, out := &in.ContentType, &out.ContentType + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomResponse. +func (in *CustomResponse) DeepCopy() *CustomResponse { + if in == nil { + return nil + } + out := new(CustomResponse) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CustomResponseMatch) DeepCopyInto(out *CustomResponseMatch) { + *out = *in + if in.StatusCodes != nil { + in, out := &in.StatusCodes, &out.StatusCodes + *out = make([]StatusCodeMatch, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomResponseMatch. +func (in *CustomResponseMatch) DeepCopy() *CustomResponseMatch { + if in == nil { + return nil + } + out := new(CustomResponseMatch) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNS) DeepCopyInto(out *DNS) { *out = *in @@ -2399,6 +2441,45 @@ func (in *ResourceMetadata) DeepCopy() *ResourceMetadata { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResponseOverride) DeepCopyInto(out *ResponseOverride) { + *out = *in + if in.Rules != nil { + in, out := &in.Rules, &out.Rules + *out = make([]ResponseOverrideRule, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResponseOverride. +func (in *ResponseOverride) DeepCopy() *ResponseOverride { + if in == nil { + return nil + } + out := new(ResponseOverride) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResponseOverrideRule) DeepCopyInto(out *ResponseOverrideRule) { + *out = *in + in.Match.DeepCopyInto(&out.Match) + in.Response.DeepCopyInto(&out.Response) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResponseOverrideRule. +func (in *ResponseOverrideRule) DeepCopy() *ResponseOverrideRule { + if in == nil { + return nil + } + out := new(ResponseOverrideRule) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Retry) DeepCopyInto(out *Retry) { *out = *in @@ -2590,6 +2671,31 @@ func (in *SlowStart) DeepCopy() *SlowStart { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StatusCodeMatch) DeepCopyInto(out *StatusCodeMatch) { + *out = *in + if in.Value != nil { + in, out := &in.Value, &out.Value + *out = new(int) + **out = **in + } + if in.Range != nil { + in, out := &in.Range, &out.Range + *out = new(StatusCodeRange) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StatusCodeMatch. +func (in *StatusCodeMatch) DeepCopy() *StatusCodeMatch { + if in == nil { + return nil + } + out := new(StatusCodeMatch) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *StringMatch) DeepCopyInto(out *StringMatch) { *out = *in @@ -3159,6 +3265,11 @@ func (in *TrafficFeatures) DeepCopyInto(out *TrafficFeatures) { *out = new(DNS) (*in).DeepCopyInto(*out) } + if in.ResponseOverride != nil { + in, out := &in.ResponseOverride, &out.ResponseOverride + *out = new(ResponseOverride) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TrafficFeatures. diff --git a/internal/provider/kubernetes/controller.go b/internal/provider/kubernetes/controller.go index 7fe3c3d32ffe..de020dfcee8a 100644 --- a/internal/provider/kubernetes/controller.go +++ b/internal/provider/kubernetes/controller.go @@ -239,7 +239,7 @@ func (r *gatewayAPIReconciler) Reconcile(ctx context.Context, _ reconcile.Reques } // Add all BackendTrafficPolicies to the resourceTree - if err = r.processBackendTrafficPolicies(ctx, gwcResource); err != nil { + if err = r.processBackendTrafficPolicies(ctx, gwcResource, resourceMappings); err != nil { return reconcile.Result{}, err } @@ -748,6 +748,39 @@ func (r *gatewayAPIReconciler) processConfigMapRef( return nil } +// processBtpConfigMapRefs adds the referenced ConfigMaps in BackendTrafficPolicies +// to the resourceTree +func (r *gatewayAPIReconciler) processBtpConfigMapRefs( + ctx context.Context, resourceTree *resource.Resources, resourceMap *resourceMappings, +) { + for _, policy := range resourceTree.BackendTrafficPolicies { + for _, ro := range policy.Spec.ResponseOverride { + if ro.Response.Body.ValueRef != nil && string(ro.Response.Body.ValueRef.Kind) == resource.KindConfigMap { + configMap := new(corev1.ConfigMap) + err := r.client.Get(ctx, + types.NamespacedName{Namespace: policy.Namespace, Name: string(ro.Response.Body.ValueRef.Name)}, + configMap, + ) + // we don't return an error here, because we want to continue + // reconciling the rest of the BackendTrafficPolicies despite that this + // reference is invalid. + // This BackendTrafficPolicies will be marked as invalid in its status + // when translating to IR because the referenced configmap can't be + // found. + if err != nil { + r.log.Error(err, + "failed to process ResponseOverride ValueRef for BackendTrafficPolicy", + "policy", policy, "ValueRef", ro.Response.Body.ValueRef.Name) + } + + resourceMap.allAssociatedNamespaces.Insert(policy.Namespace) + resourceTree.ConfigMaps = append(resourceTree.ConfigMaps, configMap) + r.log.Info("processing ConfigMap", "namespace", policy.Namespace, "name", string(ro.Response.Body.ValueRef.Name)) + } + } + } +} + func (r *gatewayAPIReconciler) getNamespace(ctx context.Context, name string) (*corev1.Namespace, error) { nsKey := types.NamespacedName{Name: name} ns := new(corev1.Namespace) @@ -942,7 +975,8 @@ func (r *gatewayAPIReconciler) processClientTrafficPolicies( } // processBackendTrafficPolicies adds BackendTrafficPolicies to the resourceTree -func (r *gatewayAPIReconciler) processBackendTrafficPolicies(ctx context.Context, resourceTree *resource.Resources) error { +func (r *gatewayAPIReconciler) processBackendTrafficPolicies(ctx context.Context, resourceTree *resource.Resources, resourceMap *resourceMappings, +) error { backendTrafficPolicies := egv1a1.BackendTrafficPolicyList{} if err := r.client.List(ctx, &backendTrafficPolicies); err != nil { return fmt.Errorf("error listing BackendTrafficPolicies: %w", err) @@ -955,6 +989,7 @@ func (r *gatewayAPIReconciler) processBackendTrafficPolicies(ctx context.Context policy.Status = gwapiv1a2.PolicyStatus{} resourceTree.BackendTrafficPolicies = append(resourceTree.BackendTrafficPolicies, &policy) } + r.processBtpConfigMapRefs(ctx, resourceTree, resourceMap) return nil } @@ -1348,7 +1383,7 @@ func (r *gatewayAPIReconciler) watchResources(ctx context.Context, mgr manager.M return err } - // Watch ConfigMap CRUDs and process affected ClienTraffiPolicies and BackendTLSPolicies. + // Watch ConfigMap CRUDs and process affected EG Resources. configMapPredicates := []predicate.TypedPredicate[*corev1.ConfigMap]{ predicate.NewTypedPredicateFuncs[*corev1.ConfigMap](func(cm *corev1.ConfigMap) bool { return r.validateConfigMapForReconcile(cm) @@ -1492,6 +1527,10 @@ func (r *gatewayAPIReconciler) watchResources(ctx context.Context, mgr manager.M return err } + if err := addBtpIndexers(ctx, mgr); err != nil { + return err + } + // Watch SecurityPolicy spPredicates := []predicate.TypedPredicate[*egv1a1.SecurityPolicy]{ predicate.TypedGenerationChangedPredicate[*egv1a1.SecurityPolicy]{}, diff --git a/internal/provider/kubernetes/indexers.go b/internal/provider/kubernetes/indexers.go index 462a70542f3b..2ad12069f988 100644 --- a/internal/provider/kubernetes/indexers.go +++ b/internal/provider/kubernetes/indexers.go @@ -46,6 +46,7 @@ const ( secretEnvoyProxyIndex = "secretEnvoyProxyIndex" secretEnvoyExtensionPolicyIndex = "secretEnvoyExtensionPolicyIndex" httpRouteFilterHTTPRouteIndex = "httpRouteFilterHTTPRouteIndex" + configMapBtpIndex = "configMapBtpIndex" ) func addReferenceGrantIndexers(ctx context.Context, mgr manager.Manager) error { @@ -641,6 +642,36 @@ func secretCtpIndexFunc(rawObj client.Object) []string { return secretReferences } +// addBtpIndexers adds indexing on BackendTrafficPolicy, for ConfigMap objects that are +// referenced in BackendTrafficPolicy objects. This helps in querying for BackendTrafficPolies that are +// affected by a particular ConfigMap CRUD. +func addBtpIndexers(ctx context.Context, mgr manager.Manager) error { + if err := mgr.GetFieldIndexer().IndexField(ctx, &egv1a1.BackendTrafficPolicy{}, configMapBtpIndex, configMapBtpIndexFunc); err != nil { + return err + } + + return nil +} + +func configMapBtpIndexFunc(rawObj client.Object) []string { + btp := rawObj.(*egv1a1.BackendTrafficPolicy) + var configMapReferences []string + + for _, ro := range btp.Spec.ResponseOverride { + if ro.Response.Body.ValueRef != nil { + if string(ro.Response.Body.ValueRef.Kind) == resource.KindConfigMap { + configMapReferences = append(configMapReferences, + types.NamespacedName{ + Namespace: btp.Namespace, + Name: string(ro.Response.Body.ValueRef.Name), + }.String(), + ) + } + } + } + return configMapReferences +} + // addBtlsIndexers adds indexing on BackendTLSPolicy, for ConfigMap objects that are // referenced in BackendTLSPolicy objects. This helps in querying for BackendTLSPolicies that are // affected by a particular ConfigMap CRUD. diff --git a/internal/provider/kubernetes/predicates.go b/internal/provider/kubernetes/predicates.go index 9c4d582b58b3..a885d58ca62d 100644 --- a/internal/provider/kubernetes/predicates.go +++ b/internal/provider/kubernetes/predicates.go @@ -588,7 +588,7 @@ func (r *gatewayAPIReconciler) handleNode(obj client.Object) bool { return true } -// validateConfigMapForReconcile checks whether the ConfigMap belongs to a valid ClientTrafficPolicy. +// validateConfigMapForReconcile checks whether the ConfigMap belongs to a valid EG resource. func (r *gatewayAPIReconciler) validateConfigMapForReconcile(obj client.Object) bool { configMap, ok := obj.(*corev1.ConfigMap) if !ok { @@ -604,8 +604,8 @@ func (r *gatewayAPIReconciler) validateConfigMapForReconcile(obj client.Object) return false } - if len(ctpList.Items) == 0 { - return false + if len(ctpList.Items) > 0 { + return true } btlsList := &gwapiv1a3.BackendTLSPolicyList{} @@ -616,11 +616,23 @@ func (r *gatewayAPIReconciler) validateConfigMapForReconcile(obj client.Object) return false } - if len(btlsList.Items) == 0 { + if len(btlsList.Items) > 0 { + return true + } + + btpList := &egv1a1.BackendTrafficPolicyList{} + if err := r.client.List(context.Background(), btpList, &client.ListOptions{ + FieldSelector: fields.OneTermEqualSelector(configMapBtpIndex, utils.NamespacedName(configMap).String()), + }); err != nil { + r.log.Error(err, "unable to find associated BackendTrafficPolicy") return false } - return true + if len(btpList.Items) > 0 { + return true + } + + return false } func (r *gatewayAPIReconciler) isEnvoyExtensionPolicyReferencingBackend(nsName *types.NamespacedName) bool { diff --git a/internal/provider/kubernetes/routes.go b/internal/provider/kubernetes/routes.go index 3a0a9f8131e6..956a2b59b3fd 100644 --- a/internal/provider/kubernetes/routes.go +++ b/internal/provider/kubernetes/routes.go @@ -56,8 +56,7 @@ func (r *gatewayAPIReconciler) processTLSRoutes(ctx context.Context, gatewayName for _, rule := range tlsRoute.Spec.Rules { for _, backendRef := range rule.BackendRefs { - ref := gatewayapi.UpgradeBackendRef(backendRef) - if err := validateBackendRef(&ref); err != nil { + if err := validateBackendRef(&backendRef); err != nil { r.log.Error(err, "invalid backendRef") continue } @@ -467,8 +466,7 @@ func (r *gatewayAPIReconciler) processTCPRoutes(ctx context.Context, gatewayName for _, rule := range tcpRoute.Spec.Rules { for _, backendRef := range rule.BackendRefs { - ref := gatewayapi.UpgradeBackendRef(backendRef) - if err := validateBackendRef(&ref); err != nil { + if err := validateBackendRef(&backendRef); err != nil { r.log.Error(err, "invalid backendRef") continue } @@ -545,8 +543,7 @@ func (r *gatewayAPIReconciler) processUDPRoutes(ctx context.Context, gatewayName for _, rule := range udpRoute.Spec.Rules { for _, backendRef := range rule.BackendRefs { - ref := gatewayapi.UpgradeBackendRef(backendRef) - if err := validateBackendRef(&ref); err != nil { + if err := validateBackendRef(&backendRef); err != nil { r.log.Error(err, "invalid backendRef") continue } diff --git a/internal/xds/translator/custom_response.go b/internal/xds/translator/custom_response.go new file mode 100644 index 000000000000..1d1bf3a5d2c5 --- /dev/null +++ b/internal/xds/translator/custom_response.go @@ -0,0 +1,450 @@ +// 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" + "strconv" + + cncfv3 "github.com/cncf/xds/go/xds/core/v3" + matcherv3 "github.com/cncf/xds/go/xds/type/matcher/v3" + typev3 "github.com/cncf/xds/go/xds/type/v3" + corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + respv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/custom_response/v3" + hcmv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" + policyv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/http/custom_response/local_response_policy/v3" + envoymatcherv3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" + expr "google.golang.org/genproto/googleapis/api/expr/v1alpha1" + "google.golang.org/protobuf/types/known/anypb" + + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" + "github.com/envoyproxy/gateway/internal/ir" + "github.com/envoyproxy/gateway/internal/xds/types" +) + +func init() { + registerHTTPFilter(&customResponse{}) +} + +type customResponse struct{} + +var _ httpFilter = &customResponse{} + +// patchHCM builds and appends the customResponse Filters to the HTTP Connection Manager +// if applicable, and it does not already exist. +// Note: this method creates an customResponse filter for each route that contains an ResponseOverride config. +// the filter is disabled by default. It is enabled on the route level. +func (c *customResponse) patchHCM(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTTPListener) error { + var errs error + + if mgr == nil { + return errors.New("hcm is nil") + } + + if irListener == nil { + return errors.New("ir listener is nil") + } + + for _, route := range irListener.Routes { + if !c.routeContainsResponseOverride(route) { + continue + } + + // Only generates one CustomResponse Envoy filter for each unique name. + // For example, if there are two routes under the same gateway with the + // same CustomResponse config, only one CustomResponse filter will be generated. + if hcmContainsFilter(mgr, c.customResponseFilterName(route.Traffic.ResponseOverride)) { + continue + } + + filter, err := c.buildHCMCustomResponseFilter(route.Traffic.ResponseOverride) + if err != nil { + errs = errors.Join(errs, err) + continue + } + + mgr.HttpFilters = append(mgr.HttpFilters, filter) + } + + return errs +} + +// buildHCMCustomResponseFilter returns an OAuth2 HTTP filter from the provided IR HTTPRoute. +func (c *customResponse) buildHCMCustomResponseFilter(ro *ir.ResponseOverride) (*hcmv3.HttpFilter, error) { + proto, err := c.customResponseConfig(ro) + if err != nil { + return nil, err + } + + if err := proto.ValidateAll(); err != nil { + return nil, err + } + + any, err := anypb.New(proto) + if err != nil { + return nil, err + } + + return &hcmv3.HttpFilter{ + Name: c.customResponseFilterName(ro), + Disabled: true, + ConfigType: &hcmv3.HttpFilter_TypedConfig{ + TypedConfig: any, + }, + }, nil +} + +func (c *customResponse) customResponseFilterName(ro *ir.ResponseOverride) string { + return perRouteFilterName(egv1a1.EnvoyFilterCustomResponse, ro.Name) +} + +func (c *customResponse) customResponseConfig(ro *ir.ResponseOverride) (*respv3.CustomResponse, error) { + var matchers []*matcherv3.Matcher_MatcherList_FieldMatcher + + for _, r := range ro.Rules { + var ( + action *matcherv3.Matcher_OnMatch_Action + predicate *matcherv3.Matcher_MatcherList_Predicate + err error + ) + + if action, err = c.buildAction(r); err != nil { + return nil, err + } + + switch { + case len(r.Match.StatusCodes) == 0: + // This is just a sanity check, as the CRD validation should have caught this. + return nil, fmt.Errorf("missing status code in response override rule") + case len(r.Match.StatusCodes) == 1: + if predicate, err = c.buildSinglePredicate(r.Match.StatusCodes[0]); err != nil { + return nil, err + } + + matcher := &matcherv3.Matcher_MatcherList_FieldMatcher{ + Predicate: predicate, + OnMatch: &matcherv3.Matcher_OnMatch{ + OnMatch: action, + }, + } + + matchers = append(matchers, matcher) + case len(r.Match.StatusCodes) > 1: + var predicates []*matcherv3.Matcher_MatcherList_Predicate + + for _, codeMatch := range r.Match.StatusCodes { + if predicate, err = c.buildSinglePredicate(codeMatch); err != nil { + return nil, err + } + + predicates = append(predicates, predicate) + } + + // Create a single matcher that ORs all the predicates together. + // The rule will match if any of the codes match. + matcher := &matcherv3.Matcher_MatcherList_FieldMatcher{ + Predicate: &matcherv3.Matcher_MatcherList_Predicate{ + MatchType: &matcherv3.Matcher_MatcherList_Predicate_OrMatcher{ + OrMatcher: &matcherv3.Matcher_MatcherList_Predicate_PredicateList{ + Predicate: predicates, + }, + }, + }, + OnMatch: &matcherv3.Matcher_OnMatch{ + OnMatch: action, + }, + } + + matchers = append(matchers, matcher) + } + + } + + // Create a MatcherList. + // The rules will be evaluated in order, and the first match wins. + cr := &respv3.CustomResponse{ + CustomResponseMatcher: &matcherv3.Matcher{ + MatcherType: &matcherv3.Matcher_MatcherList_{ + MatcherList: &matcherv3.Matcher_MatcherList{ + Matchers: matchers, + }, + }, + }, + } + + return cr, nil +} + +func (c *customResponse) buildSinglePredicate(codeMatch ir.StatusCodeMatch) (*matcherv3.Matcher_MatcherList_Predicate, error) { + var ( + httpAttributeCELInput *cncfv3.TypedExtensionConfig + statusCodeInput *cncfv3.TypedExtensionConfig + statusCodeCELMatcher *cncfv3.TypedExtensionConfig + err error + ) + + // Use CEL to match a range of status codes. + if codeMatch.Range != nil { + if httpAttributeCELInput, err = c.buildHTTPAttributeCELInput(); err != nil { + return nil, err + } + + if statusCodeCELMatcher, err = c.buildStatusCodeCELMatcher(*codeMatch.Range); err != nil { + return nil, err + } + + return &matcherv3.Matcher_MatcherList_Predicate{ + MatchType: &matcherv3.Matcher_MatcherList_Predicate_SinglePredicate_{ + SinglePredicate: &matcherv3.Matcher_MatcherList_Predicate_SinglePredicate{ + Input: httpAttributeCELInput, + Matcher: &matcherv3.Matcher_MatcherList_Predicate_SinglePredicate_CustomMatch{ + CustomMatch: statusCodeCELMatcher, + }, + }, + }, + }, nil + } else { + // Use exact string match to match a single status code. + if statusCodeInput, err = c.buildStatusCodeInput(); err != nil { + return nil, err + } + + return &matcherv3.Matcher_MatcherList_Predicate{ + MatchType: &matcherv3.Matcher_MatcherList_Predicate_SinglePredicate_{ + SinglePredicate: &matcherv3.Matcher_MatcherList_Predicate_SinglePredicate{ + Input: statusCodeInput, + Matcher: &matcherv3.Matcher_MatcherList_Predicate_SinglePredicate_ValueMatch{ + ValueMatch: &matcherv3.StringMatcher{ + MatchPattern: &matcherv3.StringMatcher_Exact{ + Exact: strconv.Itoa(*codeMatch.Value), + }, + }, + }, + }, + }, + }, nil + } +} + +func (c *customResponse) buildHTTPAttributeCELInput() (*cncfv3.TypedExtensionConfig, error) { + var ( + pb *anypb.Any + err error + ) + + if pb, err = anypb.New(&matcherv3.HttpAttributesCelMatchInput{}); err != nil { + return nil, err + } + + return &cncfv3.TypedExtensionConfig{ + Name: "http-attributes-cel-match-input", + TypedConfig: pb, + }, nil +} + +func (c *customResponse) buildStatusCodeInput() (*cncfv3.TypedExtensionConfig, error) { + var ( + pb *anypb.Any + err error + ) + + if pb, err = anypb.New(&envoymatcherv3.HttpResponseStatusCodeMatchInput{}); err != nil { + return nil, err + } + + return &cncfv3.TypedExtensionConfig{ + Name: "http-response-status-code-match-input", + TypedConfig: pb, + }, nil +} + +func (c *customResponse) buildStatusCodeCELMatcher(codeRange ir.StatusCodeRange) (*cncfv3.TypedExtensionConfig, error) { + var ( + pb *anypb.Any + err error + ) + + // Build the CEL expression AST: response.code >= codeRange.Start && response.code <= codeRange.End + matcher := &matcherv3.CelMatcher{ + ExprMatch: &typev3.CelExpression{ + ExprSpecifier: &typev3.CelExpression_ParsedExpr{ + ParsedExpr: &expr.ParsedExpr{ + Expr: &expr.Expr{ + Id: 9, + ExprKind: &expr.Expr_CallExpr{ + CallExpr: &expr.Expr_Call{ + Function: "_&&_", + Args: []*expr.Expr{ + { + Id: 3, + ExprKind: &expr.Expr_CallExpr{ + CallExpr: &expr.Expr_Call{ + Function: "_>=_", + Args: []*expr.Expr{ + { + Id: 2, + ExprKind: &expr.Expr_SelectExpr{ + SelectExpr: &expr.Expr_Select{ + Operand: &expr.Expr{ + Id: 1, + ExprKind: &expr.Expr_IdentExpr{ + IdentExpr: &expr.Expr_Ident{ + Name: "response", + }, + }, + }, + Field: "code", + }, + }, + }, + { + Id: 4, + ExprKind: &expr.Expr_ConstExpr{ + ConstExpr: &expr.Constant{ + ConstantKind: &expr.Constant_Int64Value{ + Int64Value: int64(codeRange.Start), + }, + }, + }, + }, + }, + }, + }, + }, + { + Id: 7, + ExprKind: &expr.Expr_CallExpr{ + CallExpr: &expr.Expr_Call{ + Function: "_<=_", + Args: []*expr.Expr{ + { + Id: 6, + ExprKind: &expr.Expr_SelectExpr{ + SelectExpr: &expr.Expr_Select{ + Operand: &expr.Expr{ + Id: 5, + ExprKind: &expr.Expr_IdentExpr{ + IdentExpr: &expr.Expr_Ident{ + Name: "response", + }, + }, + }, + Field: "code", + }, + }, + }, + { + Id: 8, + ExprKind: &expr.Expr_ConstExpr{ + ConstExpr: &expr.Constant{ + ConstantKind: &expr.Constant_Int64Value{ + Int64Value: int64(codeRange.End), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + if err := matcher.ValidateAll(); err != nil { + return nil, err + } + + if pb, err = anypb.New(matcher); err != nil { + return nil, err + } + + return &cncfv3.TypedExtensionConfig{ + Name: "cel-matcher", + TypedConfig: pb, + }, nil +} + +func (c *customResponse) buildAction(r ir.ResponseOverrideRule) (*matcherv3.Matcher_OnMatch_Action, error) { + response := &policyv3.LocalResponsePolicy{ + Body: &corev3.DataSource{ + Specifier: &corev3.DataSource_InlineString{ + InlineString: r.Response.Body, + }, + }, + } + + if r.Response.ContentType != nil && *r.Response.ContentType != "" { + response.ResponseHeadersToAdd = append(response.ResponseHeadersToAdd, &corev3.HeaderValueOption{ + Header: &corev3.HeaderValue{ + Key: "Content-Type", + Value: *r.Response.ContentType, + }, + AppendAction: corev3.HeaderValueOption_OVERWRITE_IF_EXISTS_OR_ADD, + }) + } + + var ( + pb *anypb.Any + err error + ) + + if err := response.ValidateAll(); err != nil { + return nil, err + } + + if pb, err = anypb.New(response); err != nil { + return nil, err + } + + return &matcherv3.Matcher_OnMatch_Action{ + Action: &cncfv3.TypedExtensionConfig{ + Name: r.Name, + TypedConfig: pb, + }, + }, nil +} + +// routeContainsResponseOverride returns true if ResponseOverride exists for the provided route. +func (c *customResponse) routeContainsResponseOverride(irRoute *ir.HTTPRoute) bool { + if irRoute != nil && + irRoute.Traffic != nil && + irRoute.Traffic.ResponseOverride != nil { + return true + } + return false +} + +func (c *customResponse) patchResources(tCtx *types.ResourceVersionTable, + routes []*ir.HTTPRoute, +) error { + return nil +} + +// patchRoute patches the provided route with the customResponse config if applicable. +// Note: this method enables the corresponding customResponse filter for the provided route. +func (c *customResponse) patchRoute(route *routev3.Route, irRoute *ir.HTTPRoute) error { + if route == nil { + return errors.New("xds route is nil") + } + if irRoute == nil { + return errors.New("ir route is nil") + } + if irRoute.Traffic == nil || irRoute.Traffic.ResponseOverride == nil { + return nil + } + filterName := c.customResponseFilterName(irRoute.Traffic.ResponseOverride) + if err := enableFilterOnRoute(route, filterName); err != nil { + return err + } + return nil +} diff --git a/internal/xds/translator/oidc.go b/internal/xds/translator/oidc.go index 963b7c8046da..e4e7b4a02161 100644 --- a/internal/xds/translator/oidc.go +++ b/internal/xds/translator/oidc.go @@ -53,9 +53,9 @@ func (*oidc) patchHCM(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTTPListe continue } - // Only generates one BasicAuth Envoy filter for each unique name. + // Only generates one OAuth2 Envoy filter for each unique name. // For example, if there are two routes under the same gateway with the - // same BasicAuth config, only one BasicAuth filter will be generated. + // same OAuth2 config, only one OAuth2 filter will be generated. if hcmContainsFilter(mgr, oauth2FilterName(route.Security.OIDC)) { continue } diff --git a/internal/xds/translator/ratelimit.go b/internal/xds/translator/ratelimit.go index 660bc2a7dec0..06b37bc4589f 100644 --- a/internal/xds/translator/ratelimit.go +++ b/internal/xds/translator/ratelimit.go @@ -157,11 +157,12 @@ func buildRouteRateLimits(descriptorPrefix string, global *ir.GlobalRateLimit) [ // Matches are ANDed rlActions := []*routev3.RateLimit_Action{routeDescriptor} for mIdx, match := range rule.HeaderMatches { + var action *routev3.RateLimit_Action // Case for distinct match if match.Distinct { // Setup RequestHeader actions descriptorKey := getRouteRuleDescriptor(rIdx, mIdx) - action := &routev3.RateLimit_Action{ + action = &routev3.RateLimit_Action{ ActionSpecifier: &routev3.RateLimit_Action_RequestHeaders_{ RequestHeaders: &routev3.RateLimit_Action_RequestHeaders{ HeaderName: match.Name, @@ -169,7 +170,6 @@ func buildRouteRateLimits(descriptorPrefix string, global *ir.GlobalRateLimit) [ }, }, } - rlActions = append(rlActions, action) } else { // Setup HeaderValueMatch actions descriptorKey := getRouteRuleDescriptor(rIdx, mIdx) @@ -184,7 +184,7 @@ func buildRouteRateLimits(descriptorPrefix string, global *ir.GlobalRateLimit) [ if match.Invert != nil && *match.Invert { expectMatch = false } - action := &routev3.RateLimit_Action{ + action = &routev3.RateLimit_Action{ ActionSpecifier: &routev3.RateLimit_Action_HeaderValueMatch_{ HeaderValueMatch: &routev3.RateLimit_Action_HeaderValueMatch{ DescriptorKey: descriptorKey, @@ -196,8 +196,8 @@ func buildRouteRateLimits(descriptorPrefix string, global *ir.GlobalRateLimit) [ }, }, } - rlActions = append(rlActions, action) } + rlActions = append(rlActions, action) } // To be able to rate limit each individual IP, we need to use a nested descriptors structure in the configuration @@ -236,7 +236,7 @@ func buildRouteRateLimits(descriptorPrefix string, global *ir.GlobalRateLimit) [ // Setup RemoteAddress action if distinct match is set if rule.CIDRMatch.Distinct { // Setup RemoteAddress action - action := &routev3.RateLimit_Action{ + action = &routev3.RateLimit_Action{ ActionSpecifier: &routev3.RateLimit_Action_RemoteAddress_{ RemoteAddress: &routev3.RateLimit_Action_RemoteAddress{}, }, @@ -245,8 +245,8 @@ func buildRouteRateLimits(descriptorPrefix string, global *ir.GlobalRateLimit) [ } } - // Case when header match is not set and the rate limit is applied - // to all traffic. + // Case when both header and cidr match are not set and the ratelimit + // will be applied to all traffic. if !rule.IsMatchSet() { // Setup GenericKey action action := &routev3.RateLimit_Action{ @@ -333,22 +333,21 @@ func BuildRateLimitServiceConfig(irListener *ir.HTTPListener) *rlsconfv3.RateLim func buildRateLimitServiceDescriptors(global *ir.GlobalRateLimit) []*rlsconfv3.RateLimitDescriptor { pbDescriptors := make([]*rlsconfv3.RateLimitDescriptor, 0, len(global.Rules)) + // The order in which matching descriptors are built is consistent with + // the order in which ratelimit actions are built: + // 1) Header Matches + // 2) CIDR Match + // 3) No Match for rIdx, rule := range global.Rules { - var head, cur *rlsconfv3.RateLimitDescriptor - if !rule.IsMatchSet() { - pbDesc := new(rlsconfv3.RateLimitDescriptor) - // GenericKey case - pbDesc.Key = getRouteRuleDescriptor(rIdx, -1) - pbDesc.Value = getRouteRuleDescriptor(rIdx, -1) - rateLimit := rlsconfv3.RateLimitPolicy{ - RequestsPerUnit: uint32(rule.Limit.Requests), - Unit: rlsconfv3.RateLimitUnit(rlsconfv3.RateLimitUnit_value[strings.ToUpper(string(rule.Limit.Unit))]), - } - pbDesc.RateLimit = &rateLimit - head = pbDesc - cur = head + rateLimitPolicy := &rlsconfv3.RateLimitPolicy{ + RequestsPerUnit: uint32(rule.Limit.Requests), + Unit: rlsconfv3.RateLimitUnit(rlsconfv3.RateLimitUnit_value[strings.ToUpper(string(rule.Limit.Unit))]), } + // We use a chain structure to describe the matching descriptors for one rule. + // The RateLimitPolicy should be added to the last descriptor in the chain. + var head, cur *rlsconfv3.RateLimitDescriptor + for mIdx, match := range rule.HeaderMatches { pbDesc := new(rlsconfv3.RateLimitDescriptor) // Case for distinct match @@ -361,15 +360,6 @@ func buildRateLimitServiceDescriptors(global *ir.GlobalRateLimit) []*rlsconfv3.R pbDesc.Value = getRouteRuleDescriptor(rIdx, mIdx) } - // Add the ratelimit values to the last descriptor - if mIdx == len(rule.HeaderMatches)-1 { - rateLimit := rlsconfv3.RateLimitPolicy{ - RequestsPerUnit: uint32(rule.Limit.Requests), - Unit: rlsconfv3.RateLimitUnit(rlsconfv3.RateLimitUnit_value[strings.ToUpper(string(rule.Limit.Unit))]), - } - pbDesc.RateLimit = &rateLimit - } - if mIdx == 0 { head = pbDesc } else { @@ -377,6 +367,9 @@ func buildRateLimitServiceDescriptors(global *ir.GlobalRateLimit) []*rlsconfv3.R } cur = pbDesc + + // Do not add the RateLimitPolicy to the last header match descriptor yet, + // as it is also possible that CIDR match descriptor also exist. } // EG supports two kinds of rate limit descriptors for the source IP: exact and distinct. @@ -405,25 +398,37 @@ func buildRateLimitServiceDescriptors(global *ir.GlobalRateLimit) []*rlsconfv3.R pbDesc := new(rlsconfv3.RateLimitDescriptor) pbDesc.Key = "masked_remote_address" pbDesc.Value = rule.CIDRMatch.CIDR - rateLimit := rlsconfv3.RateLimitPolicy{ - RequestsPerUnit: uint32(rule.Limit.Requests), - Unit: rlsconfv3.RateLimitUnit(rlsconfv3.RateLimitUnit_value[strings.ToUpper(string(rule.Limit.Unit))]), + + if cur != nil { + // The header match descriptor chain exist, add current + // descriptor to the chain. + cur.Descriptors = []*rlsconfv3.RateLimitDescriptor{pbDesc} + } else { + head = pbDesc } + cur = pbDesc if rule.CIDRMatch.Distinct { - pbDesc.Descriptors = []*rlsconfv3.RateLimitDescriptor{ - { - Key: "remote_address", - RateLimit: &rateLimit, - }, - } - } else { - pbDesc.RateLimit = &rateLimit + pbDesc := new(rlsconfv3.RateLimitDescriptor) + pbDesc.Key = "remote_address" + cur.Descriptors = []*rlsconfv3.RateLimitDescriptor{pbDesc} + cur = pbDesc } + } + + // Case when both header and cidr match are not set and the ratelimit + // will be applied to all traffic. + if !rule.IsMatchSet() { + pbDesc := new(rlsconfv3.RateLimitDescriptor) + // GenericKey case + pbDesc.Key = getRouteRuleDescriptor(rIdx, -1) + pbDesc.Value = getRouteRuleDescriptor(rIdx, -1) head = pbDesc cur = head } + // Add the ratelimit policy to the last descriptor of chain. + cur.RateLimit = rateLimitPolicy pbDescriptors = append(pbDescriptors, head) } diff --git a/internal/xds/translator/testdata/in/ratelimit-config/header-and-cidr-matches.yaml b/internal/xds/translator/testdata/in/ratelimit-config/header-and-cidr-matches.yaml new file mode 100644 index 000000000000..481b85986954 --- /dev/null +++ b/internal/xds/translator/testdata/in/ratelimit-config/header-and-cidr-matches.yaml @@ -0,0 +1,38 @@ +name: "first-listener" +address: "0.0.0.0" +port: 10080 +hostnames: +- "*" +path: + mergeSlashes: true + escapedSlashesAction: UnescapeAndRedirect +routes: +- name: "first-route" + traffic: + rateLimit: + global: + rules: + - headerMatches: + - name: "x-user-id" + exact: "one" + - name: "x-user-id" + exact: "two" + - name: "x-org-id" + exact: "three" + cidrMatch: + cidr: 0.0.0.0/0 + ip: 0.0.0.0 + maskLen: 0 + isIPv6: false + distinct: false + limit: + requests: 5 + unit: second + pathMatch: + exact: "foo/bar" + destination: + name: "first-route-dest" + settings: + - endpoints: + - host: "1.2.3.4" + port: 50000 diff --git a/internal/xds/translator/testdata/in/xds-ir/custom-response.yaml b/internal/xds/translator/testdata/in/xds-ir/custom-response.yaml new file mode 100644 index 000000000000..cb00ac65af9a --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/custom-response.yaml @@ -0,0 +1,56 @@ +http: + - address: 0.0.0.0 + hostnames: + - "*" + isHTTP2: false + metadata: + kind: Gateway + name: gateway-1 + namespace: default + sectionName: http + name: default/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + hostname: "*" + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0/match/-1/* + traffic: + responseOverride: + name: backendtrafficpolicy/default/policy-for-gateway + rules: + - match: + statusCodes: + - value: 404 + name: backendtrafficpolicy/default/policy-for-gateway/responseoverride/rule/0 + response: + body: gateway-1 Not Found + contentType: text/plain + - match: + statusCodes: + - value: 500 + - range: + end: 511 + start: 501 + name: backendtrafficpolicy/default/policy-for-gateway/responseoverride/rule/1 + response: + body: | + { + "error": "Internal Server Error" + } + contentType: application/json diff --git a/internal/xds/translator/testdata/in/xds-ir/ratelimit-headers-and-cidr.yaml b/internal/xds/translator/testdata/in/xds-ir/ratelimit-headers-and-cidr.yaml new file mode 100644 index 000000000000..fa9b6f31ae50 --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/ratelimit-headers-and-cidr.yaml @@ -0,0 +1,88 @@ +http: +- name: "first-listener" + address: "0.0.0.0" + port: 10080 + hostnames: + - "*" + path: + mergeSlashes: true + escapedSlashesAction: UnescapeAndRedirect + routes: + - name: "first-route" + hostname: "*" + traffic: + rateLimit: + global: + rules: + - headerMatches: + - name: "x-user-id" + exact: "one" + cidrMatch: + cidr: 192.168.0.0/16 + maskLen: 16 + limit: + requests: 5 + unit: second + destination: + name: "first-route-dest" + settings: + - endpoints: + - host: "1.2.3.4" + port: 50000 + - name: "second-route" + hostname: "*" + traffic: + rateLimit: + global: + rules: + - headerMatches: + - name: "x-user-id" + distinct: true + - name: "foobar" + distinct: true + cidrMatch: + cidr: 192.168.0.0/16 + maskLen: 16 + limit: + requests: 5 + unit: second + pathMatch: + exact: "example" + destination: + name: "second-route-dest" + settings: + - endpoints: + - host: "1.2.3.4" + port: 50000 + - name: "third-route" + hostname: "*" + traffic: + rateLimit: + global: + rules: + - headerMatches: + - name: "x-user-id" + exact: "one" + cidrMatch: + cidr: 192.168.0.0/16 + maskLen: 16 + limit: + requests: 5 + unit: second + - headerMatches: + - name: "x-user-id" + exact: "two" + - name: "foobar" + distinct: true + cidrMatch: + cidr: 192.169.0.0/16 + maskLen: 16 + limit: + requests: 10 + unit: second + destination: + name: "third-route-dest" + settings: + - endpoints: + - host: "1.2.3.4" + port: 50000 diff --git a/internal/xds/translator/testdata/out/ratelimit-config/header-and-cidr-matches.yaml b/internal/xds/translator/testdata/out/ratelimit-config/header-and-cidr-matches.yaml new file mode 100644 index 000000000000..83f5376dade3 --- /dev/null +++ b/internal/xds/translator/testdata/out/ratelimit-config/header-and-cidr-matches.yaml @@ -0,0 +1,38 @@ +name: first-listener +domain: first-listener +descriptors: + - key: first-route + value: first-route + rate_limit: null + descriptors: + - key: rule-0-match-0 + value: rule-0-match-0 + rate_limit: null + descriptors: + - key: rule-0-match-1 + value: rule-0-match-1 + rate_limit: null + descriptors: + - key: rule-0-match-2 + value: rule-0-match-2 + rate_limit: null + descriptors: + - key: masked_remote_address + value: 0.0.0.0/0 + rate_limit: + requests_per_unit: 5 + unit: SECOND + unlimited: false + name: "" + replaces: [] + descriptors: [] + shadow_mode: false + detailed_metric: false + shadow_mode: false + detailed_metric: false + shadow_mode: false + detailed_metric: false + shadow_mode: false + detailed_metric: false + shadow_mode: false + detailed_metric: false diff --git a/internal/xds/translator/testdata/out/xds-ir/custom-response.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/custom-response.clusters.yaml new file mode 100644 index 000000000000..9714612e3de5 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/custom-response.clusters.yaml @@ -0,0 +1,17 @@ +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: httproute/default/httproute-1/rule/0 + lbPolicy: LEAST_REQUEST + name: httproute/default/httproute-1/rule/0 + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + type: EDS diff --git a/internal/xds/translator/testdata/out/xds-ir/custom-response.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/custom-response.endpoints.yaml new file mode 100644 index 000000000000..29bb6b4e444c --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/custom-response.endpoints.yaml @@ -0,0 +1,12 @@ +- clusterName: httproute/default/httproute-1/rule/0 + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 7.7.7.7 + portValue: 8080 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: httproute/default/httproute-1/rule/0/backend/0 diff --git a/internal/xds/translator/testdata/out/xds-ir/custom-response.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/custom-response.listeners.yaml new file mode 100644 index 000000000000..19c565869601 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/custom-response.listeners.yaml @@ -0,0 +1,130 @@ +- address: + socketAddress: + address: 0.0.0.0 + portValue: 10080 + defaultFilterChain: + filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + commonHttpProtocolOptions: + headersWithUnderscoresAction: REJECT_REQUEST + http2ProtocolOptions: + initialConnectionWindowSize: 1048576 + initialStreamWindowSize: 65536 + maxConcurrentStreams: 100 + httpFilters: + - disabled: true + name: envoy.filters.http.custom_response/backendtrafficpolicy/default/policy-for-gateway + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.custom_response.v3.CustomResponse + customResponseMatcher: + matcherList: + matchers: + - onMatch: + action: + name: backendtrafficpolicy/default/policy-for-gateway/responseoverride/rule/0 + typedConfig: + '@type': type.googleapis.com/envoy.extensions.http.custom_response.local_response_policy.v3.LocalResponsePolicy + body: + inlineString: gateway-1 Not Found + responseHeadersToAdd: + - appendAction: OVERWRITE_IF_EXISTS_OR_ADD + header: + key: Content-Type + value: text/plain + predicate: + singlePredicate: + input: + name: http-response-status-code-match-input + typedConfig: + '@type': type.googleapis.com/envoy.type.matcher.v3.HttpResponseStatusCodeMatchInput + valueMatch: + exact: "404" + - onMatch: + action: + name: backendtrafficpolicy/default/policy-for-gateway/responseoverride/rule/1 + typedConfig: + '@type': type.googleapis.com/envoy.extensions.http.custom_response.local_response_policy.v3.LocalResponsePolicy + body: + inlineString: | + { + "error": "Internal Server Error" + } + responseHeadersToAdd: + - appendAction: OVERWRITE_IF_EXISTS_OR_ADD + header: + key: Content-Type + value: application/json + predicate: + orMatcher: + predicate: + - singlePredicate: + input: + name: http-response-status-code-match-input + typedConfig: + '@type': type.googleapis.com/envoy.type.matcher.v3.HttpResponseStatusCodeMatchInput + valueMatch: + exact: "500" + - singlePredicate: + customMatch: + name: cel-matcher + typedConfig: + '@type': type.googleapis.com/xds.type.matcher.v3.CelMatcher + exprMatch: + parsedExpr: + expr: + callExpr: + args: + - callExpr: + args: + - id: "2" + selectExpr: + field: code + operand: + id: "1" + identExpr: + name: response + - constExpr: + int64Value: "501" + id: "4" + function: _>=_ + id: "3" + - callExpr: + args: + - id: "6" + selectExpr: + field: code + operand: + id: "5" + identExpr: + name: response + - constExpr: + int64Value: "511" + id: "8" + function: _<=_ + id: "7" + function: _&&_ + id: "9" + input: + name: http-attributes-cel-match-input + typedConfig: + '@type': type.googleapis.com/xds.type.matcher.v3.HttpAttributesCelMatchInput + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + suppressEnvoyHeaders: true + mergeSlashes: true + normalizePath: true + pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT + rds: + configSource: + ads: {} + resourceApiVersion: V3 + routeConfigName: default/gateway-1/http + serverHeaderTransformation: PASS_THROUGH + statPrefix: http-10080 + useRemoteAddress: true + name: default/gateway-1/http + name: default/gateway-1/http + perConnectionBufferLimitBytes: 32768 diff --git a/internal/xds/translator/testdata/out/xds-ir/custom-response.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/custom-response.routes.yaml new file mode 100644 index 000000000000..8262bb6f3253 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/custom-response.routes.yaml @@ -0,0 +1,33 @@ +- ignorePortInHostMatching: true + name: default/gateway-1/http + virtualHosts: + - domains: + - '*' + metadata: + filterMetadata: + envoy-gateway: + resources: + - kind: Gateway + name: gateway-1 + namespace: default + sectionName: http + name: default/gateway-1/http/* + routes: + - match: + prefix: / + metadata: + filterMetadata: + envoy-gateway: + resources: + - kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0/match/-1/* + route: + cluster: httproute/default/httproute-1/rule/0 + upgradeConfigs: + - upgradeType: websocket + typedPerFilterConfig: + envoy.filters.http.custom_response/backendtrafficpolicy/default/policy-for-gateway: + '@type': type.googleapis.com/envoy.config.route.v3.FilterConfig + config: {} diff --git a/internal/xds/translator/testdata/out/xds-ir/ratelimit-headers-and-cidr.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/ratelimit-headers-and-cidr.clusters.yaml new file mode 100644 index 000000000000..0ba1749076af --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/ratelimit-headers-and-cidr.clusters.yaml @@ -0,0 +1,98 @@ +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: first-route-dest + lbPolicy: LEAST_REQUEST + name: first-route-dest + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + type: EDS +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: second-route-dest + lbPolicy: LEAST_REQUEST + name: second-route-dest + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + type: EDS +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: third-route-dest + lbPolicy: LEAST_REQUEST + name: third-route-dest + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + type: EDS +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + dnsRefreshRate: 30s + lbPolicy: LEAST_REQUEST + loadAssignment: + clusterName: ratelimit_cluster + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: envoy-ratelimit.envoy-gateway-system.svc.cluster.local + portValue: 8081 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: ratelimit_cluster/backend/0 + name: ratelimit_cluster + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + respectDnsTtl: true + transportSocket: + name: envoy.transport_sockets.tls + typedConfig: + '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + commonTlsContext: + tlsCertificates: + - certificateChain: + filename: /certs/tls.crt + privateKey: + filename: /certs/tls.key + validationContext: + trustedCa: + filename: /certs/ca.crt + type: STRICT_DNS + typedExtensionProtocolOptions: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + '@type': type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicitHttpConfig: + http2ProtocolOptions: + initialConnectionWindowSize: 1048576 + initialStreamWindowSize: 65536 diff --git a/internal/xds/translator/testdata/out/xds-ir/ratelimit-headers-and-cidr.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/ratelimit-headers-and-cidr.endpoints.yaml new file mode 100644 index 000000000000..475b89a087c3 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/ratelimit-headers-and-cidr.endpoints.yaml @@ -0,0 +1,36 @@ +- clusterName: first-route-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: first-route-dest/backend/0 +- clusterName: second-route-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: second-route-dest/backend/0 +- clusterName: third-route-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: third-route-dest/backend/0 diff --git a/internal/xds/translator/testdata/out/xds-ir/ratelimit-headers-and-cidr.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/ratelimit-headers-and-cidr.listeners.yaml new file mode 100644 index 000000000000..a80f448f0170 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/ratelimit-headers-and-cidr.listeners.yaml @@ -0,0 +1,44 @@ +- address: + socketAddress: + address: 0.0.0.0 + portValue: 10080 + defaultFilterChain: + filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + commonHttpProtocolOptions: + headersWithUnderscoresAction: REJECT_REQUEST + http2ProtocolOptions: + initialConnectionWindowSize: 1048576 + initialStreamWindowSize: 65536 + maxConcurrentStreams: 100 + httpFilters: + - name: envoy.filters.http.ratelimit + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit + domain: first-listener + enableXRatelimitHeaders: DRAFT_VERSION_03 + rateLimitService: + grpcService: + envoyGrpc: + clusterName: ratelimit_cluster + transportApiVersion: V3 + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + suppressEnvoyHeaders: true + mergeSlashes: true + normalizePath: true + pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT + rds: + configSource: + ads: {} + resourceApiVersion: V3 + routeConfigName: first-listener + serverHeaderTransformation: PASS_THROUGH + statPrefix: http-10080 + useRemoteAddress: true + name: first-listener + name: first-listener + perConnectionBufferLimitBytes: 32768 diff --git a/internal/xds/translator/testdata/out/xds-ir/ratelimit-headers-and-cidr.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/ratelimit-headers-and-cidr.routes.yaml new file mode 100644 index 000000000000..459d975a9b00 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/ratelimit-headers-and-cidr.routes.yaml @@ -0,0 +1,88 @@ +- ignorePortInHostMatching: true + name: first-listener + virtualHosts: + - domains: + - '*' + name: first-listener/* + routes: + - match: + prefix: / + name: first-route + route: + cluster: first-route-dest + rateLimits: + - actions: + - genericKey: + descriptorKey: first-route + descriptorValue: first-route + - headerValueMatch: + descriptorKey: rule-0-match-0 + descriptorValue: rule-0-match-0 + expectMatch: true + headers: + - name: x-user-id + stringMatch: + exact: one + - maskedRemoteAddress: + v4PrefixMaskLen: 16 + upgradeConfigs: + - upgradeType: websocket + - match: + path: example + name: second-route + route: + cluster: second-route-dest + rateLimits: + - actions: + - genericKey: + descriptorKey: second-route + descriptorValue: second-route + - requestHeaders: + descriptorKey: rule-0-match-0 + headerName: x-user-id + - requestHeaders: + descriptorKey: rule-0-match-1 + headerName: foobar + - maskedRemoteAddress: + v4PrefixMaskLen: 16 + upgradeConfigs: + - upgradeType: websocket + - match: + prefix: / + name: third-route + route: + cluster: third-route-dest + rateLimits: + - actions: + - genericKey: + descriptorKey: third-route + descriptorValue: third-route + - headerValueMatch: + descriptorKey: rule-0-match-0 + descriptorValue: rule-0-match-0 + expectMatch: true + headers: + - name: x-user-id + stringMatch: + exact: one + - maskedRemoteAddress: + v4PrefixMaskLen: 16 + - actions: + - genericKey: + descriptorKey: third-route + descriptorValue: third-route + - headerValueMatch: + descriptorKey: rule-1-match-0 + descriptorValue: rule-1-match-0 + expectMatch: true + headers: + - name: x-user-id + stringMatch: + exact: two + - requestHeaders: + descriptorKey: rule-1-match-1 + headerName: foobar + - maskedRemoteAddress: + v4PrefixMaskLen: 16 + upgradeConfigs: + - upgradeType: websocket diff --git a/site/content/en/docs/tasks/extensibility/wasm.md b/site/content/en/docs/tasks/extensibility/wasm.md index cb2e013dd808..1b1d32f9ecbe 100644 --- a/site/content/en/docs/tasks/extensibility/wasm.md +++ b/site/content/en/docs/tasks/extensibility/wasm.md @@ -23,7 +23,7 @@ kubectl get gateway/eg -o yaml ## Configuration -Envoy Gateway supports two types of Wasm extensions: +Envoy Gateway supports two types of Wasm extensions: * HTTP Wasm Extension: The Wasm extension is fetched from a remote URL. * Image Wasm Extension: The Wasm extension is packaged as an OCI image and fetched from an image registry. @@ -54,7 +54,7 @@ spec: code: type: HTTP http: - url: https://raw.githubusercontent.com/envoyproxy/envoy/main/examples/wasm-cc/lib/envoy_filter_http_wasm_example.wasm + url: https://raw.githubusercontent.com/envoyproxy/examples/main/wasm-cc/lib/envoy_filter_http_wasm_example.wasm sha256: 79c9f85128bb0177b6511afa85d587224efded376ac0ef76df56595f1e6315c0 EOF ``` @@ -80,7 +80,7 @@ spec: code: type: HTTP http: - url: https://raw.githubusercontent.com/envoyproxy/envoy/main/examples/wasm-cc/lib/envoy_filter_http_wasm_example.wasm + url: https://raw.githubusercontent.com/envoyproxy/examples/main/wasm-cc/lib/envoy_filter_http_wasm_example.wasm sha256: 79c9f85128bb0177b6511afa85d587224efded376ac0ef76df56595f1e6315c0 ``` diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index fe361099a842..4f562fce6115 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -483,6 +483,7 @@ _Appears in:_ | `rateLimit` | _[RateLimitSpec](#ratelimitspec)_ | false | RateLimit allows the user to limit the number of incoming requests
to a predefined value based on attributes within the traffic flow. | | `faultInjection` | _[FaultInjection](#faultinjection)_ | false | FaultInjection defines the fault injection policy to be applied. This configuration can be used to
inject delays and abort requests to mimic failure scenarios such as service failures and overloads | | `useClientProtocol` | _boolean_ | false | UseClientProtocol configures Envoy to prefer sending requests to backends using
the same HTTP protocol that the incoming request used. Defaults to false, which means
that Envoy will use the protocol indicated by the attached BackendRef. | +| `responseOverride` | _[ResponseOverride](#responseoverride) array_ | false | ResponseOverride defines the configuration to override specific responses with a custom one.
If multiple configurations are specified, the first one to match wins. | #### BasicAuth @@ -866,7 +867,7 @@ _Appears in:_ | Field | Type | Required | Description | | --- | --- | --- | --- | | `contentType` | _string_ | false | Content Type of the response. This will be set in the Content-Type header. | -| `body` | _[CustomResponseBody](#customresponsebody)_ | false | Body of the Custom Response | +| `body` | _[CustomResponseBody](#customresponsebody)_ | true | Body of the Custom Response | #### CustomResponseBody @@ -881,9 +882,9 @@ _Appears in:_ | Field | Type | Required | Description | | --- | --- | --- | --- | -| `type` | _[ResponseValueType](#responsevaluetype)_ | true | Type is the type of method to use to read the body value. | +| `type` | _[ResponseValueType](#responsevaluetype)_ | true | Type is the type of method to use to read the body value.
Valid values are Inline and ValueRef, default is Inline. | | `inline` | _string_ | false | Inline contains the value as an inline string. | -| `valueRef` | _[LocalObjectReference](#localobjectreference)_ | false | ValueRef contains the contents of the body
specified as a local object reference.
Only a reference to ConfigMap is supported. | +| `valueRef` | _[LocalObjectReference](#localobjectreference)_ | false | ValueRef contains the contents of the body
specified as a local object reference.
Only a reference to ConfigMap is supported.

The value of key `response.body` in the ConfigMap will be used as the response body.
If the key is not found, the first value in the ConfigMap will be used. | #### CustomResponseMatch @@ -897,7 +898,7 @@ _Appears in:_ | Field | Type | Required | Description | | --- | --- | --- | --- | -| `statusCode` | _[StatusCodeMatch](#statuscodematch) array_ | true | Status code to match on. The match evaluates to true if any of the matches are successful. | +| `statusCodes` | _[StatusCodeMatch](#statuscodematch) array_ | true | Status code to match on. The match evaluates to true if any of the matches are successful. | #### CustomTag @@ -1026,6 +1027,7 @@ _Appears in:_ | `envoy.filters.http.rbac` | EnvoyFilterRBAC defines the Envoy RBAC filter.
| | `envoy.filters.http.local_ratelimit` | EnvoyFilterLocalRateLimit defines the Envoy HTTP local rate limit filter.
| | `envoy.filters.http.ratelimit` | EnvoyFilterRateLimit defines the Envoy HTTP rate limit filter.
| +| `envoy.filters.http.custom_response` | EnvoyFilterCustomResponse defines the Envoy HTTP custom response filter.
| | `envoy.filters.http.router` | EnvoyFilterRouter defines the Envoy HTTP router filter.
| @@ -1479,7 +1481,7 @@ _Appears in:_ | `extraArgs` | _string array_ | false | ExtraArgs defines additional command line options that are provided to Envoy.
More info: https://www.envoyproxy.io/docs/envoy/latest/operations/cli#command-line-options
Note: some command line options are used internally(e.g. --log-level) so they cannot be provided here. | | `mergeGateways` | _boolean_ | false | MergeGateways defines if Gateway resources should be merged onto the same Envoy Proxy Infrastructure.
Setting this field to true would merge all Gateway Listeners under the parent Gateway Class.
This means that the port, protocol and hostname tuple must be unique for every listener.
If a duplicate listener is detected, the newer listener (based on timestamp) will be rejected and its status will be updated with a "Accepted=False" condition. | | `shutdown` | _[ShutdownConfig](#shutdownconfig)_ | false | Shutdown defines configuration for graceful envoy shutdown process. | -| `filterOrder` | _[FilterPosition](#filterposition) array_ | false | FilterOrder defines the order of filters in the Envoy proxy's HTTP filter chain.
The FilterPosition in the list will be applied in the order they are defined.
If unspecified, the default filter order is applied.
Default filter order is:

- envoy.filters.http.health_check

- envoy.filters.http.fault

- envoy.filters.http.cors

- envoy.filters.http.ext_authz

- envoy.filters.http.basic_auth

- envoy.filters.http.oauth2

- envoy.filters.http.jwt_authn

- envoy.filters.http.stateful_session

- envoy.filters.http.ext_proc

- envoy.filters.http.wasm

- envoy.filters.http.rbac

- envoy.filters.http.local_ratelimit

- envoy.filters.http.ratelimit

- envoy.filters.http.router

Note: "envoy.filters.http.router" cannot be reordered, it's always the last filter in the chain. | +| `filterOrder` | _[FilterPosition](#filterposition) array_ | false | FilterOrder defines the order of filters in the Envoy proxy's HTTP filter chain.
The FilterPosition in the list will be applied in the order they are defined.
If unspecified, the default filter order is applied.
Default filter order is:

- envoy.filters.http.health_check

- envoy.filters.http.fault

- envoy.filters.http.cors

- envoy.filters.http.ext_authz

- envoy.filters.http.basic_auth

- envoy.filters.http.oauth2

- envoy.filters.http.jwt_authn

- envoy.filters.http.stateful_session

- envoy.filters.http.ext_proc

- envoy.filters.http.wasm

- envoy.filters.http.rbac

- envoy.filters.http.local_ratelimit

- envoy.filters.http.ratelimit

- envoy.filters.http.custom_response

- envoy.filters.http.router

Note: "envoy.filters.http.router" cannot be reordered, it's always the last filter in the chain. | | `backendTLS` | _[BackendTLSConfig](#backendtlsconfig)_ | false | BackendTLS is the TLS configuration for the Envoy proxy to use when connecting to backends.
These settings are applied on backends for which TLS policies are specified. | @@ -3573,6 +3575,10 @@ ResponseValueType defines the types of values for the response body supported by _Appears in:_ - [CustomResponseBody](#customresponsebody) +| Value | Description | +| ----- | ----------- | +| `Inline` | ResponseValueTypeInline defines the "Inline" response body type.
| +| `ValueRef` | ResponseValueTypeValueRef defines the "ValueRef" response body type.
| @@ -3821,16 +3827,16 @@ _Appears in:_ - +StatusCodeMatch defines the configuration for matching a status code. _Appears in:_ - [CustomResponseMatch](#customresponsematch) | Field | Type | Required | Description | | --- | --- | --- | --- | -| `type` | _[StatusCodeValueType](#statuscodevaluetype)_ | true | Type is the type of value. | -| `value` | _string_ | false | Value contains the value of the status code. | -| `range` | _[StatusCodeRange](#statuscoderange)_ | false | ValueRef contains the contents of the body
specified as a local object reference.
Only a reference to ConfigMap is supported. | +| `type` | _[StatusCodeValueType](#statuscodevaluetype)_ | true | Type is the type of value.
Valid values are Value and Range, default is Value. | +| `value` | _integer_ | false | Value contains the value of the status code. | +| `range` | _[StatusCodeRange](#statuscoderange)_ | false | Range contains the range of status codes. | #### StatusCodeRange @@ -3857,6 +3863,10 @@ StatusCodeValueType defines the types of values for the status code match suppor _Appears in:_ - [StatusCodeMatch](#statuscodematch) +| Value | Description | +| ----- | ----------- | +| `Value` | StatusCodeValueTypeValue defines the "Value" status code match type.
| +| `Range` | StatusCodeValueTypeRange defines the "Range" status code match type.
| #### StringMatch diff --git a/site/content/en/latest/boilerplates/rollout-envoy-gateway.md b/site/content/en/latest/boilerplates/rollout-envoy-gateway.md new file mode 100644 index 000000000000..9072526868ce --- /dev/null +++ b/site/content/en/latest/boilerplates/rollout-envoy-gateway.md @@ -0,0 +1,10 @@ +--- +--- + +> After updating the `ConfigMap`, you will need to wait the configuration kicks in.
+> You can **force** the configuration to be reloaded by restarting the `envoy-gateway` deployment. +> +> ```shell +> kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system +> ``` +> \ No newline at end of file diff --git a/site/content/en/latest/install/gateway-addons-helm-api.md b/site/content/en/latest/install/gateway-addons-helm-api.md index a0ae0ed62f63..9835e21cd620 100644 --- a/site/content/en/latest/install/gateway-addons-helm-api.md +++ b/site/content/en/latest/install/gateway-addons-helm-api.md @@ -27,7 +27,7 @@ An Add-ons Helm chart for Envoy Gateway | https://grafana.github.io/helm-charts | grafana | 8.0.0 | | https://grafana.github.io/helm-charts | loki | 4.8.0 | | https://grafana.github.io/helm-charts | tempo | 1.3.1 | -| https://open-telemetry.github.io/opentelemetry-helm-charts | opentelemetry-collector | 0.73.1 | +| https://open-telemetry.github.io/opentelemetry-helm-charts | opentelemetry-collector | 0.108.0 | | https://prometheus-community.github.io/helm-charts | prometheus | 25.21.0 | ## Values @@ -82,7 +82,7 @@ An Add-ons Helm chart for Envoy Gateway | loki.singleBinary.replicas | int | `1` | | | loki.test.enabled | bool | `false` | | | loki.write.replicas | int | `0` | | -| opentelemetry-collector.config.exporters.logging.verbosity | string | `"detailed"` | | +| opentelemetry-collector.config.exporters.debug.verbosity | string | `"detailed"` | | | opentelemetry-collector.config.exporters.loki.endpoint | string | `"http://loki.monitoring.svc:3100/loki/api/v1/push"` | | | opentelemetry-collector.config.exporters.otlp.endpoint | string | `"tempo.monitoring.svc:4317"` | | | opentelemetry-collector.config.exporters.otlp.tls.insecure | bool | `true` | | @@ -91,6 +91,7 @@ An Add-ons Helm chart for Envoy Gateway | opentelemetry-collector.config.processors.attributes.actions[0].action | string | `"insert"` | | | opentelemetry-collector.config.processors.attributes.actions[0].key | string | `"loki.attribute.labels"` | | | opentelemetry-collector.config.processors.attributes.actions[0].value | string | `"k8s.pod.name, k8s.namespace.name"` | | +| opentelemetry-collector.config.receivers.datadog.endpoint | string | `"${env:MY_POD_IP}:8126"` | | | opentelemetry-collector.config.receivers.otlp.protocols.grpc.endpoint | string | `"${env:MY_POD_IP}:4317"` | | | opentelemetry-collector.config.receivers.otlp.protocols.http.endpoint | string | `"${env:MY_POD_IP}:4318"` | | | opentelemetry-collector.config.receivers.zipkin.endpoint | string | `"${env:MY_POD_IP}:9411"` | | @@ -99,12 +100,15 @@ An Add-ons Helm chart for Envoy Gateway | opentelemetry-collector.config.service.pipelines.logs.processors[0] | string | `"attributes"` | | | opentelemetry-collector.config.service.pipelines.logs.receivers[0] | string | `"otlp"` | | | opentelemetry-collector.config.service.pipelines.metrics.exporters[0] | string | `"prometheus"` | | -| opentelemetry-collector.config.service.pipelines.metrics.receivers[0] | string | `"otlp"` | | +| opentelemetry-collector.config.service.pipelines.metrics.receivers[0] | string | `"datadog"` | | +| opentelemetry-collector.config.service.pipelines.metrics.receivers[1] | string | `"otlp"` | | | opentelemetry-collector.config.service.pipelines.traces.exporters[0] | string | `"otlp"` | | -| opentelemetry-collector.config.service.pipelines.traces.receivers[0] | string | `"otlp"` | | -| opentelemetry-collector.config.service.pipelines.traces.receivers[1] | string | `"zipkin"` | | +| opentelemetry-collector.config.service.pipelines.traces.receivers[0] | string | `"datadog"` | | +| opentelemetry-collector.config.service.pipelines.traces.receivers[1] | string | `"otlp"` | | +| opentelemetry-collector.config.service.pipelines.traces.receivers[2] | string | `"zipkin"` | | | opentelemetry-collector.enabled | bool | `false` | | | opentelemetry-collector.fullnameOverride | string | `"otel-collector"` | | +| opentelemetry-collector.image.repository | string | `"otel/opentelemetry-collector-contrib"` | | | opentelemetry-collector.mode | string | `"deployment"` | | | prometheus.alertmanager.enabled | bool | `false` | | | prometheus.enabled | bool | `true` | | diff --git a/site/content/en/latest/tasks/extensibility/envoy-patch-policy.md b/site/content/en/latest/tasks/extensibility/envoy-patch-policy.md index e9709cc76512..54e69f41d0fb 100644 --- a/site/content/en/latest/tasks/extensibility/envoy-patch-policy.md +++ b/site/content/en/latest/tasks/extensibility/envoy-patch-policy.md @@ -80,11 +80,7 @@ data: {{% /tab %}} {{< /tabpane >}} -* After updating the `ConfigMap`, you will need to restart the `envoy-gateway` deployment so the configuration kicks in - -```shell -kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system -``` +{{< boilerplate rollout-envoy-gateway >}} ## Testing diff --git a/site/content/en/latest/tasks/extensibility/wasm.md b/site/content/en/latest/tasks/extensibility/wasm.md index 8a640471ee1d..baad6a5804fb 100644 --- a/site/content/en/latest/tasks/extensibility/wasm.md +++ b/site/content/en/latest/tasks/extensibility/wasm.md @@ -16,7 +16,7 @@ This instantiated resource can be linked to a [Gateway][Gateway] and [HTTPRoute] ## Configuration -Envoy Gateway supports two types of Wasm extensions: +Envoy Gateway supports two types of Wasm extensions: * HTTP Wasm Extension: The Wasm extension is fetched from a remote URL. * Image Wasm Extension: The Wasm extension is packaged as an OCI image and fetched from an image registry. @@ -47,7 +47,7 @@ spec: code: type: HTTP http: - url: https://raw.githubusercontent.com/envoyproxy/envoy/main/examples/wasm-cc/lib/envoy_filter_http_wasm_example.wasm + url: https://raw.githubusercontent.com/envoyproxy/examples/main/wasm-cc/lib/envoy_filter_http_wasm_example.wasm sha256: 79c9f85128bb0177b6511afa85d587224efded376ac0ef76df56595f1e6315c0 EOF ``` @@ -73,7 +73,7 @@ spec: code: type: HTTP http: - url: https://raw.githubusercontent.com/envoyproxy/envoy/main/examples/wasm-cc/lib/envoy_filter_http_wasm_example.wasm + url: https://raw.githubusercontent.com/envoyproxy/examples/main/wasm-cc/lib/envoy_filter_http_wasm_example.wasm sha256: 79c9f85128bb0177b6511afa85d587224efded376ac0ef76df56595f1e6315c0 ``` diff --git a/site/content/en/latest/tasks/observability/gateway-observability.md b/site/content/en/latest/tasks/observability/gateway-observability.md index 6e0040b4f5de..f23eb9097cf0 100644 --- a/site/content/en/latest/tasks/observability/gateway-observability.md +++ b/site/content/en/latest/tasks/observability/gateway-observability.md @@ -86,11 +86,7 @@ data: {{% /tab %}} {{< /tabpane >}} -After updating the `ConfigMap`, you will need to restart the `envoy-gateway` deployment so the configuration kicks in: - -```shell -kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system -``` +{{< boilerplate rollout-envoy-gateway >}} ### Enable Open Telemetry sink in Envoy Gateway @@ -157,11 +153,7 @@ data: {{% /tab %}} {{< /tabpane >}} -After updating the `ConfigMap`, you will need to restart the `envoy-gateway` deployment so the configuration kicks in: - -```shell -kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system -``` +{{< boilerplate rollout-envoy-gateway >}} Verify OTel-Collector metrics: diff --git a/site/content/en/latest/tasks/observability/proxy-trace.md b/site/content/en/latest/tasks/observability/proxy-trace.md index ddaf68e415a0..39243d329bc9 100644 --- a/site/content/en/latest/tasks/observability/proxy-trace.md +++ b/site/content/en/latest/tasks/observability/proxy-trace.md @@ -19,7 +19,7 @@ TEMPO_IP=$(kubectl get svc tempo -n monitoring -o jsonpath='{.status.loadBalance By default, Envoy Gateway doesn't send traces to any sink. You can enable traces by setting the `telemetry.tracing` in the [EnvoyProxy][envoy-proxy-crd] CRD. -Currently, Envoy Gateway support OpenTelemetry and [Zipkin](../../api/extension_types#zipkintracingprovider) tracer. +Currently, Envoy Gateway support OpenTelemetry, [Zipkin](../../api/extension_types#zipkintracingprovider) and Datadog tracer. ### Tracing Provider @@ -155,6 +155,66 @@ Verify zipkin traces from tempo: curl -s "http://$TEMPO_IP:3100/api/search?tags=component%3Dproxy+provider%3Dzipkin" | jq .traces ``` +{{% /tab %}} +{{% tab header="Datadog" %}} + +```shell +kubectl apply -f - <}} diff --git a/site/content/en/latest/tasks/observability/rate-limit-observability.md b/site/content/en/latest/tasks/observability/rate-limit-observability.md index a0e523d6c8a7..ec1244f731ee 100644 --- a/site/content/en/latest/tasks/observability/rate-limit-observability.md +++ b/site/content/en/latest/tasks/observability/rate-limit-observability.md @@ -91,8 +91,4 @@ data: {{% /tab %}} {{< /tabpane >}} -After updating the ConfigMap, you will need to restart the envoy-gateway deployment so the configuration kicks in: - -```shell -kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system -``` +{{< boilerplate rollout-envoy-gateway >}} diff --git a/site/content/en/latest/tasks/security/private-key-provider.md b/site/content/en/latest/tasks/security/private-key-provider.md index 529056b33e1e..24544f67973f 100644 --- a/site/content/en/latest/tasks/security/private-key-provider.md +++ b/site/content/en/latest/tasks/security/private-key-provider.md @@ -169,11 +169,7 @@ data: {{% /tab %}} {{< /tabpane >}} -* After updating the `ConfigMap`, you will need to restart the `envoy-gateway` deployment so the configuration kicks in - - ```shell - kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system - ``` +{{< boilerplate rollout-envoy-gateway >}} ## Create gateway for TLS termination diff --git a/site/content/en/latest/tasks/traffic/backend.md b/site/content/en/latest/tasks/traffic/backend.md index 0f2ade4dadd7..2bb2a4e647a9 100644 --- a/site/content/en/latest/tasks/traffic/backend.md +++ b/site/content/en/latest/tasks/traffic/backend.md @@ -94,11 +94,7 @@ data: {{% /tab %}} {{< /tabpane >}} -* After updating the `ConfigMap`, you will need to restart the `envoy-gateway` deployment so the configuration kicks in - -```shell -kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system -``` +{{< boilerplate rollout-envoy-gateway >}} ## Testing diff --git a/site/content/en/latest/tasks/traffic/global-rate-limit.md b/site/content/en/latest/tasks/traffic/global-rate-limit.md index 6c96b12efe75..15cc462dbf45 100644 --- a/site/content/en/latest/tasks/traffic/global-rate-limit.md +++ b/site/content/en/latest/tasks/traffic/global-rate-limit.md @@ -214,11 +214,7 @@ data: {{% /tab %}} {{< /tabpane >}} -* After updating the `ConfigMap`, you will need to restart the `envoy-gateway` deployment so the configuration kicks in - -```shell -kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system -``` +{{< boilerplate rollout-envoy-gateway >}} ## Rate Limit Specific User @@ -1287,11 +1283,7 @@ data: {{% /tab %}} {{< /tabpane >}} -* After updating the `ConfigMap`, you will need to restart the `envoy-gateway` deployment so the configuration kicks in - -```shell -kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system -``` +{{< boilerplate rollout-envoy-gateway >}} [Global Rate Limiting]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_features/global_rate_limiting [Local rate limiting]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_features/local_rate_limiting diff --git a/site/content/en/v1.1/tasks/extensibility/wasm.md b/site/content/en/v1.1/tasks/extensibility/wasm.md index cb2e013dd808..1b1d32f9ecbe 100644 --- a/site/content/en/v1.1/tasks/extensibility/wasm.md +++ b/site/content/en/v1.1/tasks/extensibility/wasm.md @@ -23,7 +23,7 @@ kubectl get gateway/eg -o yaml ## Configuration -Envoy Gateway supports two types of Wasm extensions: +Envoy Gateway supports two types of Wasm extensions: * HTTP Wasm Extension: The Wasm extension is fetched from a remote URL. * Image Wasm Extension: The Wasm extension is packaged as an OCI image and fetched from an image registry. @@ -54,7 +54,7 @@ spec: code: type: HTTP http: - url: https://raw.githubusercontent.com/envoyproxy/envoy/main/examples/wasm-cc/lib/envoy_filter_http_wasm_example.wasm + url: https://raw.githubusercontent.com/envoyproxy/examples/main/wasm-cc/lib/envoy_filter_http_wasm_example.wasm sha256: 79c9f85128bb0177b6511afa85d587224efded376ac0ef76df56595f1e6315c0 EOF ``` @@ -80,7 +80,7 @@ spec: code: type: HTTP http: - url: https://raw.githubusercontent.com/envoyproxy/envoy/main/examples/wasm-cc/lib/envoy_filter_http_wasm_example.wasm + url: https://raw.githubusercontent.com/envoyproxy/examples/main/wasm-cc/lib/envoy_filter_http_wasm_example.wasm sha256: 79c9f85128bb0177b6511afa85d587224efded376ac0ef76df56595f1e6315c0 ``` diff --git a/site/content/zh/latest/api/extension_types.md b/site/content/zh/latest/api/extension_types.md index fe361099a842..4f562fce6115 100644 --- a/site/content/zh/latest/api/extension_types.md +++ b/site/content/zh/latest/api/extension_types.md @@ -483,6 +483,7 @@ _Appears in:_ | `rateLimit` | _[RateLimitSpec](#ratelimitspec)_ | false | RateLimit allows the user to limit the number of incoming requests
to a predefined value based on attributes within the traffic flow. | | `faultInjection` | _[FaultInjection](#faultinjection)_ | false | FaultInjection defines the fault injection policy to be applied. This configuration can be used to
inject delays and abort requests to mimic failure scenarios such as service failures and overloads | | `useClientProtocol` | _boolean_ | false | UseClientProtocol configures Envoy to prefer sending requests to backends using
the same HTTP protocol that the incoming request used. Defaults to false, which means
that Envoy will use the protocol indicated by the attached BackendRef. | +| `responseOverride` | _[ResponseOverride](#responseoverride) array_ | false | ResponseOverride defines the configuration to override specific responses with a custom one.
If multiple configurations are specified, the first one to match wins. | #### BasicAuth @@ -866,7 +867,7 @@ _Appears in:_ | Field | Type | Required | Description | | --- | --- | --- | --- | | `contentType` | _string_ | false | Content Type of the response. This will be set in the Content-Type header. | -| `body` | _[CustomResponseBody](#customresponsebody)_ | false | Body of the Custom Response | +| `body` | _[CustomResponseBody](#customresponsebody)_ | true | Body of the Custom Response | #### CustomResponseBody @@ -881,9 +882,9 @@ _Appears in:_ | Field | Type | Required | Description | | --- | --- | --- | --- | -| `type` | _[ResponseValueType](#responsevaluetype)_ | true | Type is the type of method to use to read the body value. | +| `type` | _[ResponseValueType](#responsevaluetype)_ | true | Type is the type of method to use to read the body value.
Valid values are Inline and ValueRef, default is Inline. | | `inline` | _string_ | false | Inline contains the value as an inline string. | -| `valueRef` | _[LocalObjectReference](#localobjectreference)_ | false | ValueRef contains the contents of the body
specified as a local object reference.
Only a reference to ConfigMap is supported. | +| `valueRef` | _[LocalObjectReference](#localobjectreference)_ | false | ValueRef contains the contents of the body
specified as a local object reference.
Only a reference to ConfigMap is supported.

The value of key `response.body` in the ConfigMap will be used as the response body.
If the key is not found, the first value in the ConfigMap will be used. | #### CustomResponseMatch @@ -897,7 +898,7 @@ _Appears in:_ | Field | Type | Required | Description | | --- | --- | --- | --- | -| `statusCode` | _[StatusCodeMatch](#statuscodematch) array_ | true | Status code to match on. The match evaluates to true if any of the matches are successful. | +| `statusCodes` | _[StatusCodeMatch](#statuscodematch) array_ | true | Status code to match on. The match evaluates to true if any of the matches are successful. | #### CustomTag @@ -1026,6 +1027,7 @@ _Appears in:_ | `envoy.filters.http.rbac` | EnvoyFilterRBAC defines the Envoy RBAC filter.
| | `envoy.filters.http.local_ratelimit` | EnvoyFilterLocalRateLimit defines the Envoy HTTP local rate limit filter.
| | `envoy.filters.http.ratelimit` | EnvoyFilterRateLimit defines the Envoy HTTP rate limit filter.
| +| `envoy.filters.http.custom_response` | EnvoyFilterCustomResponse defines the Envoy HTTP custom response filter.
| | `envoy.filters.http.router` | EnvoyFilterRouter defines the Envoy HTTP router filter.
| @@ -1479,7 +1481,7 @@ _Appears in:_ | `extraArgs` | _string array_ | false | ExtraArgs defines additional command line options that are provided to Envoy.
More info: https://www.envoyproxy.io/docs/envoy/latest/operations/cli#command-line-options
Note: some command line options are used internally(e.g. --log-level) so they cannot be provided here. | | `mergeGateways` | _boolean_ | false | MergeGateways defines if Gateway resources should be merged onto the same Envoy Proxy Infrastructure.
Setting this field to true would merge all Gateway Listeners under the parent Gateway Class.
This means that the port, protocol and hostname tuple must be unique for every listener.
If a duplicate listener is detected, the newer listener (based on timestamp) will be rejected and its status will be updated with a "Accepted=False" condition. | | `shutdown` | _[ShutdownConfig](#shutdownconfig)_ | false | Shutdown defines configuration for graceful envoy shutdown process. | -| `filterOrder` | _[FilterPosition](#filterposition) array_ | false | FilterOrder defines the order of filters in the Envoy proxy's HTTP filter chain.
The FilterPosition in the list will be applied in the order they are defined.
If unspecified, the default filter order is applied.
Default filter order is:

- envoy.filters.http.health_check

- envoy.filters.http.fault

- envoy.filters.http.cors

- envoy.filters.http.ext_authz

- envoy.filters.http.basic_auth

- envoy.filters.http.oauth2

- envoy.filters.http.jwt_authn

- envoy.filters.http.stateful_session

- envoy.filters.http.ext_proc

- envoy.filters.http.wasm

- envoy.filters.http.rbac

- envoy.filters.http.local_ratelimit

- envoy.filters.http.ratelimit

- envoy.filters.http.router

Note: "envoy.filters.http.router" cannot be reordered, it's always the last filter in the chain. | +| `filterOrder` | _[FilterPosition](#filterposition) array_ | false | FilterOrder defines the order of filters in the Envoy proxy's HTTP filter chain.
The FilterPosition in the list will be applied in the order they are defined.
If unspecified, the default filter order is applied.
Default filter order is:

- envoy.filters.http.health_check

- envoy.filters.http.fault

- envoy.filters.http.cors

- envoy.filters.http.ext_authz

- envoy.filters.http.basic_auth

- envoy.filters.http.oauth2

- envoy.filters.http.jwt_authn

- envoy.filters.http.stateful_session

- envoy.filters.http.ext_proc

- envoy.filters.http.wasm

- envoy.filters.http.rbac

- envoy.filters.http.local_ratelimit

- envoy.filters.http.ratelimit

- envoy.filters.http.custom_response

- envoy.filters.http.router

Note: "envoy.filters.http.router" cannot be reordered, it's always the last filter in the chain. | | `backendTLS` | _[BackendTLSConfig](#backendtlsconfig)_ | false | BackendTLS is the TLS configuration for the Envoy proxy to use when connecting to backends.
These settings are applied on backends for which TLS policies are specified. | @@ -3573,6 +3575,10 @@ ResponseValueType defines the types of values for the response body supported by _Appears in:_ - [CustomResponseBody](#customresponsebody) +| Value | Description | +| ----- | ----------- | +| `Inline` | ResponseValueTypeInline defines the "Inline" response body type.
| +| `ValueRef` | ResponseValueTypeValueRef defines the "ValueRef" response body type.
| @@ -3821,16 +3827,16 @@ _Appears in:_ - +StatusCodeMatch defines the configuration for matching a status code. _Appears in:_ - [CustomResponseMatch](#customresponsematch) | Field | Type | Required | Description | | --- | --- | --- | --- | -| `type` | _[StatusCodeValueType](#statuscodevaluetype)_ | true | Type is the type of value. | -| `value` | _string_ | false | Value contains the value of the status code. | -| `range` | _[StatusCodeRange](#statuscoderange)_ | false | ValueRef contains the contents of the body
specified as a local object reference.
Only a reference to ConfigMap is supported. | +| `type` | _[StatusCodeValueType](#statuscodevaluetype)_ | true | Type is the type of value.
Valid values are Value and Range, default is Value. | +| `value` | _integer_ | false | Value contains the value of the status code. | +| `range` | _[StatusCodeRange](#statuscoderange)_ | false | Range contains the range of status codes. | #### StatusCodeRange @@ -3857,6 +3863,10 @@ StatusCodeValueType defines the types of values for the status code match suppor _Appears in:_ - [StatusCodeMatch](#statuscodematch) +| Value | Description | +| ----- | ----------- | +| `Value` | StatusCodeValueTypeValue defines the "Value" status code match type.
| +| `Range` | StatusCodeValueTypeRange defines the "Range" status code match type.
| #### StringMatch diff --git a/site/content/zh/latest/install/gateway-addons-helm-api.md b/site/content/zh/latest/install/gateway-addons-helm-api.md index a0ae0ed62f63..9835e21cd620 100644 --- a/site/content/zh/latest/install/gateway-addons-helm-api.md +++ b/site/content/zh/latest/install/gateway-addons-helm-api.md @@ -27,7 +27,7 @@ An Add-ons Helm chart for Envoy Gateway | https://grafana.github.io/helm-charts | grafana | 8.0.0 | | https://grafana.github.io/helm-charts | loki | 4.8.0 | | https://grafana.github.io/helm-charts | tempo | 1.3.1 | -| https://open-telemetry.github.io/opentelemetry-helm-charts | opentelemetry-collector | 0.73.1 | +| https://open-telemetry.github.io/opentelemetry-helm-charts | opentelemetry-collector | 0.108.0 | | https://prometheus-community.github.io/helm-charts | prometheus | 25.21.0 | ## Values @@ -82,7 +82,7 @@ An Add-ons Helm chart for Envoy Gateway | loki.singleBinary.replicas | int | `1` | | | loki.test.enabled | bool | `false` | | | loki.write.replicas | int | `0` | | -| opentelemetry-collector.config.exporters.logging.verbosity | string | `"detailed"` | | +| opentelemetry-collector.config.exporters.debug.verbosity | string | `"detailed"` | | | opentelemetry-collector.config.exporters.loki.endpoint | string | `"http://loki.monitoring.svc:3100/loki/api/v1/push"` | | | opentelemetry-collector.config.exporters.otlp.endpoint | string | `"tempo.monitoring.svc:4317"` | | | opentelemetry-collector.config.exporters.otlp.tls.insecure | bool | `true` | | @@ -91,6 +91,7 @@ An Add-ons Helm chart for Envoy Gateway | opentelemetry-collector.config.processors.attributes.actions[0].action | string | `"insert"` | | | opentelemetry-collector.config.processors.attributes.actions[0].key | string | `"loki.attribute.labels"` | | | opentelemetry-collector.config.processors.attributes.actions[0].value | string | `"k8s.pod.name, k8s.namespace.name"` | | +| opentelemetry-collector.config.receivers.datadog.endpoint | string | `"${env:MY_POD_IP}:8126"` | | | opentelemetry-collector.config.receivers.otlp.protocols.grpc.endpoint | string | `"${env:MY_POD_IP}:4317"` | | | opentelemetry-collector.config.receivers.otlp.protocols.http.endpoint | string | `"${env:MY_POD_IP}:4318"` | | | opentelemetry-collector.config.receivers.zipkin.endpoint | string | `"${env:MY_POD_IP}:9411"` | | @@ -99,12 +100,15 @@ An Add-ons Helm chart for Envoy Gateway | opentelemetry-collector.config.service.pipelines.logs.processors[0] | string | `"attributes"` | | | opentelemetry-collector.config.service.pipelines.logs.receivers[0] | string | `"otlp"` | | | opentelemetry-collector.config.service.pipelines.metrics.exporters[0] | string | `"prometheus"` | | -| opentelemetry-collector.config.service.pipelines.metrics.receivers[0] | string | `"otlp"` | | +| opentelemetry-collector.config.service.pipelines.metrics.receivers[0] | string | `"datadog"` | | +| opentelemetry-collector.config.service.pipelines.metrics.receivers[1] | string | `"otlp"` | | | opentelemetry-collector.config.service.pipelines.traces.exporters[0] | string | `"otlp"` | | -| opentelemetry-collector.config.service.pipelines.traces.receivers[0] | string | `"otlp"` | | -| opentelemetry-collector.config.service.pipelines.traces.receivers[1] | string | `"zipkin"` | | +| opentelemetry-collector.config.service.pipelines.traces.receivers[0] | string | `"datadog"` | | +| opentelemetry-collector.config.service.pipelines.traces.receivers[1] | string | `"otlp"` | | +| opentelemetry-collector.config.service.pipelines.traces.receivers[2] | string | `"zipkin"` | | | opentelemetry-collector.enabled | bool | `false` | | | opentelemetry-collector.fullnameOverride | string | `"otel-collector"` | | +| opentelemetry-collector.image.repository | string | `"otel/opentelemetry-collector-contrib"` | | | opentelemetry-collector.mode | string | `"deployment"` | | | prometheus.alertmanager.enabled | bool | `false` | | | prometheus.enabled | bool | `true` | | diff --git a/site/layouts/shortcodes/boilerplate.html b/site/layouts/shortcodes/boilerplate.html index b120a8d1b143..752c4ad2e893 100644 --- a/site/layouts/shortcodes/boilerplate.html +++ b/site/layouts/shortcodes/boilerplate.html @@ -15,7 +15,7 @@ {{- $pattern := printf "%s*" $name -}} {{- $resource := $bundle.Resources.GetMatch $pattern -}} {{- with $resource -}} - {{- .Content | markdownify -}} + {{- .Content -}} {{- else -}} {{- errorf "Could not find boilerplate '%s' (%s)" $name $position -}} {{- end -}} diff --git a/test/cel-validation/backendtrafficpolicy_test.go b/test/cel-validation/backendtrafficpolicy_test.go index 49f033eb6ae6..d5e6a1b2d1fd 100644 --- a/test/cel-validation/backendtrafficpolicy_test.go +++ b/test/cel-validation/backendtrafficpolicy_test.go @@ -1257,6 +1257,251 @@ func TestBackendTrafficPolicyTarget(t *testing.T) { }, wantErrors: []string{}, }, + { + desc: "both targetref and targetrefs specified", + mutate: func(btp *egv1a1.BackendTrafficPolicy) { + btp.Spec = egv1a1.BackendTrafficPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetRef: &gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{ + Group: gwapiv1a2.Group("gateway.networking.k8s.io"), + Kind: gwapiv1a2.Kind("Gateway"), + Name: gwapiv1a2.ObjectName("eg"), + }, + }, + }, + ResponseOverride: []*egv1a1.ResponseOverride{ + { + Match: egv1a1.CustomResponseMatch{ + StatusCodes: []egv1a1.StatusCodeMatch{ + { + Type: ptr.To(egv1a1.StatusCodeValueTypeValue), + Range: &egv1a1.StatusCodeRange{ + Start: 100, + End: 200, + }, + }, + }, + }, + }, + }, + } + }, + wantErrors: []string{ + "value must be set for type Value", + }, + }, + { + desc: "both targetref and targetrefs specified", + mutate: func(btp *egv1a1.BackendTrafficPolicy) { + btp.Spec = egv1a1.BackendTrafficPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetRef: &gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{ + Group: gwapiv1a2.Group("gateway.networking.k8s.io"), + Kind: gwapiv1a2.Kind("Gateway"), + Name: gwapiv1a2.ObjectName("eg"), + }, + }, + }, + ResponseOverride: []*egv1a1.ResponseOverride{ + { + Match: egv1a1.CustomResponseMatch{ + StatusCodes: []egv1a1.StatusCodeMatch{ + { + Range: &egv1a1.StatusCodeRange{ + Start: 100, + End: 200, + }, + }, + }, + }, + }, + }, + } + }, + wantErrors: []string{ + "value must be set for type Value", + }, + }, + { + desc: "both targetref and targetrefs specified", + mutate: func(btp *egv1a1.BackendTrafficPolicy) { + btp.Spec = egv1a1.BackendTrafficPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetRef: &gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{ + Group: gwapiv1a2.Group("gateway.networking.k8s.io"), + Kind: gwapiv1a2.Kind("Gateway"), + Name: gwapiv1a2.ObjectName("eg"), + }, + }, + }, + ResponseOverride: []*egv1a1.ResponseOverride{ + { + Match: egv1a1.CustomResponseMatch{ + StatusCodes: []egv1a1.StatusCodeMatch{ + { + Type: ptr.To(egv1a1.StatusCodeValueTypeRange), + Value: ptr.To(100), + }, + }, + }, + }, + }, + } + }, + wantErrors: []string{ + "range must be set for type Range", + }, + }, + { + desc: "both targetref and targetrefs specified", + mutate: func(btp *egv1a1.BackendTrafficPolicy) { + btp.Spec = egv1a1.BackendTrafficPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetRef: &gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{ + Group: gwapiv1a2.Group("gateway.networking.k8s.io"), + Kind: gwapiv1a2.Kind("Gateway"), + Name: gwapiv1a2.ObjectName("eg"), + }, + }, + }, + ResponseOverride: []*egv1a1.ResponseOverride{ + { + Match: egv1a1.CustomResponseMatch{ + StatusCodes: []egv1a1.StatusCodeMatch{ + { + Type: ptr.To(egv1a1.StatusCodeValueTypeRange), + Range: &egv1a1.StatusCodeRange{ + Start: 200, + End: 100, + }, + }, + }, + }, + }, + }, + } + }, + wantErrors: []string{ + "end must be greater than start", + }, + }, + { + desc: "both targetref and targetrefs specified", + mutate: func(btp *egv1a1.BackendTrafficPolicy) { + btp.Spec = egv1a1.BackendTrafficPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetRef: &gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{ + Group: gwapiv1a2.Group("gateway.networking.k8s.io"), + Kind: gwapiv1a2.Kind("Gateway"), + Name: gwapiv1a2.ObjectName("eg"), + }, + }, + }, + ResponseOverride: []*egv1a1.ResponseOverride{ + { + Match: egv1a1.CustomResponseMatch{ + StatusCodes: []egv1a1.StatusCodeMatch{ + { + Value: ptr.To(100), + }, + }, + }, + Response: egv1a1.CustomResponse{ + Body: egv1a1.CustomResponseBody{ + ValueRef: &gwapiv1a2.LocalObjectReference{ + Kind: gwapiv1a2.Kind("ConfigMap"), + Name: gwapiv1a2.ObjectName("eg"), + }, + }, + }, + }, + }, + } + }, + wantErrors: []string{ + "inline must be set for type Inline", + }, + }, + { + desc: "both targetref and targetrefs specified", + mutate: func(btp *egv1a1.BackendTrafficPolicy) { + btp.Spec = egv1a1.BackendTrafficPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetRef: &gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{ + Group: gwapiv1a2.Group("gateway.networking.k8s.io"), + Kind: gwapiv1a2.Kind("Gateway"), + Name: gwapiv1a2.ObjectName("eg"), + }, + }, + }, + ResponseOverride: []*egv1a1.ResponseOverride{ + { + Match: egv1a1.CustomResponseMatch{ + StatusCodes: []egv1a1.StatusCodeMatch{ + { + Value: ptr.To(100), + }, + }, + }, + Response: egv1a1.CustomResponse{ + Body: egv1a1.CustomResponseBody{ + Type: ptr.To(egv1a1.ResponseValueTypeValueRef), + Inline: ptr.To("foo"), + }, + }, + }, + }, + } + }, + wantErrors: []string{ + "valueRef must be set for type ValueRef", + }, + }, + { + desc: "both targetref and targetrefs specified", + mutate: func(btp *egv1a1.BackendTrafficPolicy) { + btp.Spec = egv1a1.BackendTrafficPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetRef: &gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{ + Group: gwapiv1a2.Group("gateway.networking.k8s.io"), + Kind: gwapiv1a2.Kind("Gateway"), + Name: gwapiv1a2.ObjectName("eg"), + }, + }, + }, + ResponseOverride: []*egv1a1.ResponseOverride{ + { + Match: egv1a1.CustomResponseMatch{ + StatusCodes: []egv1a1.StatusCodeMatch{ + { + Value: ptr.To(100), + }, + }, + }, + Response: egv1a1.CustomResponse{ + Body: egv1a1.CustomResponseBody{ + Type: ptr.To(egv1a1.ResponseValueTypeValueRef), + ValueRef: &gwapiv1a2.LocalObjectReference{ + Kind: gwapiv1a2.Kind("Foo"), + Name: gwapiv1a2.ObjectName("eg"), + }, + }, + }, + }, + }, + } + }, + wantErrors: []string{ + "only ConfigMap is supported for ValueRe", + }, + }, } for _, tc := range cases { diff --git a/test/e2e/testdata/ratelimit-headers-and-cidr-match.yaml b/test/e2e/testdata/ratelimit-headers-and-cidr-match.yaml new file mode 100644 index 000000000000..fef2f645a2b1 --- /dev/null +++ b/test/e2e/testdata/ratelimit-headers-and-cidr-match.yaml @@ -0,0 +1,45 @@ +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: BackendTrafficPolicy +metadata: + name: ratelimit-headers-and-cidr + namespace: gateway-conformance-infra +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: header-and-cidr-ratelimit + rateLimit: + type: Global + global: + rules: + - clientSelectors: + - headers: + - name: x-user-id + type: Exact + value: one + - name: x-user-org + type: Exact + value: acme + sourceCIDR: + value: 0.0.0.0/0 + type: Distinct + limit: + requests: 3 + unit: Hour +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: header-and-cidr-ratelimit + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + rules: + - matches: + - path: + type: PathPrefix + value: /get + backendRefs: + - name: infra-backend-v1 + port: 8080 diff --git a/test/e2e/testdata/response-override.yaml b/test/e2e/testdata/response-override.yaml new file mode 100644 index 000000000000..084747aaa6c8 --- /dev/null +++ b/test/e2e/testdata/response-override.yaml @@ -0,0 +1,62 @@ +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: response-override + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: infra-backend-v1 + port: 8080 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: response-override-config + namespace: gateway-conformance-infra +data: + response.body: '{"error": "Internal Server Error"}' +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: BackendTrafficPolicy +metadata: + name: response-override + namespace: gateway-conformance-infra +spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: response-override + responseOverride: + - match: + statusCodes: + - type: Value + value: 404 + response: + contentType: text/plain + body: + type: Inline + inline: "Oops! Your request is not found." + - match: + statusCodes: + - type: Value + value: 500 + - type: Range + range: + start: 501 + end: 511 + response: + contentType: application/json + body: + type: ValueRef + valueRef: + group: "" + kind: ConfigMap + name: response-override-config diff --git a/test/e2e/testdata/tracing-datadog.yaml b/test/e2e/testdata/tracing-datadog.yaml new file mode 100644 index 000000000000..e4f54a7eebea --- /dev/null +++ b/test/e2e/testdata/tracing-datadog.yaml @@ -0,0 +1,91 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: eg-special-case-datadog + namespace: gateway-conformance-infra +spec: + gatewayClassName: "{GATEWAY_CLASS_NAME}" + listeners: + - name: http + port: 80 + protocol: HTTP + allowedRoutes: + namespaces: + from: All + infrastructure: + parametersRef: + group: gateway.envoyproxy.io + kind: EnvoyProxy + name: datadog-tracing +--- +apiVersion: v1 +kind: Service +metadata: + name: datadog-agent + namespace: monitoring +spec: + selector: + app.kubernetes.io/instance: eg-addons + app.kubernetes.io/name: opentelemetry-collector + component: standalone-collector + ports: + - protocol: TCP + port: 8126 + targetPort: 8126 +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyProxy +metadata: + name: datadog-tracing + namespace: gateway-conformance-infra +spec: + logging: + level: + default: debug + telemetry: + tracing: + provider: + type: Datadog + backendRefs: + - name: datadog-agent + namespace: monitoring + port: 8126 + customTags: + "provider": + type: Literal + literal: + value: "datadog" + "k8s.cluster.name": + type: Literal + literal: + value: "envoy-gateway" + "k8s.pod.name": + type: Environment + environment: + name: ENVOY_POD_NAME + defaultValue: "-" + "k8s.namespace.name": + type: Environment + environment: + name: ENVOY_GATEWAY_NAMESPACE + defaultValue: "envoy-gateway-system" + shutdown: + drainTimeout: 5s + minDrainDuration: 1s +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: tracing-datadog + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: eg-special-case-datadog + rules: + - matches: + - path: + type: PathPrefix + value: /datadog + backendRefs: + - name: infra-backend-v2 + port: 8080 diff --git a/test/e2e/tests/ratelimit.go b/test/e2e/tests/ratelimit.go index b87576b60aa0..f0082d77e856 100644 --- a/test/e2e/tests/ratelimit.go +++ b/test/e2e/tests/ratelimit.go @@ -30,6 +30,7 @@ func init() { ConformanceTests = append(ConformanceTests, RateLimitHeadersDisabled) ConformanceTests = append(ConformanceTests, RateLimitBasedJwtClaimsTest) ConformanceTests = append(ConformanceTests, RateLimitMultipleListenersTest) + ConformanceTests = append(ConformanceTests, RateLimitHeadersAndCIDRMatchTest) } var RateLimitCIDRMatchTest = suite.ConformanceTest{ @@ -538,6 +539,118 @@ var RateLimitMultipleListenersTest = suite.ConformanceTest{ }, } +var RateLimitHeadersAndCIDRMatchTest = suite.ConformanceTest{ + ShortName: "RateLimitHeadersAndCIDRMatch", + Description: "Limit requests on rule that has both headers and cidr matches", + Manifests: []string{"testdata/ratelimit-headers-and-cidr-match.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "header-and-cidr-ratelimit", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + + t.Run("all matched both headers and cidr can got limited", func(t *testing.T) { + requestHeaders := map[string]string{ + "x-user-id": "one", + "x-user-org": "acme", + } + + ratelimitHeader := make(map[string]string) + expectOkResp := http.ExpectedResponse{ + Request: http.Request{ + Path: "/get", + Headers: requestHeaders, + }, + Response: http.Response{ + StatusCode: 200, + Headers: ratelimitHeader, + }, + Namespace: ns, + } + expectOkResp.Response.Headers["X-Ratelimit-Limit"] = "3, 3;w=3600" + expectOkReq := http.MakeRequest(t, &expectOkResp, gwAddr, "HTTP", "http") + + expectLimitResp := http.ExpectedResponse{ + Request: http.Request{ + Path: "/get", + Headers: requestHeaders, + }, + Response: http.Response{ + StatusCode: 429, + }, + Namespace: ns, + } + expectLimitReq := http.MakeRequest(t, &expectLimitResp, gwAddr, "HTTP", "http") + + // should just send exactly 4 requests, and expect 429 + + // keep sending requests till get 200 first, that will cost one 200 + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectOkResp) + + // fire the rest of the requests + if err := GotExactExpectedResponse(t, 2, suite.RoundTripper, expectOkReq, expectOkResp); err != nil { + t.Errorf("failed to get expected response for the first three requests: %v", err) + } + if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, expectLimitReq, expectLimitResp); err != nil { + t.Errorf("failed to get expected response for the last (fourth) request: %v", err) + } + }) + + t.Run("only partly matched headers cannot got limited", func(t *testing.T) { + requestHeaders := map[string]string{ + "x-user-id": "one", + } + + // it does not require any rate limit header, since this request never be rate limited. + expectOkResp := http.ExpectedResponse{ + Request: http.Request{ + Path: "/get", + Headers: requestHeaders, + }, + Response: http.Response{ + StatusCode: 200, + }, + Namespace: ns, + } + expectOkReq := http.MakeRequest(t, &expectOkResp, gwAddr, "HTTP", "http") + + // send exactly 4 requests, and still expect 200 + + // keep sending requests till get 200 first, that will cost one 200 + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectOkResp) + + // fire the rest of the requests + if err := GotExactExpectedResponse(t, 3, suite.RoundTripper, expectOkReq, expectOkResp); err != nil { + t.Errorf("failed to get expected responses for the request: %v", err) + } + }) + + t.Run("only matched cidr cannot got limited", func(t *testing.T) { + // it does not require any rate limit header, since this request never be rate limited. + expectOkResp := http.ExpectedResponse{ + Request: http.Request{ + Path: "/get", + }, + Response: http.Response{ + StatusCode: 200, + }, + Namespace: ns, + } + expectOkReq := http.MakeRequest(t, &expectOkResp, gwAddr, "HTTP", "http") + + // send exactly 4 requests, and still expect 200 + + // keep sending requests till get 200 first, that will cost one 200 + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectOkResp) + + // fire the rest of the requests + if err := GotExactExpectedResponse(t, 3, suite.RoundTripper, expectOkReq, expectOkResp); err != nil { + t.Errorf("failed to get expected responses for the request: %v", err) + } + }) + }, +} + func GotExactExpectedResponse(t *testing.T, n int, r roundtripper.RoundTripper, req roundtripper.Request, resp http.ExpectedResponse) error { for i := 0; i < n; i++ { cReq, cRes, err := r.CaptureRoundTrip(req) diff --git a/test/e2e/tests/response-override.go b/test/e2e/tests/response-override.go new file mode 100644 index 000000000000..b21db88e2428 --- /dev/null +++ b/test/e2e/tests/response-override.go @@ -0,0 +1,83 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +//go:build e2e + +package tests + +import ( + "fmt" + "io" + "net/http" + "net/url" + "testing" + + "k8s.io/apimachinery/pkg/types" + gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" + gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + httputils "sigs.k8s.io/gateway-api/conformance/utils/http" + "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" + "sigs.k8s.io/gateway-api/conformance/utils/suite" + + "github.com/envoyproxy/gateway/internal/gatewayapi" + "github.com/envoyproxy/gateway/internal/gatewayapi/resource" +) + +func init() { + ConformanceTests = append(ConformanceTests, ResponseOverrideTest) +} + +var ResponseOverrideTest = suite.ConformanceTest{ + ShortName: "ResponseOverrideSpecificUser", + Description: "Response Override", + Manifests: []string{"testdata/response-override.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + t.Run("response override", func(t *testing.T) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "response-override", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + + ancestorRef := gwapiv1a2.ParentReference{ + Group: gatewayapi.GroupPtr(gwapiv1.GroupName), + Kind: gatewayapi.KindPtr(resource.KindGateway), + Namespace: gatewayapi.NamespacePtr(gwNN.Namespace), + Name: gwapiv1.ObjectName(gwNN.Name), + } + BackendTrafficPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "response-override", Namespace: ns}, suite.ControllerName, ancestorRef) + verifyResponseOverride(t, gwAddr, 404, "text/plain", "Oops! Your request is not found.") + verifyResponseOverride(t, gwAddr, 500, "application/json", `{"error": "Internal Server Error"}`) + }) + }, +} + +func verifyResponseOverride(t *testing.T, gwAddr string, statusCode int, expectedContentType string, expectedBody string) { + reqURL := url.URL{ + Scheme: "http", + Host: httputils.CalculateHost(t, gwAddr, "http"), + Path: fmt.Sprintf("/status/%d", statusCode), + } + + rsp, err := http.Get(reqURL.String()) + if err != nil { + t.Fatalf("failed to get response: %v", err) + } + + // Verify that the response body is overridden + defer rsp.Body.Close() + body, err := io.ReadAll(rsp.Body) + if err != nil { + t.Fatalf("failed to read response body: %v", err) + } + if string(body) != expectedBody { + t.Errorf("expected response body to be %s but got %s", expectedBody, string(body)) + } + + // Verify that the content type is overridden + contentType := rsp.Header.Get("Content-Type") + if contentType != expectedContentType { + t.Errorf("expected content type to be %s but got %s", expectedContentType, contentType) + } +} diff --git a/test/e2e/tests/tracing.go b/test/e2e/tests/tracing.go index 5ead14b48a10..93e4cb23897b 100644 --- a/test/e2e/tests/tracing.go +++ b/test/e2e/tests/tracing.go @@ -24,7 +24,7 @@ import ( ) func init() { - ConformanceTests = append(ConformanceTests, OpenTelemetryTracingTest, ZipkinTracingTest) + ConformanceTests = append(ConformanceTests, OpenTelemetryTracingTest, ZipkinTracingTest, DatadogTracingTest) } var OpenTelemetryTracingTest = suite.ConformanceTest{ @@ -141,3 +141,68 @@ var ZipkinTracingTest = suite.ConformanceTest{ }) }, } + +var DatadogTracingTest = suite.ConformanceTest{ + ShortName: "DatadogTracing", + Description: "Make sure Datadog tracing is working", + Manifests: []string{"testdata/tracing-datadog.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + t.Run("tempo", func(t *testing.T) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "tracing-datadog", Namespace: ns} + gwNN := types.NamespacedName{Name: "eg-special-case-datadog", Namespace: ns} + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + + expectedResponse := httputils.ExpectedResponse{ + Request: httputils.Request{ + Path: "/datadog", + }, + Response: httputils.Response{ + StatusCode: 200, + }, + Namespace: ns, + } + // make sure listener is ready + httputils.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) + + tags := map[string]string{ + "component": "proxy", + "provider": "datadog", + "service.name": fmt.Sprintf("%s.%s", gwNN.Name, gwNN.Namespace), + } + if err := wait.PollUntilContextTimeout(context.TODO(), time.Second, time.Minute, true, + func(ctx context.Context) (bool, error) { + preCount, err := QueryTraceFromTempo(t, suite.Client, tags) + if err != nil { + tlog.Logf(t, "failed to get trace count from tempo: %v", err) + return false, nil + } + + httputils.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) + + // looks like we need almost 15 seconds to get the trace from Tempo? + err = wait.PollUntilContextTimeout(context.TODO(), time.Second, 60*time.Second, true, func(ctx context.Context) (done bool, err error) { + curCount, err := QueryTraceFromTempo(t, suite.Client, tags) + if err != nil { + tlog.Logf(t, "failed to get curCount count from tempo: %v", err) + return false, nil + } + + if curCount > preCount { + return true, nil + } + + return false, nil + }) + if err != nil { + tlog.Logf(t, "failed to get current count from tempo: %v", err) + return false, nil + } + + return true, nil + }); err != nil { + t.Errorf("failed to get trace from tempo: %v", err) + } + }) + }, +} diff --git a/test/helm/gateway-addons-helm/e2e.out.yaml b/test/helm/gateway-addons-helm/e2e.out.yaml index 15445239f3de..1e7c8fda8ff8 100644 --- a/test/helm/gateway-addons-helm/e2e.out.yaml +++ b/test/helm/gateway-addons-helm/e2e.out.yaml @@ -32,10 +32,10 @@ metadata: name: otel-collector namespace: monitoring labels: - helm.sh/chart: opentelemetry-collector-0.73.1 + helm.sh/chart: opentelemetry-collector-0.108.0 app.kubernetes.io/name: opentelemetry-collector app.kubernetes.io/instance: gateway-addons-helm - app.kubernetes.io/version: "0.88.0" + app.kubernetes.io/version: "0.111.0" app.kubernetes.io/managed-by: Helm --- # Source: gateway-addons-helm/charts/prometheus/templates/serviceaccount.yaml @@ -219,16 +219,16 @@ metadata: name: otel-collector namespace: monitoring labels: - helm.sh/chart: opentelemetry-collector-0.73.1 + helm.sh/chart: opentelemetry-collector-0.108.0 app.kubernetes.io/name: opentelemetry-collector app.kubernetes.io/instance: gateway-addons-helm - app.kubernetes.io/version: "0.88.0" + app.kubernetes.io/version: "0.111.0" app.kubernetes.io/managed-by: Helm + data: relay: | exporters: - debug: {} - logging: + debug: verbosity: detailed loki: endpoint: http://loki.monitoring.svc:3100/loki/api/v1/push @@ -239,9 +239,8 @@ data: prometheus: endpoint: 0.0.0.0:19001 extensions: - health_check: {} - memory_ballast: - size_in_percentage: 40 + health_check: + endpoint: ${env:MY_POD_IP}:13133 processors: attributes: actions: @@ -254,6 +253,8 @@ data: limit_percentage: 80 spike_limit_percentage: 25 receivers: + datadog: + endpoint: ${env:MY_POD_IP}:8126 jaeger: protocols: grpc: @@ -296,6 +297,7 @@ data: - memory_limiter - batch receivers: + - datadog - otlp traces: exporters: @@ -304,6 +306,7 @@ data: - memory_limiter - batch receivers: + - datadog - otlp - zipkin telemetry: @@ -9517,11 +9520,12 @@ metadata: name: otel-collector namespace: monitoring labels: - helm.sh/chart: opentelemetry-collector-0.73.1 + helm.sh/chart: opentelemetry-collector-0.108.0 app.kubernetes.io/name: opentelemetry-collector app.kubernetes.io/instance: gateway-addons-helm - app.kubernetes.io/version: "0.88.0" + app.kubernetes.io/version: "0.111.0" app.kubernetes.io/managed-by: Helm + component: standalone-collector spec: type: ClusterIP @@ -9733,11 +9737,12 @@ metadata: name: otel-collector namespace: monitoring labels: - helm.sh/chart: opentelemetry-collector-0.73.1 + helm.sh/chart: opentelemetry-collector-0.108.0 app.kubernetes.io/name: opentelemetry-collector app.kubernetes.io/instance: gateway-addons-helm - app.kubernetes.io/version: "0.88.0" + app.kubernetes.io/version: "0.111.0" app.kubernetes.io/managed-by: Helm + spec: replicas: 1 revisionHistoryLimit: 10 @@ -9751,7 +9756,7 @@ spec: template: metadata: annotations: - checksum/config: 4eb06aca6ff4da4de927cb9ba7d8ceb883d2484011fbd670683037b8ea4d996c + checksum/config: 270a8503091b51a264317115cf6df46b4501b03fc135eca95b93dca57a522a70 labels: app.kubernetes.io/name: opentelemetry-collector @@ -9765,12 +9770,11 @@ spec: {} containers: - name: opentelemetry-collector - command: - - /otelcol-contrib + args: - --config=/conf/relay.yaml securityContext: {} - image: "otel/opentelemetry-collector-contrib:0.88.0" + image: "otel/opentelemetry-collector-contrib:0.111.0" imagePullPolicy: IfNotPresent ports: diff --git a/tools/make/docs.mk b/tools/make/docs.mk index d6c6b4d8232e..698896f089ac 100644 --- a/tools/make/docs.mk +++ b/tools/make/docs.mk @@ -118,6 +118,9 @@ docs-check-links: # Check for broken links in the docs @$(LOG_TARGET) linkinator site/public/ -r --concurrency 25 --skip $(LINKINATOR_IGNORE) +docs-markdown-lint: + markdownlint -c .github/markdown_lint_config.json site/content/* + release-notes-docs: $(tools/release-notes-docs) @$(LOG_TARGET) @for file in $(wildcard release-notes/*.yaml); do \