Skip to content

fix(controller): normalize vhost values on the write path#2184

Draft
mehara-rothila wants to merge 1 commit into
wso2:mainfrom
mehara-rothila:fix/vhost-normalization-collision
Draft

fix(controller): normalize vhost values on the write path#2184
mehara-rothila wants to merge 1 commit into
wso2:mainfrom
mehara-rothila:fix/vhost-normalization-collision

Conversation

@mehara-rothila

@mehara-rothila mehara-rothila commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Purpose

The gateway controller stored API vhost values verbatim and emitted them as Envoy virtual-host domains. Envoy compares domains case-insensitively, ignores surrounding whitespace, and requires every domain in a route configuration to be unique. Two APIs whose vhosts differed only in case or whitespace therefore produced duplicate Envoy domains. The control plane returned success (201), but Envoy rejected the entire shared_route_config and stopped applying any further route updates gateway-wide until the offending API was removed.

Resolves #2183.

Goals

Store vhost values in a canonical form (lower-cased and trimmed) so that values differing only in case or surrounding whitespace are no longer emitted as separate, colliding Envoy domains, while keeping legitimate vhost sharing (multiple APIs on one host, routed by path) working.

Approach

Normalize vhost values inside the shared resolveVhostSentinels resolver, which runs on the deploy path before vhosts are persisted and rendered. Helpers normalizeVhost and isGatewayDefaultVhost are added, and the resolver now:

  • populates omitted vhosts with the normalized router defaults,
  • detects the gateway-default sentinel case- and whitespace-insensitively, and
  • normalizes explicit main and sandbox values.

Supporting changes that keep the canonical form consistent everywhere it is compared:

  • The configured router defaults (router.vhosts.main.default and router.vhosts.sandbox.default) are canonicalized once at config validation (validateVHostsConfig), so a mixed-case configured default cannot drift from the stored values.
  • The xDS translator's default-domain expansion (getVHostDomains) compares the stored vhost against the configured default case- and whitespace-insensitively. This keeps domain expansion working for APIs that were stored before this change with a non-canonical default (for example a previously mixed-case configured default), so the change is safe on upgrade without a redeploy.

