Skip to content

feat(api-gateway): add header match invert filter - #5593

Open
bharath-k1999 wants to merge 5 commits into
mainfrom
bharath/header-routing-invert
Open

feat(api-gateway): add header match invert filter#5593
bharath-k1999 wants to merge 5 commits into
mainfrom
bharath/header-routing-invert

Conversation

@bharath-k1999

@bharath-k1999 bharath-k1999 commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds Invert (negation) support to API Gateway HTTP header matching, bringing it to parity
with Ingress Gateway which already supports Invert bool on ServiceRouteHTTPMatchHeader.

Because the native Kubernetes Gateway API HTTPHeaderMatch struct has no invert/negate field,
this is implemented via a new Consul-specific CRD — RouteHeaderMatchInvertFilter — referenced
as an ExtensionRef filter on an HTTPRoute rule. The CRD lists the header names whose match
condition should be negated when the rule is translated into a Consul http-route config entry.

Motivating patterns

Pattern Expression
Route when header is absent Present = true, Invert = true
Route when header value is not "v2" Exact = "v2", Invert = true
Default-to-new-version routing (canary) x-canary: "true", Invert = true → send to new version when canary header absent
Legacy client filtering x-legacy-client: present, Invert = true → serve new API to non-legacy clients

Changes

consul core — api/config_entry_routes.go + agent/structs/config_entry_routes.go

  • Added Invert bool to HTTPHeaderMatch in both the HTTP API type and the internal struct.

consul core — proto/private/pbconfigentry/

  • Added bool Invert = 4 to the HTTPHeaderMatch protobuf message.
  • Updated config_entry.gen.go mog converters and config_entry.pb.go generated file
    (rawDesc string, field descriptor, GetInvert() accessor).

consul core — agent/consul/discoverychain/gateway_httproute.go

  • Propagated Invert in all five HTTPRouteMatchToServiceRouteHTTPMatch match-type cases
    (exact, prefix, suffix, regex, present).

consul-k8s — control-plane/api/v1alpha1/routeheadermatchinvertfilter_types.go (new)

  • New CRD RouteHeaderMatchInvertFilter with spec.headerNames []string.
  • Registered in the scheme via init().

consul-k8s — control-plane/api/v1alpha1/zz_generated.deepcopy.go

  • DeepCopy methods for RouteHeaderMatchInvertFilter, RouteHeaderMatchInvertFilterList,
    RouteHeaderMatchInvertFilterSpec, RouteHeaderMatchInvertFilterStatus.

consul-k8s — control-plane/api-gateway/controllers/index.go

  • Added HTTPRoute_RouteHeaderMatchInvertFilterIndex constant and index registration entry.

consul-k8s — control-plane/api-gateway/controllers/gateway_controller.go

  • Added Watches entry for RouteHeaderMatchInvertFilter objects.
  • Added transformRouteHeaderMatchInvertFilter enqueue function.
  • Added RouteHeaderMatchInvertFilterKind case in the external filter resolver switch.

consul-k8s — control-plane/api-gateway/common/translation.go

  • translateHTTPHeaderMatch now delegates to translateHTTPHeaderMatchWithInvert.
  • translateHTTPHeaderMatchWithInvert sets Invert = true when the header name (lowercased)
    appears in an invertedHeaders set.
  • invertedHeaderNamesForRule scans a rule's filters for a RouteHeaderMatchInvertFilter
    ExtensionRef and returns the lowercase set of header names.
  • translateHTTPRouteRule calls invertedHeaderNamesForRule and passes the set through
    when translating each header match.

consul-k8s — control-plane/api-gateway/common/diff.go

  • Bug fix: httpHeaderMatchesEqual was missing Invert from its comparison — without this,
    day-2 updates to a RouteHeaderMatchInvertFilter would not trigger a Consul config entry write.

Tests added

File Coverage
control-plane/api-gateway/common/header_match_invert_diff_test.go 12 subtests for the diff comparator
control-plane/api-gateway/common/header_match_invert_translation_test.go 25 subtests: all 5 match types × invert on/off, multi-header, day-2 lifecycle
acceptance/tests/api-gateway/api_gateway_header_match_invert_test.go TestAPIGateway_HeaderMatchInvert_Lifecycle — 3-phase lifecycle acceptance test
acceptance/tests/fixtures/cases/api-gateways/header-match-invert/ Fixture YAML (kustomization, gcc, class, gateway, filters, httproute)

Local Testing

Unit tests

# consul core
cd hashicorp/consul
go test ./agent/structs/... -run TestHTTPHeaderMatch -v
go test ./agent/consul/discoverychain/... -run TestHTTPRouteMatchToServiceRouteHTTPMatch_Invert -v
go test ./agent/xds/... -run TestAPIGatewayHeaderMatchInvert -v

# consul-k8s control-plane
cd hashicorp/consul-k8s/control-plane
go test ./api-gateway/common/... -v -run "TestEntryComparator_HTTPHeaderMatchesEqual_Invert|TestInvertedHeaderNamesForRule|TestTranslateHTTPHeaderMatchWithInvert|TestHTTPRouteWithInvertFilter_Day2Lifecycle"

Expected output (all pass):

