From d60959ec98d46df61de6127464677fa571fd2dae Mon Sep 17 00:00:00 2001 From: Sotiris Nanopoulos Date: Fri, 28 Jul 2023 12:02:14 -0400 Subject: [PATCH] Adds support for treating missing headers as empty (#5584) `TreatMissingHeadersAsEmpty` specifies if the header match rule specified header does not exist, this header value will be treated as empty. Defaults to false. Unlike the underlying Envoy implementation this is **only** supported for negative matches (e.g. NotContains, NotExact). The reason that I implemented only for negative matches is that is substantially simpler to reason about. I am open to implementing it for all cases if you think it is going to be useful. Signed-off-by: Sotiris Nanopoulos --- Makefile | 4 +- apis/projectcontour/v1/httpproxy.go | 9 ++ changelogs/unreleased/5584-davinci26-minor.md | 4 + examples/contour/01-crds.yaml | 83 +++++++++++++++-- examples/render/contour-deployment.yaml | 83 +++++++++++++++-- .../render/contour-gateway-provisioner.yaml | 83 +++++++++++++++-- examples/render/contour-gateway.yaml | 83 +++++++++++++++-- examples/render/contour.yaml | 83 +++++++++++++++-- internal/dag/conditions.go | 22 +++-- internal/dag/dag.go | 12 ++- internal/dag/httpproxy_processor.go | 3 + internal/envoy/v3/route.go | 2 + internal/envoy/v3/route_test.go | 48 ++++++++++ .../featuretests/v3/headercondition_test.go | 91 ++++++++++++++----- internal/featuretests/v3/httpproxy.go | 18 ++-- .../docs/main/config/api-reference.html | 18 ++++ .../docs/main/config/request-routing.md | 11 ++- .../httpproxy/header_condition_match_test.go | 64 +++++++++++++ 18 files changed, 631 insertions(+), 90 deletions(-) create mode 100644 changelogs/unreleased/5584-davinci26-minor.md diff --git a/Makefile b/Makefile index fa855eb79e8..70ea501f8e4 100644 --- a/Makefile +++ b/Makefile @@ -256,7 +256,7 @@ generate-crd-yaml: generate-gateway-yaml: @echo "Generating Gateway API CRD YAML documents..." @GATEWAY_API_VERSION=$(GATEWAY_API_VERSION) ./hack/generate-gateway-yaml.sh - + .PHONY: generate-api-docs generate-api-docs: @@ -306,7 +306,7 @@ setup-kind-cluster: ## Make a kind cluster for testing install-contour-working: | setup-kind-cluster ## Install the local working directory version of Contour into a kind cluster ./test/scripts/install-contour-working.sh -.PHONY: install-contour-release +.PHONY: install-contour-release install-contour-release: | setup-kind-cluster ## Install the release version of Contour in CONTOUR_UPGRADE_FROM_VERSION, defaults to latest ./test/scripts/install-contour-release.sh $(CONTOUR_UPGRADE_FROM_VERSION) diff --git a/apis/projectcontour/v1/httpproxy.go b/apis/projectcontour/v1/httpproxy.go index c05da51adef..02f6cd4c3b6 100644 --- a/apis/projectcontour/v1/httpproxy.go +++ b/apis/projectcontour/v1/httpproxy.go @@ -90,6 +90,8 @@ type MatchCondition struct { // HeaderMatchCondition specifies how to conditionally match against HTTP // headers. The Name field is required, only one of Present, NotPresent, // Contains, NotContains, Exact, NotExact and Regex can be set. +// For negative matching rules only (e.g. NotContains or NotExact) you can set +// TreatMissingAsEmpty. // IgnoreCase has no effect for Regex. type HeaderMatchCondition struct { // Name is the name of the header to match against. Name is required. @@ -137,6 +139,13 @@ type HeaderMatchCondition struct { // value. // +optional Regex string `json:"regex,omitempty"` + + // TreatMissingAsEmpty specifies if the header match rule specified header + // does not exist, this header value will be treated as empty. Defaults to false. + // Unlike the underlying Envoy implementation this is **only** supported for + // negative matches (e.g. NotContains, NotExact). + // +optional + TreatMissingAsEmpty bool `json:"treatMissingAsEmpty,omitempty"` } // QueryParameterMatchCondition specifies how to conditionally match against HTTP diff --git a/changelogs/unreleased/5584-davinci26-minor.md b/changelogs/unreleased/5584-davinci26-minor.md new file mode 100644 index 00000000000..c9988ac1bed --- /dev/null +++ b/changelogs/unreleased/5584-davinci26-minor.md @@ -0,0 +1,4 @@ +## Adds support for treating missing headers as empty when they are not present as part of header matching + +`TreatMissingAsEmpty` specifies if the header match rule specified header does not exist, this header value will be treated as empty. Defaults to false. +Unlike the underlying Envoy implementation this is **only** supported for negative matches (e.g. NotContains, NotExact). diff --git a/examples/contour/01-crds.yaml b/examples/contour/01-crds.yaml index bcc1a3eb050..68283476307 100644 --- a/examples/contour/01-crds.yaml +++ b/examples/contour/01-crds.yaml @@ -727,8 +727,10 @@ spec: headers. The Name field is required, only one of Present, NotPresent, Contains, NotContains, Exact, NotExact and Regex - can be set. IgnoreCase has no effect for - Regex. + can be set. For negative matching rules + only (e.g. NotContains or NotExact) you + can set TreatMissingAsEmpty. IgnoreCase + has no effect for Regex. properties: contains: description: Contains specifies a substring @@ -783,6 +785,16 @@ spec: expression pattern that must match the header value. type: string + treatMissingAsEmpty: + description: TreatMissingAsEmpty specifies + if the header match rule specified + header does not exist, this header + value will be treated as empty. Defaults + to false. Unlike the underlying Envoy + implementation this is **only** supported + for negative matches (e.g. NotContains, + NotExact). + type: boolean required: - name type: object @@ -4121,8 +4133,10 @@ spec: HTTP headers. The Name field is required, only one of Present, NotPresent, Contains, NotContains, Exact, NotExact and Regex - can be set. IgnoreCase has no effect - for Regex. + can be set. For negative matching + rules only (e.g. NotContains or NotExact) + you can set TreatMissingAsEmpty. IgnoreCase + has no effect for Regex. properties: contains: description: Contains specifies @@ -4180,6 +4194,17 @@ spec: expression pattern that must match the header value. type: string + treatMissingAsEmpty: + description: TreatMissingAsEmpty + specifies if the header match + rule specified header does not + exist, this header value will + be treated as empty. Defaults + to false. Unlike the underlying + Envoy implementation this is **only** + supported for negative matches + (e.g. NotContains, NotExact). + type: boolean required: - name type: object @@ -4978,6 +5003,14 @@ spec: description: Regex specifies a regular expression pattern that must match the header value. type: string + treatMissingAsEmpty: + description: TreatMissingAsEmpty specifies if the + header match rule specified header does not exist, + this header value will be treated as empty. Defaults + to false. Unlike the underlying Envoy implementation + this is **only** supported for negative matches + (e.g. NotContains, NotExact). + type: boolean required: - name type: object @@ -5140,6 +5173,14 @@ spec: description: Regex specifies a regular expression pattern that must match the header value. type: string + treatMissingAsEmpty: + description: TreatMissingAsEmpty specifies if the + header match rule specified header does not exist, + this header value will be treated as empty. Defaults + to false. Unlike the underlying Envoy implementation + this is **only** supported for negative matches + (e.g. NotContains, NotExact). + type: boolean required: - name type: object @@ -5670,8 +5711,10 @@ spec: is required, only one of Present, NotPresent, Contains, NotContains, Exact, NotExact and Regex can be - set. IgnoreCase has no effect for - Regex. + set. For negative matching rules + only (e.g. NotContains or NotExact) + you can set TreatMissingAsEmpty. + IgnoreCase has no effect for Regex. properties: contains: description: Contains specifies @@ -5732,6 +5775,17 @@ spec: regular expression pattern that must match the header value. type: string + treatMissingAsEmpty: + description: TreatMissingAsEmpty + specifies if the header match + rule specified header does not + exist, this header value will + be treated as empty. Defaults + to false. Unlike the underlying + Envoy implementation this is + **only** supported for negative + matches (e.g. NotContains, NotExact). + type: boolean required: - name type: object @@ -7039,8 +7093,10 @@ spec: HTTP headers. The Name field is required, only one of Present, NotPresent, Contains, NotContains, Exact, NotExact and Regex - can be set. IgnoreCase has no effect - for Regex. + can be set. For negative matching + rules only (e.g. NotContains or NotExact) + you can set TreatMissingAsEmpty. IgnoreCase + has no effect for Regex. properties: contains: description: Contains specifies @@ -7098,6 +7154,17 @@ spec: expression pattern that must match the header value. type: string + treatMissingAsEmpty: + description: TreatMissingAsEmpty + specifies if the header match + rule specified header does not + exist, this header value will + be treated as empty. Defaults + to false. Unlike the underlying + Envoy implementation this is **only** + supported for negative matches + (e.g. NotContains, NotExact). + type: boolean required: - name type: object diff --git a/examples/render/contour-deployment.yaml b/examples/render/contour-deployment.yaml index 4697f85d47d..c488e79ade6 100644 --- a/examples/render/contour-deployment.yaml +++ b/examples/render/contour-deployment.yaml @@ -940,8 +940,10 @@ spec: headers. The Name field is required, only one of Present, NotPresent, Contains, NotContains, Exact, NotExact and Regex - can be set. IgnoreCase has no effect for - Regex. + can be set. For negative matching rules + only (e.g. NotContains or NotExact) you + can set TreatMissingAsEmpty. IgnoreCase + has no effect for Regex. properties: contains: description: Contains specifies a substring @@ -996,6 +998,16 @@ spec: expression pattern that must match the header value. type: string + treatMissingAsEmpty: + description: TreatMissingAsEmpty specifies + if the header match rule specified + header does not exist, this header + value will be treated as empty. Defaults + to false. Unlike the underlying Envoy + implementation this is **only** supported + for negative matches (e.g. NotContains, + NotExact). + type: boolean required: - name type: object @@ -4334,8 +4346,10 @@ spec: HTTP headers. The Name field is required, only one of Present, NotPresent, Contains, NotContains, Exact, NotExact and Regex - can be set. IgnoreCase has no effect - for Regex. + can be set. For negative matching + rules only (e.g. NotContains or NotExact) + you can set TreatMissingAsEmpty. IgnoreCase + has no effect for Regex. properties: contains: description: Contains specifies @@ -4393,6 +4407,17 @@ spec: expression pattern that must match the header value. type: string + treatMissingAsEmpty: + description: TreatMissingAsEmpty + specifies if the header match + rule specified header does not + exist, this header value will + be treated as empty. Defaults + to false. Unlike the underlying + Envoy implementation this is **only** + supported for negative matches + (e.g. NotContains, NotExact). + type: boolean required: - name type: object @@ -5191,6 +5216,14 @@ spec: description: Regex specifies a regular expression pattern that must match the header value. type: string + treatMissingAsEmpty: + description: TreatMissingAsEmpty specifies if the + header match rule specified header does not exist, + this header value will be treated as empty. Defaults + to false. Unlike the underlying Envoy implementation + this is **only** supported for negative matches + (e.g. NotContains, NotExact). + type: boolean required: - name type: object @@ -5353,6 +5386,14 @@ spec: description: Regex specifies a regular expression pattern that must match the header value. type: string + treatMissingAsEmpty: + description: TreatMissingAsEmpty specifies if the + header match rule specified header does not exist, + this header value will be treated as empty. Defaults + to false. Unlike the underlying Envoy implementation + this is **only** supported for negative matches + (e.g. NotContains, NotExact). + type: boolean required: - name type: object @@ -5883,8 +5924,10 @@ spec: is required, only one of Present, NotPresent, Contains, NotContains, Exact, NotExact and Regex can be - set. IgnoreCase has no effect for - Regex. + set. For negative matching rules + only (e.g. NotContains or NotExact) + you can set TreatMissingAsEmpty. + IgnoreCase has no effect for Regex. properties: contains: description: Contains specifies @@ -5945,6 +5988,17 @@ spec: regular expression pattern that must match the header value. type: string + treatMissingAsEmpty: + description: TreatMissingAsEmpty + specifies if the header match + rule specified header does not + exist, this header value will + be treated as empty. Defaults + to false. Unlike the underlying + Envoy implementation this is + **only** supported for negative + matches (e.g. NotContains, NotExact). + type: boolean required: - name type: object @@ -7252,8 +7306,10 @@ spec: HTTP headers. The Name field is required, only one of Present, NotPresent, Contains, NotContains, Exact, NotExact and Regex - can be set. IgnoreCase has no effect - for Regex. + can be set. For negative matching + rules only (e.g. NotContains or NotExact) + you can set TreatMissingAsEmpty. IgnoreCase + has no effect for Regex. properties: contains: description: Contains specifies @@ -7311,6 +7367,17 @@ spec: expression pattern that must match the header value. type: string + treatMissingAsEmpty: + description: TreatMissingAsEmpty + specifies if the header match + rule specified header does not + exist, this header value will + be treated as empty. Defaults + to false. Unlike the underlying + Envoy implementation this is **only** + supported for negative matches + (e.g. NotContains, NotExact). + type: boolean required: - name type: object diff --git a/examples/render/contour-gateway-provisioner.yaml b/examples/render/contour-gateway-provisioner.yaml index 6acc7139209..d2515bc2585 100644 --- a/examples/render/contour-gateway-provisioner.yaml +++ b/examples/render/contour-gateway-provisioner.yaml @@ -741,8 +741,10 @@ spec: headers. The Name field is required, only one of Present, NotPresent, Contains, NotContains, Exact, NotExact and Regex - can be set. IgnoreCase has no effect for - Regex. + can be set. For negative matching rules + only (e.g. NotContains or NotExact) you + can set TreatMissingAsEmpty. IgnoreCase + has no effect for Regex. properties: contains: description: Contains specifies a substring @@ -797,6 +799,16 @@ spec: expression pattern that must match the header value. type: string + treatMissingAsEmpty: + description: TreatMissingAsEmpty specifies + if the header match rule specified + header does not exist, this header + value will be treated as empty. Defaults + to false. Unlike the underlying Envoy + implementation this is **only** supported + for negative matches (e.g. NotContains, + NotExact). + type: boolean required: - name type: object @@ -4135,8 +4147,10 @@ spec: HTTP headers. The Name field is required, only one of Present, NotPresent, Contains, NotContains, Exact, NotExact and Regex - can be set. IgnoreCase has no effect - for Regex. + can be set. For negative matching + rules only (e.g. NotContains or NotExact) + you can set TreatMissingAsEmpty. IgnoreCase + has no effect for Regex. properties: contains: description: Contains specifies @@ -4194,6 +4208,17 @@ spec: expression pattern that must match the header value. type: string + treatMissingAsEmpty: + description: TreatMissingAsEmpty + specifies if the header match + rule specified header does not + exist, this header value will + be treated as empty. Defaults + to false. Unlike the underlying + Envoy implementation this is **only** + supported for negative matches + (e.g. NotContains, NotExact). + type: boolean required: - name type: object @@ -4992,6 +5017,14 @@ spec: description: Regex specifies a regular expression pattern that must match the header value. type: string + treatMissingAsEmpty: + description: TreatMissingAsEmpty specifies if the + header match rule specified header does not exist, + this header value will be treated as empty. Defaults + to false. Unlike the underlying Envoy implementation + this is **only** supported for negative matches + (e.g. NotContains, NotExact). + type: boolean required: - name type: object @@ -5154,6 +5187,14 @@ spec: description: Regex specifies a regular expression pattern that must match the header value. type: string + treatMissingAsEmpty: + description: TreatMissingAsEmpty specifies if the + header match rule specified header does not exist, + this header value will be treated as empty. Defaults + to false. Unlike the underlying Envoy implementation + this is **only** supported for negative matches + (e.g. NotContains, NotExact). + type: boolean required: - name type: object @@ -5684,8 +5725,10 @@ spec: is required, only one of Present, NotPresent, Contains, NotContains, Exact, NotExact and Regex can be - set. IgnoreCase has no effect for - Regex. + set. For negative matching rules + only (e.g. NotContains or NotExact) + you can set TreatMissingAsEmpty. + IgnoreCase has no effect for Regex. properties: contains: description: Contains specifies @@ -5746,6 +5789,17 @@ spec: regular expression pattern that must match the header value. type: string + treatMissingAsEmpty: + description: TreatMissingAsEmpty + specifies if the header match + rule specified header does not + exist, this header value will + be treated as empty. Defaults + to false. Unlike the underlying + Envoy implementation this is + **only** supported for negative + matches (e.g. NotContains, NotExact). + type: boolean required: - name type: object @@ -7053,8 +7107,10 @@ spec: HTTP headers. The Name field is required, only one of Present, NotPresent, Contains, NotContains, Exact, NotExact and Regex - can be set. IgnoreCase has no effect - for Regex. + can be set. For negative matching + rules only (e.g. NotContains or NotExact) + you can set TreatMissingAsEmpty. IgnoreCase + has no effect for Regex. properties: contains: description: Contains specifies @@ -7112,6 +7168,17 @@ spec: expression pattern that must match the header value. type: string + treatMissingAsEmpty: + description: TreatMissingAsEmpty + specifies if the header match + rule specified header does not + exist, this header value will + be treated as empty. Defaults + to false. Unlike the underlying + Envoy implementation this is **only** + supported for negative matches + (e.g. NotContains, NotExact). + type: boolean required: - name type: object diff --git a/examples/render/contour-gateway.yaml b/examples/render/contour-gateway.yaml index c1730d9cc16..29d943daef8 100644 --- a/examples/render/contour-gateway.yaml +++ b/examples/render/contour-gateway.yaml @@ -946,8 +946,10 @@ spec: headers. The Name field is required, only one of Present, NotPresent, Contains, NotContains, Exact, NotExact and Regex - can be set. IgnoreCase has no effect for - Regex. + can be set. For negative matching rules + only (e.g. NotContains or NotExact) you + can set TreatMissingAsEmpty. IgnoreCase + has no effect for Regex. properties: contains: description: Contains specifies a substring @@ -1002,6 +1004,16 @@ spec: expression pattern that must match the header value. type: string + treatMissingAsEmpty: + description: TreatMissingAsEmpty specifies + if the header match rule specified + header does not exist, this header + value will be treated as empty. Defaults + to false. Unlike the underlying Envoy + implementation this is **only** supported + for negative matches (e.g. NotContains, + NotExact). + type: boolean required: - name type: object @@ -4340,8 +4352,10 @@ spec: HTTP headers. The Name field is required, only one of Present, NotPresent, Contains, NotContains, Exact, NotExact and Regex - can be set. IgnoreCase has no effect - for Regex. + can be set. For negative matching + rules only (e.g. NotContains or NotExact) + you can set TreatMissingAsEmpty. IgnoreCase + has no effect for Regex. properties: contains: description: Contains specifies @@ -4399,6 +4413,17 @@ spec: expression pattern that must match the header value. type: string + treatMissingAsEmpty: + description: TreatMissingAsEmpty + specifies if the header match + rule specified header does not + exist, this header value will + be treated as empty. Defaults + to false. Unlike the underlying + Envoy implementation this is **only** + supported for negative matches + (e.g. NotContains, NotExact). + type: boolean required: - name type: object @@ -5197,6 +5222,14 @@ spec: description: Regex specifies a regular expression pattern that must match the header value. type: string + treatMissingAsEmpty: + description: TreatMissingAsEmpty specifies if the + header match rule specified header does not exist, + this header value will be treated as empty. Defaults + to false. Unlike the underlying Envoy implementation + this is **only** supported for negative matches + (e.g. NotContains, NotExact). + type: boolean required: - name type: object @@ -5359,6 +5392,14 @@ spec: description: Regex specifies a regular expression pattern that must match the header value. type: string + treatMissingAsEmpty: + description: TreatMissingAsEmpty specifies if the + header match rule specified header does not exist, + this header value will be treated as empty. Defaults + to false. Unlike the underlying Envoy implementation + this is **only** supported for negative matches + (e.g. NotContains, NotExact). + type: boolean required: - name type: object @@ -5889,8 +5930,10 @@ spec: is required, only one of Present, NotPresent, Contains, NotContains, Exact, NotExact and Regex can be - set. IgnoreCase has no effect for - Regex. + set. For negative matching rules + only (e.g. NotContains or NotExact) + you can set TreatMissingAsEmpty. + IgnoreCase has no effect for Regex. properties: contains: description: Contains specifies @@ -5951,6 +5994,17 @@ spec: regular expression pattern that must match the header value. type: string + treatMissingAsEmpty: + description: TreatMissingAsEmpty + specifies if the header match + rule specified header does not + exist, this header value will + be treated as empty. Defaults + to false. Unlike the underlying + Envoy implementation this is + **only** supported for negative + matches (e.g. NotContains, NotExact). + type: boolean required: - name type: object @@ -7258,8 +7312,10 @@ spec: HTTP headers. The Name field is required, only one of Present, NotPresent, Contains, NotContains, Exact, NotExact and Regex - can be set. IgnoreCase has no effect - for Regex. + can be set. For negative matching + rules only (e.g. NotContains or NotExact) + you can set TreatMissingAsEmpty. IgnoreCase + has no effect for Regex. properties: contains: description: Contains specifies @@ -7317,6 +7373,17 @@ spec: expression pattern that must match the header value. type: string + treatMissingAsEmpty: + description: TreatMissingAsEmpty + specifies if the header match + rule specified header does not + exist, this header value will + be treated as empty. Defaults + to false. Unlike the underlying + Envoy implementation this is **only** + supported for negative matches + (e.g. NotContains, NotExact). + type: boolean required: - name type: object diff --git a/examples/render/contour.yaml b/examples/render/contour.yaml index 907bb6a57f8..5745b5e9c95 100644 --- a/examples/render/contour.yaml +++ b/examples/render/contour.yaml @@ -940,8 +940,10 @@ spec: headers. The Name field is required, only one of Present, NotPresent, Contains, NotContains, Exact, NotExact and Regex - can be set. IgnoreCase has no effect for - Regex. + can be set. For negative matching rules + only (e.g. NotContains or NotExact) you + can set TreatMissingAsEmpty. IgnoreCase + has no effect for Regex. properties: contains: description: Contains specifies a substring @@ -996,6 +998,16 @@ spec: expression pattern that must match the header value. type: string + treatMissingAsEmpty: + description: TreatMissingAsEmpty specifies + if the header match rule specified + header does not exist, this header + value will be treated as empty. Defaults + to false. Unlike the underlying Envoy + implementation this is **only** supported + for negative matches (e.g. NotContains, + NotExact). + type: boolean required: - name type: object @@ -4334,8 +4346,10 @@ spec: HTTP headers. The Name field is required, only one of Present, NotPresent, Contains, NotContains, Exact, NotExact and Regex - can be set. IgnoreCase has no effect - for Regex. + can be set. For negative matching + rules only (e.g. NotContains or NotExact) + you can set TreatMissingAsEmpty. IgnoreCase + has no effect for Regex. properties: contains: description: Contains specifies @@ -4393,6 +4407,17 @@ spec: expression pattern that must match the header value. type: string + treatMissingAsEmpty: + description: TreatMissingAsEmpty + specifies if the header match + rule specified header does not + exist, this header value will + be treated as empty. Defaults + to false. Unlike the underlying + Envoy implementation this is **only** + supported for negative matches + (e.g. NotContains, NotExact). + type: boolean required: - name type: object @@ -5191,6 +5216,14 @@ spec: description: Regex specifies a regular expression pattern that must match the header value. type: string + treatMissingAsEmpty: + description: TreatMissingAsEmpty specifies if the + header match rule specified header does not exist, + this header value will be treated as empty. Defaults + to false. Unlike the underlying Envoy implementation + this is **only** supported for negative matches + (e.g. NotContains, NotExact). + type: boolean required: - name type: object @@ -5353,6 +5386,14 @@ spec: description: Regex specifies a regular expression pattern that must match the header value. type: string + treatMissingAsEmpty: + description: TreatMissingAsEmpty specifies if the + header match rule specified header does not exist, + this header value will be treated as empty. Defaults + to false. Unlike the underlying Envoy implementation + this is **only** supported for negative matches + (e.g. NotContains, NotExact). + type: boolean required: - name type: object @@ -5883,8 +5924,10 @@ spec: is required, only one of Present, NotPresent, Contains, NotContains, Exact, NotExact and Regex can be - set. IgnoreCase has no effect for - Regex. + set. For negative matching rules + only (e.g. NotContains or NotExact) + you can set TreatMissingAsEmpty. + IgnoreCase has no effect for Regex. properties: contains: description: Contains specifies @@ -5945,6 +5988,17 @@ spec: regular expression pattern that must match the header value. type: string + treatMissingAsEmpty: + description: TreatMissingAsEmpty + specifies if the header match + rule specified header does not + exist, this header value will + be treated as empty. Defaults + to false. Unlike the underlying + Envoy implementation this is + **only** supported for negative + matches (e.g. NotContains, NotExact). + type: boolean required: - name type: object @@ -7252,8 +7306,10 @@ spec: HTTP headers. The Name field is required, only one of Present, NotPresent, Contains, NotContains, Exact, NotExact and Regex - can be set. IgnoreCase has no effect - for Regex. + can be set. For negative matching + rules only (e.g. NotContains or NotExact) + you can set TreatMissingAsEmpty. IgnoreCase + has no effect for Regex. properties: contains: description: Contains specifies @@ -7311,6 +7367,17 @@ spec: expression pattern that must match the header value. type: string + treatMissingAsEmpty: + description: TreatMissingAsEmpty + specifies if the header match + rule specified header does not + exist, this header value will + be treated as empty. Defaults + to false. Unlike the underlying + Envoy implementation this is **only** + supported for negative matches + (e.g. NotContains, NotExact). + type: boolean required: - name type: object diff --git a/internal/dag/conditions.go b/internal/dag/conditions.go index f51aa7ef029..19f5c62d718 100644 --- a/internal/dag/conditions.go +++ b/internal/dag/conditions.go @@ -181,11 +181,12 @@ func headerMatchConditions(conditions []contour_api_v1.HeaderMatchCondition) []H }) case cond.NotContains != "": hc = append(hc, HeaderMatchCondition{ - Name: cond.Name, - Value: cond.NotContains, - MatchType: HeaderMatchTypeContains, - Invert: true, - IgnoreCase: cond.IgnoreCase, + Name: cond.Name, + Value: cond.NotContains, + MatchType: HeaderMatchTypeContains, + Invert: true, + IgnoreCase: cond.IgnoreCase, + TreatMissingAsEmpty: cond.TreatMissingAsEmpty, }) case cond.Exact != "": hc = append(hc, HeaderMatchCondition{ @@ -196,11 +197,12 @@ func headerMatchConditions(conditions []contour_api_v1.HeaderMatchCondition) []H }) case cond.NotExact != "": hc = append(hc, HeaderMatchCondition{ - Name: cond.Name, - Value: cond.NotExact, - MatchType: HeaderMatchTypeExact, - Invert: true, - IgnoreCase: cond.IgnoreCase, + Name: cond.Name, + Value: cond.NotExact, + MatchType: HeaderMatchTypeExact, + Invert: true, + IgnoreCase: cond.IgnoreCase, + TreatMissingAsEmpty: cond.TreatMissingAsEmpty, }) case cond.Regex != "": hc = append(hc, HeaderMatchCondition{ diff --git a/internal/dag/dag.go b/internal/dag/dag.go index 30edfbc5bca..de934223818 100644 --- a/internal/dag/dag.go +++ b/internal/dag/dag.go @@ -140,11 +140,12 @@ const ( // HeaderMatchCondition matches request headers by MatchType type HeaderMatchCondition struct { - Name string - Value string - MatchType string - Invert bool - IgnoreCase bool + Name string + Value string + MatchType string + Invert bool + IgnoreCase bool + TreatMissingAsEmpty bool } func (hc *HeaderMatchCondition) String() string { @@ -152,6 +153,7 @@ func (hc *HeaderMatchCondition) String() string { "name=" + hc.Name, "value=" + hc.Value, "matchtype=", hc.MatchType, + "TreatMissingAsEmpty=", strconv.FormatBool(hc.TreatMissingAsEmpty), "invert=", strconv.FormatBool(hc.Invert), "ignorecase=", strconv.FormatBool(hc.IgnoreCase), }, "&") diff --git a/internal/dag/httpproxy_processor.go b/internal/dag/httpproxy_processor.go index fd7124df3e9..6b5caea4807 100644 --- a/internal/dag/httpproxy_processor.go +++ b/internal/dag/httpproxy_processor.go @@ -1722,6 +1722,9 @@ func includeMatchConditionsIdentical(includeConds []contour_api_v1.MatchConditio if includeHeaderConds[i].Invert != includeHeaderConds[j].Invert { return includeHeaderConds[i].Invert } + if includeHeaderConds[i].TreatMissingAsEmpty != includeHeaderConds[j].TreatMissingAsEmpty { + return includeHeaderConds[i].TreatMissingAsEmpty + } if includeHeaderConds[i].Name != includeHeaderConds[j].Name { return includeHeaderConds[i].Name < includeHeaderConds[j].Name } diff --git a/internal/envoy/v3/route.go b/internal/envoy/v3/route.go index 3d9f06f59ce..205d7c54ee5 100644 --- a/internal/envoy/v3/route.go +++ b/internal/envoy/v3/route.go @@ -736,6 +736,8 @@ func headerMatcher(headers []dag.HeaderMatchCondition) []*envoy_route_v3.HeaderM header := &envoy_route_v3.HeaderMatcher{ Name: h.Name, InvertMatch: h.Invert, + // We only want to turn on TreatMissingHeaderAsEmpty on invert matches + TreatMissingHeaderAsEmpty: h.Invert && h.TreatMissingAsEmpty, } switch h.MatchType { diff --git a/internal/envoy/v3/route_test.go b/internal/envoy/v3/route_test.go index 212456ccb9e..102924d7654 100644 --- a/internal/envoy/v3/route_test.go +++ b/internal/envoy/v3/route_test.go @@ -1926,6 +1926,31 @@ func TestRouteMatch(t *testing.T) { }}, }, }, + "notcontains match -- treat missing as empty": { + route: &dag.Route{ + HeaderMatchConditions: []dag.HeaderMatchCondition{{ + Name: "x-header", + Value: "foo", + MatchType: "contains", + Invert: true, + TreatMissingAsEmpty: true, + }}, + }, + want: &envoy_route_v3.RouteMatch{ + Headers: []*envoy_route_v3.HeaderMatcher{{ + Name: "x-header", + InvertMatch: true, + TreatMissingHeaderAsEmpty: true, + HeaderMatchSpecifier: &envoy_route_v3.HeaderMatcher_StringMatch{ + StringMatch: &matcher.StringMatcher{ + MatchPattern: &matcher.StringMatcher_Contains{ + Contains: "foo", + }, + }, + }, + }}, + }, + }, "path prefix string prefix": { route: &dag.Route{ PathMatchCondition: &dag.PrefixMatchCondition{ @@ -2127,6 +2152,29 @@ func TestRouteMatch(t *testing.T) { }}, }, }, + "header not exact -- treat missing as empty": { + route: &dag.Route{ + HeaderMatchConditions: []dag.HeaderMatchCondition{{ + Name: "x-header-foo", + MatchType: dag.HeaderMatchTypeExact, + Value: "bar", + Invert: true, + TreatMissingAsEmpty: true, + }}, + }, + want: &envoy_route_v3.RouteMatch{ + Headers: []*envoy_route_v3.HeaderMatcher{{ + Name: "x-header-foo", + InvertMatch: true, + TreatMissingHeaderAsEmpty: true, + HeaderMatchSpecifier: &envoy_route_v3.HeaderMatcher_StringMatch{ + StringMatch: &matcher.StringMatcher{ + MatchPattern: &matcher.StringMatcher_Exact{Exact: "bar"}, + }, + }, + }}, + }, + }, "header contains": { route: &dag.Route{ HeaderMatchConditions: []dag.HeaderMatchCondition{{ diff --git a/internal/featuretests/v3/headercondition_test.go b/internal/featuretests/v3/headercondition_test.go index dc35f691ed9..13ec7289152 100644 --- a/internal/featuretests/v3/headercondition_test.go +++ b/internal/featuretests/v3/headercondition_test.go @@ -55,25 +55,48 @@ func TestConditions_ContainsHeader_HTTProxy(t *testing.T) { Name: "svc1", Port: 80, }}, - }, { - Conditions: matchconditions( - prefixMatchCondition("/"), - headerContainsMatchCondition("x-header", "abc", false), - ), - Services: []contour_api_v1.Service{{ - Name: "svc2", - Port: 80, - }}, - }, { - Conditions: matchconditions( - prefixMatchCondition("/blog"), - headerContainsMatchCondition("x-header", "abc", false), - ), - Services: []contour_api_v1.Service{{ - Name: "svc3", - Port: 80, - }}, - }}, + }, + { + Conditions: matchconditions( + prefixMatchCondition("/"), + headerContainsMatchCondition("x-header", "abc", false), + ), + Services: []contour_api_v1.Service{{ + Name: "svc2", + Port: 80, + }}, + }, + { + Conditions: matchconditions( + prefixMatchCondition("/blog"), + headerContainsMatchCondition("x-header", "abc", false), + ), + Services: []contour_api_v1.Service{{ + Name: "svc3", + Port: 80, + }}, + }, + { + Conditions: matchconditions( + prefixMatchCondition("/blog"), + headerNotExactMatchCondition("x-beta-release", "true", false, true), + ), + Services: []contour_api_v1.Service{{ + Name: "svc2", + Port: 80, + }}, + }, + { + Conditions: matchconditions( + prefixMatchCondition("/blog"), + headerNotContainsMatchCondition("x-beta-release", "t", false, true), + ), + Services: []contour_api_v1.Service{{ + Name: "svc2", + Port: 80, + }}, + }, + }, }, } rh.OnAdd(proxy1) @@ -82,6 +105,26 @@ func TestConditions_ContainsHeader_HTTProxy(t *testing.T) { Resources: resources(t, envoy_v3.RouteConfiguration("ingress_http", envoy_v3.VirtualHost("hello.world", + &envoy_route_v3.Route{ + Match: routePrefixWithHeaderConditions("/blog", dag.HeaderMatchCondition{ + Name: "x-beta-release", + Value: "true", + MatchType: "exact", + Invert: true, + TreatMissingAsEmpty: true, + }), + Action: routeCluster("default/svc2/80/da39a3ee5e"), + }, + &envoy_route_v3.Route{ + Match: routePrefixWithHeaderConditions("/blog", dag.HeaderMatchCondition{ + Name: "x-beta-release", + Value: "t", + MatchType: "contains", + Invert: true, + TreatMissingAsEmpty: true, + }), + Action: routeCluster("default/svc2/80/da39a3ee5e"), + }, &envoy_route_v3.Route{ Match: routePrefixWithHeaderConditions("/blog", dag.HeaderMatchCondition{ Name: "x-header", @@ -121,7 +164,7 @@ func TestConditions_ContainsHeader_HTTProxy(t *testing.T) { }, { Conditions: matchconditions( prefixMatchCondition("/"), - headerNotContainsMatchCondition("x-header", "123", false), + headerNotContainsMatchCondition("x-header", "123", false, false), ), Services: []contour_api_v1.Service{{ Name: "svc2", @@ -130,7 +173,7 @@ func TestConditions_ContainsHeader_HTTProxy(t *testing.T) { }, { Conditions: matchconditions( prefixMatchCondition("/blog"), - headerNotContainsMatchCondition("x-header", "abc", false), + headerNotContainsMatchCondition("x-header", "abc", false, false), ), Services: []contour_api_v1.Service{{ Name: "svc3", @@ -247,7 +290,7 @@ func TestConditions_ContainsHeader_HTTProxy(t *testing.T) { }, { Conditions: matchconditions( prefixMatchCondition("/"), - headerNotExactMatchCondition("x-header", "abc", false), + headerNotExactMatchCondition("x-header", "abc", false, false), ), Services: []contour_api_v1.Service{{ Name: "svc2", @@ -256,7 +299,7 @@ func TestConditions_ContainsHeader_HTTProxy(t *testing.T) { }, { Conditions: matchconditions( prefixMatchCondition("/blog"), - headerNotExactMatchCondition("x-header", "123", false), + headerNotExactMatchCondition("x-header", "123", false, false), ), Services: []contour_api_v1.Service{{ Name: "svc3", @@ -438,7 +481,7 @@ func TestConditions_ContainsHeader_HTTProxy(t *testing.T) { { Conditions: matchconditions( prefixMatchCondition("/"), - headerNotContainsMatchCondition("x-header", "abc", false), + headerNotContainsMatchCondition("x-header", "abc", false, false), ), Services: []contour_api_v1.Service{{ Name: "svc2", diff --git a/internal/featuretests/v3/httpproxy.go b/internal/featuretests/v3/httpproxy.go index 0796262ec39..d46ed1a1ba9 100644 --- a/internal/featuretests/v3/httpproxy.go +++ b/internal/featuretests/v3/httpproxy.go @@ -39,12 +39,13 @@ func headerContainsMatchCondition(name, value string, ignoreCase bool) contour_a } } -func headerNotContainsMatchCondition(name, value string, ignoreCase bool) contour_api_v1.MatchCondition { +func headerNotContainsMatchCondition(name, value string, ignoreCase, treatMissingAsEmpty bool) contour_api_v1.MatchCondition { return contour_api_v1.MatchCondition{ Header: &contour_api_v1.HeaderMatchCondition{ - Name: name, - NotContains: value, - IgnoreCase: ignoreCase, + Name: name, + NotContains: value, + IgnoreCase: ignoreCase, + TreatMissingAsEmpty: treatMissingAsEmpty, }, } } @@ -59,12 +60,13 @@ func headerExactMatchCondition(name, value string, ignoreCase bool) contour_api_ } } -func headerNotExactMatchCondition(name, value string, ignoreCase bool) contour_api_v1.MatchCondition { +func headerNotExactMatchCondition(name, value string, ignoreCase bool, treatMissingAsEmpty bool) contour_api_v1.MatchCondition { return contour_api_v1.MatchCondition{ Header: &contour_api_v1.HeaderMatchCondition{ - Name: name, - NotExact: value, - IgnoreCase: ignoreCase, + Name: name, + NotExact: value, + IgnoreCase: ignoreCase, + TreatMissingAsEmpty: treatMissingAsEmpty, }, } } diff --git a/site/content/docs/main/config/api-reference.html b/site/content/docs/main/config/api-reference.html index b7b58ff6866..fed7ba6810e 100644 --- a/site/content/docs/main/config/api-reference.html +++ b/site/content/docs/main/config/api-reference.html @@ -1887,6 +1887,8 @@

HeaderMatchCondition

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

@@ -2024,6 +2026,22 @@

HeaderMatchCondition value.

+

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

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

+

HeaderValue diff --git a/site/content/docs/main/config/request-routing.md b/site/content/docs/main/config/request-routing.md index 4e15f825bb1..19ef5386e86 100644 --- a/site/content/docs/main/config/request-routing.md +++ b/site/content/docs/main/config/request-routing.md @@ -105,8 +105,13 @@ Regex conditions **must** start with a `/` if they are present. #### Header conditions -For `header` conditions there is one required field, `name`, six operator fields: `present`, `notpresent`, `contains`, `notcontains`, `exact`, and `notexact` and a modifier `ignoreCase` which can be used together with all of the operator fields except `regex`, `notpresent` and `present`. +For `header` conditions there is the following structure: +1. one required field, `name` +2. six operator fields: `present`, `notpresent`, `contains`, `notcontains`, `exact`, and `notexact` +3. two optional modifiers: `ignoreCase` and `treatMissingAsEmpty` + +Operators: - `present` is a boolean and checks that the header is present. The value will not be checked. - `notpresent` similarly checks that the header is *not* present. @@ -117,6 +122,10 @@ For `header` conditions there is one required field, `name`, six operator fields - `regex` is a string representing a regular expression, and checks that the header value matches against the given regular expression. +Modifiers: +- `ignoreCase`: IgnoreCase specifies that string matching should be case insensitive. It has no effect on the `Regex` parameter. +- `treatMissingAsEmpty`: specifies if the header match rule specified header does not exist, this header value will be treated as empty. Defaults to false. Unlike the underlying Envoy implementation this is **only** supported for negative matches (e.g. NotContains, NotExact). + #### Query parameter conditions Similar to the `header` conditions, `queryParameter` conditions also require the diff --git a/test/e2e/httpproxy/header_condition_match_test.go b/test/e2e/httpproxy/header_condition_match_test.go index 7906cfc0742..ca51d8ae892 100644 --- a/test/e2e/httpproxy/header_condition_match_test.go +++ b/test/e2e/httpproxy/header_condition_match_test.go @@ -38,9 +38,11 @@ func testHeaderConditionMatch(namespace string) { f.Fixtures.Echo.Deploy(namespace, "echo-header-contains") f.Fixtures.Echo.Deploy(namespace, "echo-header-contains-case-insensitive") f.Fixtures.Echo.Deploy(namespace, "echo-header-notcontains") + f.Fixtures.Echo.Deploy(namespace, "echo-header-notcontains-set-missing-as-empty") f.Fixtures.Echo.Deploy(namespace, "echo-header-exact") f.Fixtures.Echo.Deploy(namespace, "echo-header-exact-case-insensitive") f.Fixtures.Echo.Deploy(namespace, "echo-header-notexact") + f.Fixtures.Echo.Deploy(namespace, "echo-header-notexact-set-missing-as-empty") f.Fixtures.Echo.Deploy(namespace, "echo-header-regex") // This HTTPProxy tests everything except the "notpresent" match type, @@ -120,6 +122,32 @@ func testHeaderConditionMatch(namespace string) { }, }, }, + { + Services: []contourv1.Service{ + { + Name: "echo-header-notcontains-set-missing-as-empty", + Port: 80, + }, + }, + Conditions: []contourv1.MatchCondition{ + { + Header: &contourv1.HeaderMatchCondition{ + Name: "Target-NotContains", + NotContains: "ContainsValue", + TreatMissingAsEmpty: true, + }, + }, + // We use this case to and the two conditions otherwise the not + // contains statement would match anything and make the tests + // brittle. + { + Header: &contourv1.HeaderMatchCondition{ + Name: "X-Force-NotContains-Case", + Exact: "True", + }, + }, + }, + }, { Services: []contourv1.Service{ { @@ -170,6 +198,32 @@ func testHeaderConditionMatch(namespace string) { }, }, }, + { + Services: []contourv1.Service{ + { + Name: "echo-header-notexact-set-missing-as-empty", + Port: 80, + }, + }, + Conditions: []contourv1.MatchCondition{ + { + Header: &contourv1.HeaderMatchCondition{ + Name: "Target-NotExact", + NotContains: "ExactValue", + TreatMissingAsEmpty: true, + }, + }, + // We use this case to and the two conditions otherwise the not + // contains statement would match anything and make the tests + // brittle. + { + Header: &contourv1.HeaderMatchCondition{ + Name: "X-Force-NotExact-Case", + Exact: "True", + }, + }, + }, + }, { Services: []contourv1.Service{ { @@ -226,6 +280,11 @@ func testHeaderConditionMatch(namespace string) { headers: map[string]string{"Target-NotContains": "ContainsValue"}, expectResponse: 404, }, + { + headers: map[string]string{"X-Force-NotContains-Case": "True"}, + expectResponse: 200, + expectService: "echo-header-notcontains-set-missing-as-empty", + }, { headers: map[string]string{"Target-NotContains": "xxx ContainsValue xxx"}, expectResponse: 404, @@ -267,6 +326,11 @@ func testHeaderConditionMatch(namespace string) { headers: map[string]string{"Target-NotExact": "ExactValue"}, expectResponse: 404, }, + { + headers: map[string]string{"X-Force-NotExact-Case": "True"}, + expectResponse: 200, + expectService: "echo-header-notexact-set-missing-as-empty", + }, { headers: map[string]string{"Target-Regex": "RegexMatch"}, expectResponse: 200,