Template expressions in a vhost (for example {{ env "API_HOST" }}) are left untouched by normalization, because they are rendered later by RenderSpec and their case is significant for the lookup key. A helper, canonicalizeVhost, normalizes a value only when it is not a template (does not contain {{), matching the template check RenderSpec itself uses. Sentinel and configured-default substitutions are never templates, so they continue to be normalized directly.

Because normalization collapses case and whitespace variants to the same string, they group into a single Envoy virtual host instead of duplicate domains.

A cross-API vhost uniqueness rejection is intentionally not added: multiple APIs may legitimately share a vhost and route by path (this is how the default host is used), so rejecting a shared vhost would break normal routing. The fix is canonical storage, not rejection.

User stories

As an API developer, when I deploy an API whose vhost differs only in letter case or surrounding whitespace from another API's vhost, the gateway should keep programming routes normally instead of silently freezing all route updates.

Documentation

N/A. This is a bug fix. The stored and echoed vhost value is now lower-cased and trimmed; a vhost is a hostname and is case-insensitive, so this does not change routing behavior. Templated vhosts are unaffected, their template text is preserved for rendering. Existing deployments are unaffected on upgrade, the translator's default-domain comparison is case-insensitive, so already-stored non-canonical vhosts still match the configured default.

Automation tests

  • Unit tests

    New tests in pkg/utils/api_deployment_test.go: TestResolveVhostSentinels_NormalizesCaseAndWhitespace (explicit main and sandbox values are lower-cased and trimmed), TestResolveVhostSentinels_NormalizesSentinelCaseAndWhitespace (case and whitespace variants of the gateway-default sentinel resolve to the configured defaults), and TestResolveVhostSentinels_PreservesTemplateExpressions (a vhost containing a template expression is left untouched). New test in pkg/config/config_test.go: TestConfig_ValidateVHostsConfig_NormalizesDefaults (configured defaults are canonicalized at validation). New subtest in pkg/xds/translator_test.go under TestTranslator_GetVHostDomains (a non-canonical stored vhost still matches the canonical default case- and whitespace-insensitively). The existing tests continue to pass.

  • Integration tests

    None added in this PR. The resolver runs on the existing deploy path that the vhost-routing integration suite already exercises.

Security checks

Samples

Two APIs deployed on the same host with different letter case:

# API A
spec:
  vhosts:
    main: a.example.com
# API B (same host, different case)
spec:
  vhosts:
    main: A.EXAMPLE.COM

Before: API B was stored as A.EXAMPLE.COM, Envoy saw two a.example.com domains, rejected the whole route configuration, and route programming froze gateway-wide. After: API B is stored as a.example.com, both APIs share one virtual host and route by path, and the route configuration is applied normally.

Related PRs

N/A.

Test environment

  • Go (version per gateway/gateway-controller/go.mod)
  • Unit tests pass (go test ./pkg/utils/... ./pkg/config/... ./pkg/xds/...).

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8d5a30f7-7b46-4578-958d-996c328f430d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This pull request adds vhost canonicalization helpers (trim + lowercase, sentinel detection, and template-preserving canonicalization), applies them in resolveVhostSentinels for RestAPI/WebSubAPI/WebBrokerApi (using normalized router defaults and using normalized sentinel checks), and normalizes router vhost defaults during config validation. Tests were added to verify template-expression preservation, explicit normalization, and sentinel normalization.

Sequence Diagram(s)

sequenceDiagram
  participant Client as API submission
  participant Controller as resolveVhostSentinels
  participant RouterCfg as RouterConfig (normalized defaults)
  participant Store as API store
  Client->>Controller: submit API (may include vhosts.main / vhosts.sandbox)
  Controller->>RouterCfg: read defaults (already normalized)
  Controller->>Controller: isGatewayDefaultVhost / canonicalizeVhost / normalizeVhost
  Controller->>Store: persist resolved/canonicalized vhosts
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the primary change: normalizing vhost values during persistence on the write path.
Linked Issues check ✅ Passed The PR fully implements the requirements from issue #2183: normalizes vhosts (lowercase and trim) on the write path, applies case/whitespace-insensitive sentinel detection, populates omitted vhosts with normalized defaults, and preserves template expressions.
Out of Scope Changes check ✅ Passed All changes are scoped to vhost normalization: helper functions in api_deployment.go, resolver logic updates, config validation changes, and corresponding test coverage. No unrelated modifications detected.
Description check ✅ Passed The pull request description comprehensively covers all required template sections with detailed explanations, clear rationale, and technical implementation details.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@mehara-rothila

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
gateway/gateway-controller/pkg/utils/api_deployment_test.go (1)

999-1060: ⚡ Quick win

Add a regression case for mixed-case router default values.

Current new tests validate input/sentinel normalization well, but they don’t cover router defaults containing mixed/upper case. Add a case where routerCfg.VHosts.Main.Default/Sandbox.Default are mixed-case and verify resolved behavior remains consistent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gateway/gateway-controller/pkg/utils/api_deployment_test.go` around lines 999
- 1060, Add a regression test to ensure router default vhost values are
normalized when they contain mixed/upper-case: update or add a
TestResolveVhostSentinels_* case that builds a routerCfg whose
VHosts.Main.Default and VHosts.Sandbox.Default include mixed case and
surrounding whitespace (e.g. "*.WSO2.COM " and " *-SANDBOX.WSO2.COM"), call
resolveVhostSentinels(&cfg, routerCfg) and assert the resolved values (from
cfg.(api.RestAPI).Spec.Vhosts) equal the expected lower-cased, trimmed defaults
("*.wso2.com" and "*-sandbox.wso2.com"); reference resolveVhostSentinels and the
VHosts.Default fields when locating where to add the assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@gateway/gateway-controller/pkg/utils/api_deployment.go`:
- Around line 682-699: The code compares c.Spec.Vhosts.Main against
routerCfg.VHosts.Main.Default but normalizeVhost now lowercases
stored/default-resolved vhosts, so make the equality check use the same
canonical form: call normalizeVhost(...) on routerCfg.VHosts.Main.Default (and
routerCfg.VHosts.Sandbox.Default where used) before passing to
isGatewayDefaultVhost or assigning/ comparing; e.g., replace direct uses of
routerCfg.VHosts.Main.Default with normalizeVhost(routerCfg.VHosts.Main.Default)
and ensure isGatewayDefaultVhost is invoked with the normalized value so
default-domain expansion triggers correctly for mixed-case configured defaults
(update both the isGatewayDefaultVhost(c.Spec.Vhosts.Main) check and the
assignment branches involving c.Spec.Vhosts.Main).

---

Nitpick comments:
In `@gateway/gateway-controller/pkg/utils/api_deployment_test.go`:
- Around line 999-1060: Add a regression test to ensure router default vhost
values are normalized when they contain mixed/upper-case: update or add a
TestResolveVhostSentinels_* case that builds a routerCfg whose
VHosts.Main.Default and VHosts.Sandbox.Default include mixed case and
surrounding whitespace (e.g. "*.WSO2.COM " and " *-SANDBOX.WSO2.COM"), call
resolveVhostSentinels(&cfg, routerCfg) and assert the resolved values (from
cfg.(api.RestAPI).Spec.Vhosts) equal the expected lower-cased, trimmed defaults
("*.wso2.com" and "*-sandbox.wso2.com"); reference resolveVhostSentinels and the
VHosts.Default fields when locating where to add the assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 59af2b40-2aad-407a-b481-5afd610b809b

📥 Commits

Reviewing files that changed from the base of the PR and between 4acc5fc and bfedb6b.

📒 Files selected for processing (2)
  • gateway/gateway-controller/pkg/utils/api_deployment.go
  • gateway/gateway-controller/pkg/utils/api_deployment_test.go

Comment thread gateway/gateway-controller/pkg/utils/api_deployment.go
@mehara-rothila mehara-rothila force-pushed the fix/vhost-normalization-collision branch 2 times, most recently from 7615025 to 2a7f13e Compare June 13, 2026 18:08
@mehara-rothila

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

Vhost values were stored verbatim and emitted as Envoy virtual-host
domains. Envoy compares domains case-insensitively and ignores surrounding
whitespace, so two APIs whose vhosts differ only in case or whitespace
produced duplicate domains. Envoy then rejected the whole route
configuration and stopped applying further route updates gateway-wide,
while the deploy still returned success.

Normalize vhost values (lower-case and trim) when resolving the
gateway-default sentinel and when populating omitted defaults, so the
stored value is canonical, and match the sentinel case- and
whitespace-insensitively. Canonicalize the configured router defaults at
config validation too. The xDS translator default-domain expansion now
compares the stored vhost against the configured default case- and
whitespace-insensitively, so APIs stored before this change still match on
upgrade without a redeploy. Template expressions in a vhost (for example
{{ env "API_HOST" }}) are left untouched, since they are rendered later and
their case is significant for the lookup key.

- Add unit tests covering explicit case/whitespace normalization, sentinel
  case/whitespace variants, configured-default canonicalization, template
  preservation, and case-insensitive default-domain matching

Fixes wso2#2183
@mehara-rothila mehara-rothila force-pushed the fix/vhost-normalization-collision branch from 2a7f13e to 200751e Compare June 13, 2026 20:30
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.

[Bug]: Colliding vhosts accepted at control plane, rejected by Envoy

1 participant