--- PASS: TestEntryComparator_HTTPHeaderMatchesEqual_Invert/same_name_value_invert_true_are_equal
--- PASS: TestEntryComparator_HTTPHeaderMatchesEqual_Invert/invert_true_vs_false_are_NOT_equal
--- PASS: TestEntryComparator_HTTPHeaderMatchesEqual_Invert/value_change_is_still_detected_independently_of_invert
--- PASS: TestEntryComparator_HTTPHeaderMatchesEqual_Invert/match_type_change_is_still_detected_independently_of_invert
--- PASS: TestInvertedHeaderNamesForRule/single_header_name_returned_lowercased
--- PASS: TestInvertedHeaderNamesForRule/multiple_header_names_are_all_lowercased
--- PASS: TestInvertedHeaderNamesForRule/no_matching_filter_returns_nil
--- PASS: TestTranslateHTTPHeaderMatchWithInvert/header_name_NOT_in_set_→_Invert_false
--- PASS: TestTranslateHTTPHeaderMatchWithInvert/all_five_match_types_propagate_correctly
--- PASS: TestHTTPRouteWithInvertFilter_Day2Lifecycle/...

Manual end-to-end (plain HTTP gateway, no cluster required)

1. Apply the RouteHeaderMatchInvertFilter CRD

Apply the new CRD to your cluster:

cd hashicorp/consul-k8s
kubectl apply -f control-plane/config/crd/bases/consul.hashicorp.com_routeheadermatchinvertfilters.yaml

2. Deploy all fixture resources

kubectl apply -k acceptance/tests/fixtures/cases/api-gateways/header-match-invert/

This deploys in one shot:

  • static-server backend (connect-injected)
  • GatewayClassConfigGatewayClass (header-invert-class)
  • Gateway (header-invert-gw) — plain HTTP on port 8080
  • RouteHeaderMatchInvertFilter × 2 (invert-filter-v1, invert-filter-v2)
  • HTTPRoute (invert-route) — Day-1 state with ExtensionRef → invert-filter-v1

3. Verify the Consul config entry (Day 1)

consul config read -kind http-route -name invert-route

Expected — Rules[0].Matches[0].Headers[0] should show:

{
  "Name":   "x-canary",
  "Value":  "true",
  "Match":  "exact",
  "Invert": true
}

4. Simulate Day 2 — swap filter to invert-filter-v2

kubectl patch httproute invert-route --type=json -p='[
  {"op":"replace","path":"/spec/rules/0/matches/0/headers/0/name","value":"x-version"},
  {"op":"replace","path":"/spec/rules/0/filters/0/extensionRef/name","value":"invert-filter-v2"}
]'

Re-read the Consul config entry and verify:

consul config read -kind http-route -name invert-route

Expected:

{
  "Name":   "x-version",
  "Value":  "true",
  "Match":  "exact",
  "Invert": true
}

5. Simulate Day 3 — remove the ExtensionRef entirely

kubectl patch httproute invert-route --type=json -p='[
  {"op":"remove","path":"/spec/rules/0/filters"}
]'

Expected — Invert reverts to false:

{
  "Name":   "x-version",
  "Value":  "true",
  "Match":  "exact",
  "Invert": false
}

6. Verify traffic routing with curl

Assuming the gateway is port-forwarded to localhost:8080:

# Should reach static-server (x-canary absent → Invert match fires)
curl -s http://localhost:8080/

# Should NOT match (x-canary present → Invert suppresses the rule → falls to default)
curl -s -H "x-canary: true" http://localhost:8080/

Run the acceptance test

The acceptance test requires a real Kubernetes cluster with Consul deployed via Helm.

cd hashicorp/consul-k8s/acceptance

go test ./tests/api-gateway/... \
  -run TestAPIGateway_HeaderMatchInvert_Lifecycle \
  -v \
  -timeout 20m \
  -p 1 \
  2>&1 | tee /tmp/header-invert-test.log

The test exercises the full 3-phase lifecycle:

Phase What happens Consul assertion
Day 1 Create HTTPRoute + invert-filter-v1 (x-canary) Headers[0].Name == "x-canary", Invert == true
Day 2 Swap ExtensionRef to invert-filter-v2 (x-version) Headers[0].Name == "x-version", Invert == true
Day 3 Remove ExtensionRef entirely Invert == false
Teardown Delete Gateway api-gateway + http-route config entries absent from Consul

@bharath-k1999
bharath-k1999 requested review from a team as code owners August 14, 2026 03:30
@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown

Go Test Coverage: 61.4%

Patch coverage: 30.4% (38/125 changed lines covered)

See the workflow run for the full per-package breakdown and downloadable HTML report.

@codecov-commenter

codecov-commenter commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 30.18868% with 74 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.54%. Comparing base (cb007b6) to head (637adc1).
⚠️ Report is 12 commits behind head on main.

Files with missing lines Patch % Lines
...ontrol-plane/api/v1alpha1/zz_generated.deepcopy.go 0.00% 60 Missing ⚠️
...lane/api-gateway/controllers/gateway_controller.go 0.00% 8 Missing ⚠️
control-plane/api-gateway/common/translation.go 87.87% 3 Missing and 1 partial ⚠️
...api/v1alpha1/routeheadermatchinvertfilter_types.go 50.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5593      +/-   ##
==========================================
- Coverage   58.62%   58.54%   -0.09%     
==========================================
  Files         298      299       +1     
  Lines       36767    36867     +100     
==========================================
+ Hits        21554    21583      +29     
- Misses      13268    13341      +73     
+ Partials     1945     1943       -2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants