Skip to content

Add per-operation upstream override for REST API operations#2012

Open
mehara-rothila wants to merge 6 commits into
wso2:mainfrom
mehara-rothila:feat/per-op-upstream-gateway
Open

Add per-operation upstream override for REST API operations#2012
mehara-rothila wants to merge 6 commits into
wso2:mainfrom
mehara-rothila:feat/per-op-upstream-gateway

Conversation

@mehara-rothila

@mehara-rothila mehara-rothila commented May 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a per-operation upstream override on REST API operations: an operation can route its main and/or sandbox traffic to a different backend than the API-level upstream. Per-op targets are ref-only: each references a named entry in spec.upstreamDefinitions rather than carrying an inline URL. Operations without a per-op upstream fall back to the API-level upstream, exactly as before.

A per-op route reuses the referenced upstreamDefinition's cluster (one cluster per definition, so operations sharing a ref share it) rather than minting a cluster per operation. API-level main/sandbox cluster names are URL-stable (derived from a hash of the API's identity, not the URL). Both are URL-independent, so URL edits update Envoy endpoints in place instead of recreating clusters.

Why

Real-world APIs often need individual operations to reach different backend services. Today that needs path-based workarounds or splitting into multiple APIs. This lets the API definition declare per-operation routing directly, while backend URLs stay defined once in upstreamDefinitions and are referenced by name.

Design decisions

Ref-only at the operation level. operations[].upstream.main / .sandbox carry only a ref to a named upstreamDefinition, with no inline url and no per-target timeout/hostRewrite. Backends are declared once in spec.upstreamDefinitions; connect timeout lives on the definition. (API-level upstream still accepts either url or ref, unchanged.)

Per-op routes reuse the definition cluster. A per-op ref does not create a new cluster. It points the operation's route at the cluster the referenced upstreamDefinition already owns (upstream_<kind>_<apiID>_<defName>, built once per definition). N operations sharing a ref cost one cluster, not N, and the route inherits the definition's authoritative base path. cluster_header stays on with that cluster as the default, so a dynamic-endpoint policy can still override the operation (precedence: op-policy > api-policy > per-op ref > api-level upstream).

URL-stable API-level cluster naming. API-level main/sandbox cluster names derive from the full sha256(apiID) (64 hex), via clusterkey.HashedName(env, apiID), producing main_<hash> / sandbox_<hash>. The env is a plaintext prefix, not part of the hash, so main and sandbox share the same hash. The URL is intentionally excluded, so a URL edit keeps the same cluster name and Envoy updates its endpoints in place (no connection drain) rather than recreating the cluster. The clusters are STRICT_DNS with an inline load assignment. Rollout: a one-time cluster rebuild on the first config push after upgrade (URL-derived to identity-based names), handled gracefully by Envoy's CDS warming; stable thereafter.

Single source of truth. Ref resolution and connect-timeout parsing go through pkg/utils/upstreamref (FindByName, ParseConnectTimeout); API-level cluster-name hashing goes through pkg/utils/clusterkey (Hash returns the full SHA-256 hash, HashedName joins a prefix to it). Both are consumed by the validator, the RDC transformer, and the xDS translator so they cannot drift. The policy-engine kernel (a separate Go module that cannot import clusterkey) reconstructs the definition cluster name for dynamic-endpoint targets; a kernel unit test pins its output byte-for-byte to clusterkey.DefinitionName so the two modules cannot drift either.

What changed (gateway-controller layer)

  • OpenAPI schema (management-openapi.yaml): optional upstream on Operation resolving to OperationUpstream (main/sandbox), each an inline ref-only object ({ ref }, no named target schema). Wrapper and each sub-field locked with additionalProperties: false, plus minProperties: 1 on the wrapper (an empty upstream: {} is rejected).
  • Validator (api_validator.go): rejects an empty upstream: {} wrapper; requires at least one of main/sandbox; rejects a ref that does not resolve; and enforces one name contract (^[a-zA-Z0-9\-_]+$, max 100 chars) on per-op refs, API-level refs, and upstreamDefinition names so every valid definition name is referenceable.
  • Transform (transform/restapi.go) and xDS translator (xds/translator.go): per-op routes reuse the referenced definition's cluster (upstream_<kind>_<apiID>_<defName>, built once per definition); API-level clusters use URL-stable names main_<hash>/sandbox_<hash>; operations without a per-op upstream fall back to the API-level cluster.
  • Shared utils (pkg/utils/upstreamref, pkg/utils/clusterkey): new leaf packages. upstreamref handles ref resolution and connect-timeout parsing; the stdlib-only clusterkey handles URL-stable API-level cluster-key hashing (Hash / HashedName) and definition cluster naming (DefinitionName).

Cluster names: before / after

For an API with UUID 0190b3e2-7b1c-7c2a-9b3d-1a2b3c4d5e6f (sha256(apiID) = 54a9b3e5ce2b6ccb97168e5948a66f48e084213b38eb8c7dc01c6f624a63c2f2):

Cluster Before After
API-level main cluster_http_default-backend_8080 (URL-derived) main_54a9b3e5ce2b6ccb97168e5948a66f48e084213b38eb8c7dc01c6f624a63c2f2 (identity-based)
API-level sandbox cluster_http_sandbox-backend_8080 (URL-derived) sandbox_54a9b3e5ce2b6ccb97168e5948a66f48e084213b38eb8c7dc01c6f624a63c2f2 (identity-based)
Definition user-service upstream_RestApi_0190b3e2-7b1c-7c2a-9b3d-1a2b3c4d5e6f_user-service unchanged (same name, now built via the shared clusterkey.DefinitionName helper)

Only the API-level names change. Editing a backend URL previously renamed the API-level cluster, so Envoy dropped and recreated it; with identity-based names the cluster name never changes and Envoy updates the endpoints in place. Definition cluster names were already URL-independent.

Test coverage

  • Unit tests across pkg/config, pkg/transform, pkg/xds, pkg/utils/upstreamref, and pkg/utils/clusterkey: deterministic hash tests (Hash), validator rejection tests (empty/unknown/pattern/length refs, with exact error reasons), URL-stable cluster-name contract tests (same name across a URL edit), definition-cluster reuse and cross-API isolation tests, per-op route tests asserting the exact referenced definition cluster, a policy chain-order test (API-level policies precede operation-level, so the operation-level policy wins as the last write), and a policy-engine kernel test pinning the x-target-upstream value for a dotted/coloned definition name byte-for-byte to the controller's clusterkey.DefinitionName.
  • Integration tests (godog) across the per-op and URL-stable feature files: API-level fallback, per-op main/sandbox ref overrides, routing to two distinct real backends, validation rejections (strict 400), dynamic-endpoint precedence over per-op refs at both operation and API level, and URL-stable in-place endpoint updates. All passing against a local docker-compose stack.

Example

One API exercising every capability: API-level fallback, per-op main override, per-op main+sandbox overrides, definition base path and connect timeout, host rewrite, and a dynamic-endpoint policy overriding a per-op ref.

apiVersion: gateway.api-platform.wso2.com/v1
kind: RestApi
metadata:
  name: shop-api-v1.0
spec:
  displayName: Shop API
  version: v1.0
  context: /shop/$version
  vhosts:
    main: shop.example.com
    sandbox: sandbox.shop.example.com

  # Named backends, declared once, referenced everywhere
  upstreamDefinitions:
    - name: user-service
      basePath: /api/v2                 # prepended to requests routed to this backend
      timeout:
        connect: 5s                     # connect timeout lives on the definition (per-op targets are ref-only)
      upstreams:
        - url: http://user-service:8080
    - name: order-service
      timeout:
        connect: 500ms
      upstreams:
        - url: http://order-service:8080
    - name: promo-service
      upstreams:
        - url: http://promo-service:8080

  # API-level default upstream (url or ref; hostRewrite lives at this level)
  upstream:
    main:
      url: http://default-backend:8080
      hostRewrite: auto
    sandbox:
      ref: user-service                 # API-level upstream can also use a ref

  operations:
    # 1. No per-op upstream: falls back to the API-level upstream
    - method: GET
      path: /products

    # 2. Per-op main override: routes to user-service, inheriting its basePath and timeout
    - method: GET
      path: /users/{id}
      upstream:
        main:
          ref: user-service

    # 3. Per-op main and sandbox overrides: different backends per environment
    - method: POST
      path: /orders
      upstream:
        main:
          ref: order-service
        sandbox:
          ref: user-service

    # 4. Per-op ref plus dynamic-endpoint policy: the policy wins over the ref
    - method: GET
      path: /recommendations
      upstream:
        main:
          ref: user-service
      policies:
        - name: dynamic-endpoint
          version: v1
          params:
            targetUpstream: promo-service

Routing result:

  • GET /products goes to default-backend (no per-op upstream, API-level fallback)
  • GET /users/{id} goes to user-service under /api/v2 (per-op main ref, inherits the definition's basePath)
  • POST /orders goes to order-service on main and user-service on sandbox (per-op overrides per environment)
  • GET /recommendations goes to promo-service (the operation-level dynamic-endpoint policy overrides the per-op ref)

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds per-operation main and sandbox upstream references with schema support, validation, shared upstream lookup, deterministic cluster naming, REST/XDS routing integration, timeout propagation, persistence coverage, runtime regressions, and end-to-end scenarios.

Changes

Per-Operation Upstream Routing

Layer / File(s) Summary
Schema and generated management models
gateway/gateway-controller/api/management-openapi.yaml, gateway/gateway-controller/pkg/api/management/generated.go
Adds reusable upstream reference constraints and optional per-operation main and sandbox upstream targets.
Shared reference and cluster-key utilities
gateway/gateway-controller/pkg/utils/clusterkey/*, gateway/gateway-controller/pkg/utils/upstreamref/*
Adds deterministic cluster-key generation, upstream-definition lookup, timeout parsing, and sandbox activation helpers.
Definition and per-operation validation
gateway/gateway-controller/pkg/config/api_validator.go, gateway/gateway-controller/pkg/config/validator_test.go
Validates reference format, length, existence, operation override shape, upstream-definition names, and timeout values.
REST and XDS route wiring
gateway/gateway-controller/pkg/transform/restapi.go, gateway/gateway-controller/pkg/xds/translator.go
Routes per-operation overrides to referenced definition clusters, preserves API-level fallback, inherits base paths and timeouts, and uses API-scoped cluster names.
Persistence, runtime, and end-to-end coverage
gateway/gateway-controller/tests/integration/storage_test.go, gateway/gateway-runtime/policy-engine/internal/kernel/translator_test.go, gateway/it/features/*, gateway/it/suite_test.go
Adds storage, dynamic-endpoint, routing, precedence, validation, URL-stability, and cluster-identity coverage.

Estimated code review effort: 5 (Critical) | ~120 minutes

Suggested reviewers: krishanx92, renuka-fernando, malinthaprasan, thushani-jayasekera, arshardh

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant APIValidator
  participant RESTTransformer
  participant XDSTranslator
  participant UpstreamDefinition

  Client->>APIValidator: Submit operation upstream refs
  APIValidator->>UpstreamDefinition: Resolve referenced definitions
  UpstreamDefinition-->>APIValidator: Definition and timeout
  APIValidator-->>RESTTransformer: Validated operation configuration
  RESTTransformer->>XDSTranslator: Route override, cluster, and basePath
  XDSTranslator->>UpstreamDefinition: Resolve cluster and timeout
  XDSTranslator-->>Client: Main or sandbox route configuration
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% 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 is concise and clearly summarizes the main change: per-operation upstream overrides for REST API operations.
Description check ✅ Passed The description is mostly complete and covers purpose, goals, approach, tests, and examples, though some template sections like security checks and test environment are missing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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.

@CLAassistant

CLAassistant commented May 22, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@mehara-rothila

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

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: 3

🤖 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/api/management-openapi.yaml`:
- Around line 4139-4148: OperationUpstream currently allows an empty object
which is invalid; update the OperationUpstream schema (the OperationUpstream
object that defines properties main and sandbox) to require at least one of
those properties by adding an anyOf clause such as anyOf: - required: [main] -
required: [sandbox] (or an equivalent oneOf/anyOf expression) so the schema
enforces presence of main or sandbox and prevents {} being valid.

In `@gateway/gateway-controller/pkg/policy/builder.go`:
- Around line 185-186: The current condition treats any non-nil
op.Upstream.Sandbox as active; change it to perform a content-based check like
apiSandboxHasContent so empty sandbox objects are ignored. Introduce/compute a
per-op predicate (e.g., perOpSandboxHasContent) that mirrors the api-level
sandbox content test and use if apiSandboxHasContent || (op.Upstream != nil &&
perOpSandboxHasContent) to decide whether to append effectiveSandboxVHost.

In `@gateway/gateway-controller/pkg/xds/translator_test.go`:
- Around line 2061-2074: Update TestResolvePerOpUpstream_DedupSameURL to ensure
URL-stability by creating two distinct api.Upstream instances with different Url
values (e.g., "http://shared-svc:8080" and "http://shared-svc:8081") but the
same apiID, method, path and env, then call translator.resolvePerOpUpstream for
both and assert the returned cluster names are equal; keep the test name and use
the Translator.resolvePerOpUpstream and translator variable references so the
test still targets the same logic and verifies that cluster key derivation does
not depend on the exact URL string.
🪄 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: 4012d4ee-58df-4773-b82e-fa11e65290d8

📥 Commits

Reviewing files that changed from the base of the PR and between 943c653 and b2426a7.

📒 Files selected for processing (16)
  • gateway/gateway-controller/api/management-openapi.yaml
  • gateway/gateway-controller/pkg/api/management/generated.go
  • gateway/gateway-controller/pkg/config/api_validator.go
  • gateway/gateway-controller/pkg/config/validator_test.go
  • gateway/gateway-controller/pkg/models/runtime_deploy_config.go
  • gateway/gateway-controller/pkg/policy/builder.go
  • gateway/gateway-controller/pkg/policy/builder_test.go
  • gateway/gateway-controller/pkg/transform/restapi.go
  • gateway/gateway-controller/pkg/transform/restapi_test.go
  • gateway/gateway-controller/pkg/utils/commonutils.go
  • gateway/gateway-controller/pkg/utils/commonutils_test.go
  • gateway/gateway-controller/pkg/xds/translator.go
  • gateway/gateway-controller/pkg/xds/translator_test.go
  • gateway/it/features/per-op-upstream-basic.feature
  • gateway/it/features/per-op-upstream-ref.feature
  • gateway/it/features/per-op-upstream-validation.feature

Comment thread gateway/gateway-controller/api/management-openapi.yaml Outdated
Comment thread gateway/gateway-controller/pkg/policy/builder.go Outdated
Comment thread gateway/gateway-controller/pkg/xds/translator_test.go Outdated
mehara-rothila pushed a commit to mehara-rothila/api-platform that referenced this pull request May 22, 2026
- OperationUpstream schema now requires at least one of main or sandbox
  via anyOf, rejecting empty {} at the schema layer as defense in depth
  alongside the validator.
- Policy builder per-op sandbox check is now content-based (trims and
  checks Url/Ref non-empty) matching the apiSandboxHasContent pattern
  used elsewhere. Fixes asymmetric pointer-only check that would emit
  sandbox policy chains for empty sandbox wrappers.
- TestResolvePerOpUpstream_DedupSameURL now uses two distinct URLs with
  identical apiID|METHOD|path|env, asserting cluster name independence
  from URL. Pins the EDS-stable design contract.
@mehara-rothila

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

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.

@mehara-rothila

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 23, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

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.

@mehara-rothila

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

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.

🧹 Nitpick comments (1)
gateway/gateway-controller/pkg/policyxds/policyxds_test.go (1)

120-120: 💤 Low value

Consider using a hash-like cluster key suffix for test realism.

The cluster key "main_fixture" has the correct main_ prefix for an API-level main environment cluster, but the suffix fixture doesn't reflect the production naming pattern introduced in this PR. According to the PR objectives, API-level clusters follow the pattern <env>_<sha256(apiID|env)[:8]> (e.g., main_a1b2c3d4). While the current fixture name is functionally correct and clear, using a hash-like suffix such as "main_12ab34cd" would make the test more representative of production behavior.

📝 Proposed update for more realistic fixture naming
 					Upstream: models.RouteUpstream{
-						ClusterKey: "main_fixture",
+						ClusterKey: "main_12ab34cd",
 					},
 				UpstreamClusters: map[string]*models.UpstreamCluster{
-					"main_fixture": {
+					"main_12ab34cd": {
 						BasePath:  "/",

Also applies to: 132-132

🤖 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/policyxds/policyxds_test.go` at line 120,
Update the test fixture cluster key strings that currently use "main_fixture" to
a realistic hash-like suffix such as "main_12ab34cd" to match the production
pattern; locate the occurrences in the policyxds_test setup where ClusterKey:
"main_fixture" is declared (the test fixture variables/constants used in Policy
XDS tests) and replace them with "main_12ab34cd" (or another 8-char hex-like
hash) to make the test names reflect the <env>_<sha256(... )[:8]> convention.
🤖 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.

Nitpick comments:
In `@gateway/gateway-controller/pkg/policyxds/policyxds_test.go`:
- Line 120: Update the test fixture cluster key strings that currently use
"main_fixture" to a realistic hash-like suffix such as "main_12ab34cd" to match
the production pattern; locate the occurrences in the policyxds_test setup where
ClusterKey: "main_fixture" is declared (the test fixture variables/constants
used in Policy XDS tests) and replace them with "main_12ab34cd" (or another
8-char hex-like hash) to make the test names reflect the <env>_<sha256(...
)[:8]> convention.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8729c50c-d6aa-40f4-b0b0-efec0f9c903c

📥 Commits

Reviewing files that changed from the base of the PR and between c242faa and ab55925.

📒 Files selected for processing (13)
  • gateway/gateway-controller/cmd/controller/main.go
  • gateway/gateway-controller/pkg/config/api_validator.go
  • gateway/gateway-controller/pkg/policyxds/policyxds_test.go
  • gateway/gateway-controller/pkg/transform/restapi.go
  • gateway/gateway-controller/pkg/transform/restapi_test.go
  • gateway/gateway-controller/pkg/utils/clusterkey/clusterkey.go
  • gateway/gateway-controller/pkg/utils/clusterkey/clusterkey_test.go
  • gateway/gateway-controller/pkg/utils/commonutils_test.go
  • gateway/gateway-controller/pkg/xds/translator.go
  • gateway/gateway-controller/pkg/xds/translator_test.go
  • gateway/it/features/api-level-eds-stable.feature
  • gateway/it/features/per-op-upstream-basic.feature
  • gateway/it/suite_test.go
💤 Files with no reviewable changes (1)
  • gateway/gateway-controller/pkg/utils/commonutils_test.go
✅ Files skipped from review due to trivial changes (2)
  • gateway/gateway-controller/cmd/controller/main.go
  • gateway/it/suite_test.go

@mehara-rothila

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

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: 2

🧹 Nitpick comments (1)
gateway/gateway-controller/pkg/config/validator_test.go (1)

773-796: ⚡ Quick win

Isolate invalid-timeout cases with subtests.

Using require inside the loop means the first failing case can stop execution before the next case is checked. Wrapping each badTimeout in t.Run(...) keeps both cases independently validated and easier to diagnose.

Proposed test refactor
- for _, badTimeout := range []string{"0s", "-5s"} {
+ for _, badTimeout := range []string{"0s", "-5s"} {
+   t.Run(badTimeout, func(t *testing.T) {
      connect := badTimeout
      definitions := &[]api.UpstreamDefinition{
        {
          Name: "my-upstream",
          Timeout: &api.UpstreamTimeout{
            Connect: &connect,
          },
          Upstreams: []struct {
            Url    string `json:"url" yaml:"url"`
            Weight *int   `json:"weight,omitempty" yaml:"weight,omitempty"`
          }{
            {
              Url: "http://backend:8080",
            },
          },
        },
      }

      errors := validator.validateUpstreamDefinitions(definitions)
      require.Len(t, errors, 1, "timeout %q must be rejected", badTimeout)
      assert.Equal(t, "spec.upstreamDefinitions[0].timeout.connect", errors[0].Field)
      assert.Contains(t, errors[0].Message, "must be a positive duration")
+   })
  }
🤖 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/config/validator_test.go` around lines 773 -
796, The test loop in validator_test.go uses require inside a for-range which
can abort the whole loop on first failure; refactor by wrapping each badTimeout
iteration in a subtest using t.Run(fmt.Sprintf("timeout=%s", badTimeout), func(t
*testing.T) { ... }) and move the assertions (require.Len, assert.Equal,
assert.Contains) into that subtest, capturing the loop variable (e.g., tt :=
badTimeout) before using it to build the definitions passed to
validator.validateUpstreamDefinitions so each case runs and reports
independently.
🤖 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/api/management-openapi.yaml`:
- Around line 4136-4137: The schemas that currently use a $ref with a sibling
description (referencing "`#/components/schemas/RestAPIOperationUpstream`") must
be changed so the description is not a sibling of $ref; instead wrap the
reference in an allOf and move the description onto the enclosing schema object.
Concretely, replace occurrences where a property has "$ref:
'`#/components/schemas/RestAPIOperationUpstream`'" alongside "description" by
creating an object with "description: <same text>" and "allOf: [{ $ref:
'`#/components/schemas/RestAPIOperationUpstream`' }]" (apply this change for every
place referencing RestAPIOperationUpstream in the file).

In `@gateway/gateway-controller/pkg/config/api_validator.go`:
- Around line 656-667: The per-operation upstream ref checks (using
upstreamRefRegex and length >100 in the validate path that returns
ValidationError for field/refName) are stricter than names allowed by
validateUpstreamDefinitions, causing valid definition names to become
unreferencable; to fix, apply the same validation rules to
spec.upstreamDefinitions[*].name inside validateUpstreamDefinitions (reuse
upstreamRefRegex and the 100-character limit and return a ValidationError with
the same message format when a definition name violates them) OR remove the
extra constraints from the per-operation check so both places use the same
contract; update validateUpstreamDefinitions to reference the same
upstreamRefRegex and error messages (Field and Message) so names and refs are
aligned.

---

Nitpick comments:
In `@gateway/gateway-controller/pkg/config/validator_test.go`:
- Around line 773-796: The test loop in validator_test.go uses require inside a
for-range which can abort the whole loop on first failure; refactor by wrapping
each badTimeout iteration in a subtest using t.Run(fmt.Sprintf("timeout=%s",
badTimeout), func(t *testing.T) { ... }) and move the assertions (require.Len,
assert.Equal, assert.Contains) into that subtest, capturing the loop variable
(e.g., tt := badTimeout) before using it to build the definitions passed to
validator.validateUpstreamDefinitions so each case runs and reports
independently.
🪄 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: a0b21e0b-ca37-459a-b596-04dce4de1349

📥 Commits

Reviewing files that changed from the base of the PR and between ab55925 and 4d08e3a.

📒 Files selected for processing (18)
  • gateway/gateway-controller/api/management-openapi.yaml
  • gateway/gateway-controller/pkg/api/management/generated.go
  • gateway/gateway-controller/pkg/config/api_validator.go
  • gateway/gateway-controller/pkg/config/validator_test.go
  • gateway/gateway-controller/pkg/policy/builder.go
  • gateway/gateway-controller/pkg/policy/builder_test.go
  • gateway/gateway-controller/pkg/transform/restapi.go
  • gateway/gateway-controller/pkg/transform/restapi_test.go
  • gateway/gateway-controller/pkg/utils/upstreamref/upstreamref.go
  • gateway/gateway-controller/pkg/utils/upstreamref/upstreamref_test.go
  • gateway/gateway-controller/pkg/xds/translator.go
  • gateway/gateway-controller/pkg/xds/translator_test.go
  • gateway/gateway-controller/tests/integration/storage_test.go
  • gateway/gateway-runtime/policy-engine/internal/kernel/translator.go
  • gateway/gateway-runtime/policy-engine/internal/kernel/translator_test.go
  • gateway/it/features/per-op-upstream-basic.feature
  • gateway/it/features/per-op-upstream-ref.feature
  • gateway/it/features/per-op-upstream-validation.feature
🚧 Files skipped from review as they are similar to previous changes (4)
  • gateway/gateway-controller/pkg/policy/builder.go
  • gateway/gateway-controller/pkg/policy/builder_test.go
  • gateway/gateway-controller/pkg/transform/restapi.go
  • gateway/gateway-controller/pkg/xds/translator.go

Comment thread gateway/gateway-controller/api/management-openapi.yaml Outdated
Comment thread gateway/gateway-controller/pkg/config/api_validator.go Outdated
@mehara-rothila

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

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.

@renuka-fernando
renuka-fernando changed the base branch from main to feature/operation-level-ep June 3, 2026 12:46
@mehara-rothila
mehara-rothila force-pushed the feat/per-op-upstream-gateway branch 2 times, most recently from 4cfd850 to 4caca38 Compare June 4, 2026 00:16
@mehara-rothila

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 4, 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.

@renuka-fernando
renuka-fernando changed the base branch from feature/operation-level-ep to main June 4, 2026 00:31
@renuka-fernando
renuka-fernando changed the base branch from main to feature/operation-level-ep June 4, 2026 00:32
@renuka-fernando
renuka-fernando marked this pull request as ready for review June 4, 2026 00:32
@mehara-rothila
mehara-rothila force-pushed the feat/per-op-upstream-gateway branch from 80924d0 to 66dd517 Compare July 6, 2026 09:09
@mehara-rothila

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

Please do a focused pass on code duplication only (not correctness - the code is
tested and green). For each changed file, and between changed files and the
pre-existing code around them, flag:

  • logic written twice that could be one shared helper or constant
  • repeated string/number literals that should be centralized
  • loop-invariant work recomputed per iteration

For each finding: file:line, why it duplicates, and a concrete shape to fix it.

Known and intentionally left as-is, please skip these:

  • clusterkey.DefinitionName kept separate from Hash/HashedName
  • perOpDefinitionClusterKey vs resolvePerOpDefinitionCluster (different outputs, cross-module)
  • the policy-engine kernel's mirrored cluster-name builder (separate module, byte-identity pinned)

Cluster-name outputs must stay byte-identical - don't propose anything that changes them.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "===1. VALIDATOR: repeated literals/logic in api_validator.go==="
grep -n "100\|maxLength\|MaxLength\|not found in upstreamDefinitions\|must match pattern\|must not exceed\|regexp.MustCompile\|upstreamRefRegex\|connectTimeoutRegex" gateway/gateway-controller/pkg/config/api_validator.go

echo ""
echo "===2. FULL VALIDATOR FILE (for structural dup check)==="
cat -n gateway/gateway-controller/pkg/config/api_validator.go

echo ""
echo "===3. RESTAPI.GO FULL (for per-op loop dup check)==="
cat -n gateway/gateway-controller/pkg/transform/restapi.go

echo ""
echo "===4. TRANSLATOR.GO FULL (for per-op loop dup check)==="
cat -n gateway/gateway-controller/pkg/xds/translator.go

Length of output: 185847


I have enough evidence now. Here are the concrete duplication findings, scoped strictly to duplication (not correctness), with the three known items excluded as requested.


@mehara-rothila — focused duplication pass across the changed files. All cluster-name-producing code is left untouched per your constraint; these are message/logic/literal consolidations only.

1. spec.upstream.<label> field-path string built ad hoc, 8+ times

File: gateway/gateway-controller/pkg/config/api_validator.go, Lines 174, 183, 206, 215, 223, 229, 274, 282, 288, 298

Every branch of validateUpstream, validateUpstreamUrl, and validateUpstreamRef re-concatenates "spec.upstream." + label + ... inline. validateOperationUpstreamRef (Line 720) does the equivalent for the per-op path with its own fmt.Sprintf.

Fix shape:

func apiUpstreamField(label, suffix string) string {
    return "spec.upstream." + label + suffix
}

Replace the 8 inline concatenations in validateUpstream/validateUpstreamUrl/validateUpstreamRef with calls to this helper (e.g. apiUpstreamField(label, ".url"), apiUpstreamField(label, ".ref")).


2. HTTP scheme/host validation duplicated between API-level and definition-level upstream URLs

File: api_validator.go, Lines 221–233 (validateUpstreamUrl) vs. Lines 367–379 (validateUpstreamDefinitions upstream loop)

Both blocks perform the identical scheme check (!= "http" && != "https") and identical host-empty check, just against different field strings and with the definitions-loop version additionally checking path/query/fragment.

Fix shape:

// validateHTTPURLBasics returns scheme/host errors shared by both call sites.
func validateHTTPURLBasics(field string, parsedURL *url.URL) []ValidationError

validateUpstreamUrl calls it directly; validateUpstreamDefinitions calls it then appends its extra path/query/fragment checks.


3. "no URLs configured" error duplicated 5 times with drifting wording

Files/lines:

  • restapi.go Line 435: "upstream definition '%s' has no URLs" (perOpDefinitionClusterKey)
  • restapi.go Line 456: "upstream definition '%s' has no URLs" (resolveUpstreamURL)
  • translator.go Line 926: "upstream definition '%s' has no URLs configured" (translateAPIConfig)
  • translator.go Line 996: "upstream definition '%s' has no URLs configured" (resolveUpstreamCluster)
  • translator.go Line 1053: "upstream definition '%s' has no URLs configured" (resolvePerOpDefinitionCluster)

Two different message strings for the same condition, checked with the identical len(def.Upstreams) == 0 || def.Upstreams[0].Url == "" guard each time.

Fix shape: add to upstreamref.go (already the shared home for this kind of check):

func RequireURL(def *api.UpstreamDefinition) (string, error) {
    if def == nil || len(def.Upstreams) == 0 || def.Upstreams[0].Url == "" {
        return "", fmt.Errorf("upstream definition '%s' has no URLs configured", def.Name)
    }
    return def.Upstreams[0].Url, nil
}

All five call sites reduce to rawURL, err := upstreamref.RequireURL(def).


4. BasePath-defaulting logic repeated with two different defaults ("/" vs "")

Lines:

  • restapi.go Line 259–261 → defaults to "/"
  • restapi.go Line 458–461 (resolveUpstreamURL) → defaults to ""
  • translator.go Line 814–819 (upstreamDefPaths population) → defaults to "/"
  • translator.go Line 1002–1006 (resolveUpstreamCluster) → defaults to ""
  • translator.go Line 1065–1067 (resolvePerOpDefinitionCluster) → defaults to "/"

Same if def.BasePath != nil && *def.BasePath != "" { ... } shape appears 5 times, and the default value silently differs by call site (a latent inconsistency risk, though not a correctness bug today since callers normalize downstream).

Fix shape: add one helper to upstreamref.go and have every site pass its own default explicitly so the difference is visible at the call site instead of hidden in copy-pasted logic:

func BasePathOrDefault(def *api.UpstreamDefinition, fallback string) string {
    if def != nil && def.BasePath != nil && *def.BasePath != "" {
        return *def.BasePath
    }
    return fallback
}

5. "ref is required" empty-check duplicated with drifting wording

Lines: api_validator.go Line 271–278 (validateUpstreamRef: "Upstream reference is required") vs. Line 723–728 (validateOperationUpstreamRef: "Upstream ref is required")

Same empty/whitespace check (ref == nil || strings.TrimSpace(*ref) == ""), same intent, different message text purely by accident of two authors writing it separately.

Fix shape: factor the trim+empty check into validateUpstreamRefName's caller convention, or a tiny shared helper:

func requireNonEmptyRef(field string, ref *string) (string, []ValidationError) {
    if ref == nil || strings.TrimSpace(*ref) == "" {
        return "", []ValidationError{{Field: field, Message: "Upstream ref is required"}}
    }
    return strings.TrimSpace(*ref), nil
}

Both validateUpstreamRef and validateOperationUpstreamRef call this, converging on one message string.


Not flagged (per your exclusion list, confirmed present but intentionally skipped)

  • clusterkey.DefinitionName vs Hash/HashedName — separate by design.
  • perOpDefinitionClusterKey (restapi.go Line 429) vs resolvePerOpDefinitionCluster (translator.go Line 1043) — different output shapes (cluster key only vs. cluster+basePath+timeout), cross-module.
  • Policy-engine kernel's mirrored cluster-name builder — separate module, byte-pinned.

None of the above five suggestions touch cluster-name generation, clusterkey.DefinitionName/HashedName, or anything on the byte-identity-pinned path — they're purely message/validation/base-path helper consolidations.


ヽ(・∀・)ノ

@mehara-rothila

Copy link
Copy Markdown
Contributor Author

@coderabbitai Thanks, but please re-run scoped to THIS PR's code only.

Only flag duplication where BOTH occurrences are in code this PR adds or modifies (check against the diff base d891a12). Do NOT flag pre-existing base functions or duplication between our code and base code - specifically skip validateUpstream, validateUpstreamUrl, validateUpstreamRef, validateUpstreamDefinitions (api_validator.go), resolveUpstreamURL (restapi.go), and resolveUpstreamCluster (translator.go) - those predate this PR and are out of scope.

In-scope surfaces are only: the upstreamref package, validateOperationUpstreamRef and validateUpstreamRefName, perOpDefinitionClusterKey, resolvePerOpDefinitionCluster, the sandbox helpers HasContent/SandboxActive, the clusterkey Hash/HashedName helpers, and the per-op route loops we added in restapi.go and translator.go.

Still exclude (by design): clusterkey.DefinitionName, the perOpDefinitionClusterKey vs resolvePerOpDefinitionCluster cross-module pair, and the policy-engine kernel mirror. Cluster-name outputs must stay byte-identical.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "===CHECK BASE COMMIT AVAILABLE==="
git cat-file -e d891a12e4 2>&1 && echo "base exists" || echo "base NOT found, trying fetch"
git log --oneline -1 d891a12e4 2>/dev/null || true

echo ""
echo "===DIFF FOR upstreamref.go==="
git diff d891a12e4 -- gateway/gateway-controller/pkg/utils/upstreamref/upstreamref.go || true

echo ""
echo "===DIFF FOR clusterkey.go==="
git diff d891a12e4 -- gateway/gateway-controller/pkg/utils/clusterkey/clusterkey.go || true

echo ""
echo "===DIFF FOR api_validator.go (validateOperationUpstreamRef / validateUpstreamRefName)==="
git diff d891a12e4 -- gateway/gateway-controller/pkg/config/api_validator.go | grep -n "validateOperationUpstreamRef\|validateUpstreamRefName\|^@@\|^+" | head -300

echo ""
echo "===DIFF FOR restapi.go (perOpDefinitionClusterKey, sandbox helpers, per-op loop)==="
git diff d891a12e4 -- gateway/gateway-controller/pkg/transform/restapi.go > /tmp/restapi.diff
wc -l /tmp/restapi.diff
cat /tmp/restapi.diff

echo ""
echo "===DIFF FOR translator.go (resolvePerOpDefinitionCluster, per-op loop)==="
git diff d891a12e4 -- gateway/gateway-controller/pkg/xds/translator.go > /tmp/translator.diff
wc -l /tmp/translator.diff
cat /tmp/translator.diff

Length of output: 41480


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "===restapi.go line numbers for per-op override block==="
grep -n "op.Upstream.Main != nil\|op.Upstream.Sandbox != nil\|perOpDefinitionClusterKey\|per-op main upstream\|per-op sandbox upstream" gateway/gateway-controller/pkg/transform/restapi.go

echo ""
echo "===translator.go line numbers for per-op override block==="
grep -n "op.Upstream != nil && op.Upstream.Main != nil\|op.Upstream != nil && op.Upstream.Sandbox != nil\|resolvePerOpDefinitionCluster\|per-op main upstream\|per-op sandbox upstream" gateway/gateway-controller/pkg/xds/translator.go

echo ""
echo "===api_validator.go and upstreamref.go trim-empty check line numbers==="
grep -n "refName == \"\"\|TrimSpace(\*ref)\|TrimSpace(ref)" gateway/gateway-controller/pkg/config/api_validator.go gateway/gateway-controller/pkg/utils/upstreamref/upstreamref.go

Length of output: 3593


@mehara-rothila — re-scanned strictly within the PR-added/modified code, restricted to the in-scope surfaces you listed, and excluding what you asked to skip. Two solid intra-file findings, plus one minor pattern worth a mention.


1. restapi.go — Main/Sandbox per-op override branches are the same shape twice

Lines 176–192 (per-op loop in Transform)

if op.Upstream.Main != nil {
    defClusterKey, err := perOpDefinitionClusterKey(cfg.Kind, cfg.UUID, &op.Upstream.Main.Ref, apiData.UpstreamDefinitions)
    if err != nil {
        return nil, fmt.Errorf("per-op main upstream for %s %s: %w", string(op.Method), op.Path, err)
    }
    mainVhostClusterKey = defClusterKey
    mainVhostUseClusterHeader = true
    mainVhostDefaultCluster = defClusterKey
}
if op.Upstream.Sandbox != nil {
    defClusterKey, err := perOpDefinitionClusterKey(cfg.Kind, cfg.UUID, &op.Upstream.Sandbox.Ref, apiData.UpstreamDefinitions)
    if err != nil {
        return nil, fmt.Errorf("per-op sandbox upstream for %s %s: %w", string(op.Method), op.Path, err)
    }
    sandboxVhostClusterKey = defClusterKey
    sandboxVhostUseClusterHeader = true
    sandboxVhostDefaultCluster = defClusterKey
}

Both branches: resolve the same way (perOpDefinitionClusterKey), wrap the error identically (only the "main"/"sandbox" word differs), then fan the same result out to three vars. Pure copy-paste with the label swapped.

Fix shape: extract a small local helper that returns (clusterKey string, err error) and takes the label for the error message:

func resolvePerOpOverride(kind, uuid, label string, target *api.RestAPIOperationUpstreamTarget, defs *[]api.UpstreamDefinition, method, path string) (string, error) {
    if target == nil {
        return "", nil
    }
    key, err := perOpDefinitionClusterKey(kind, uuid, &target.Ref, defs)
    if err != nil {
        return "", fmt.Errorf("per-op %s upstream for %s %s: %w", label, method, path, err)
    }
    return key, nil
}

Both branches collapse to one call each, and UseClusterHeader/DefaultCluster assignment stays inline (both always become true/key when non-empty, so that fan-out is trivial at the call site).


2. translator.go — same duplication pattern in translateAPIConfig

Lines 833–836 vs Lines 901–904

if op.Upstream != nil && op.Upstream.Main != nil {
    defClusterName, defBasePath, defTimeout, err := t.resolvePerOpDefinitionCluster(cfg.Kind, cfg.UUID, &op.Upstream.Main.Ref, apiData.UpstreamDefinitions)
    if err != nil {
        return nil, nil, fmt.Errorf("per-op main upstream for %s %s: %w", string(op.Method), op.Path, err)
    }
    ...
}

and the sandbox branch at Line 901 is byte-for-byte the same shape (only MainSandbox, 3-return-value assignment to sbRoute* vars, and the nil, nil, vs nil, nil, return arity match).

Fix shape: same pattern as above, one level up — a helper on *Translator returning (clusterName, basePath string, timeout *resolvedTimeout, err error):

func (t *Translator) resolvePerOpOverride(kind, apiID, label string, target *api.RestAPIOperationUpstreamTarget, defs *[]api.UpstreamDefinition, method, path string) (string, string, *resolvedTimeout, error) {
    if target == nil {
        return "", "", nil, nil
    }
    name, basePath, timeout, err := t.resolvePerOpDefinitionCluster(kind, apiID, &target.Ref, defs)
    if err != nil {
        return "", "", nil, fmt.Errorf("per-op %s upstream for %s %s: %w", label, method, path, err)
    }
    return name, basePath, timeout, nil
}

This does not touch resolvePerOpDefinitionCluster itself or any cluster-name output — it only wraps the two call sites that dispatch into it, so byte-identity is preserved.


3. Minor — trim+empty-ref check repeated across three sites (low value to dedupe, noting for completeness)

  • api_validator.go Line 271 (validateUpstreamRefexcluded, predates PR, not counted)
  • api_validator.go Line 722–723 (validateOperationUpstreamRef, new)
  • upstreamref.go Line 38–39 (FindByName, new)

Lines 722–723 and 38–39 both do strings.TrimSpace(ref) == "" as their very first check. This is genuinely two occurrences in new code, but they return different types (ValidationError vs. error) for different callers, so a shared helper would need an awkward signature to serve both. I'm flagging it for visibility only — not proposing an extraction, since the two-line pattern is trivial and the abstraction cost would likely exceed the benefit.


Nothing here changes any cluster-name output, clusterkey.DefinitionName, the perOpDefinitionClusterKey/resolvePerOpDefinitionCluster pair itself, or the policy-engine kernel mirror — findings 1 and 2 only touch the dispatch code that calls into those already-shared resolvers.

@mehara-rothila

Copy link
Copy Markdown
Contributor Author

@coderabbitai we're renaming the OperationUpstream schema in gateway/gateway-controller/api/management-openapi.yaml (the per-operation {main, sandbox} upstream override, each sub-field a ref to a named upstreamDefinitions entry). The same schema exists identically in the platform-api OpenAPI, so the chosen name must work on both sides.

Our candidates:

  1. OperationUpstream (current)
  2. OperationUpstreamOverride
  3. PerOperationUpstream
  4. OperationUpstreamRef
  5. OperationLevelUpstream

Please (a) add any stronger options we've missed, then (b) rank ALL of them (ours + yours) best-to-worst with a one-line rationale each. Judge by: clarity of intent (it overrides the API-level upstream for a single operation), consistency with the sibling schemas (Upstream, UpstreamDefinition, UpstreamReference, UpstreamTimeout), and that it works as a schema name on both the gateway and platform-api sides.

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@mehara-rothila — here's the naming analysis.

(a) Additional candidates worth considering

  • OperationUpstreamOverride (you already have this one — it's my top pick, see below)
  • OperationUpstreamSelector — frames it as "selecting" a definition per host, but undersells that it's overriding the API-level default.
  • OperationUpstreamBinding — technically accurate (binding an operation to a definition) but "binding" isn't used elsewhere in this schema family, so it'd be a new vocabulary word for no real gain.
  • OperationBackendOverride — swaps "upstream" for "backend"; rejected because every sibling schema (Upstream, UpstreamDefinition, UpstreamReference, UpstreamTimeout) uses "Upstream" consistently, and introducing "Backend" as a synonym would fragment vocabulary across the same OpenAPI doc.

(b) Full ranking, best to worst

Rank Name Rationale
1 OperationUpstreamOverride Names the exact behavior (overrides the API-level upstream for one operation), keeps the Upstream root shared with UpstreamDefinition/UpstreamReference/UpstreamTimeout, and is unambiguous on both gateway and platform-api sides since it doesn't reference any gateway-specific concept.
2 PerOperationUpstream Clear and readable, but "Per-" as a prefix is a less common OpenAPI schema-naming convention than a trailing qualifier, and it doesn't explicitly say "override" — a reader could momentarily wonder if it's a full upstream definition rather than a pointer to one.
3 OperationLevelUpstream Same clarity as #2 but noticeably more verbose for no added precision; "Level" doesn't pull its weight as a word.
4 OperationUpstream (current) Grammatically fine and short, but ambiguous — nothing in the name signals it's ref-only and override-only (as opposed to a full inline upstream config like the API-level Upstream schema). This ambiguity is exactly the kind of confusion a rename should resolve.
5 OperationUpstreamSelector (new) Reasonable but weaker than "Override" — "selector" implies choosing among several, not superseding a default.
6 OperationUpstreamRef Actively misleading: the schema wraps two independent ref-only sub-targets (main, sandbox), it is not itself a single reference. Using "Ref" in the container name invites confusion with RestAPIOperationUpstreamTarget (the actual per-target ref holder) or the shared UpstreamReference string schema.
7 OperationUpstreamBinding (new) Accurate but introduces new terminology with no precedent elsewhere in the spec; adds a vocabulary word for no clarity gain over "Override".

Recommendation: go with OperationUpstreamOverride. It reads unambiguously as "this operation's upstream, overriding the API-level one," keeps the shared Upstream root consistent with its sibling schemas, and requires no gateway/platform-api-specific context to understand — which satisfies your "works on both sides" constraint directly.

@mehara-rothila

Copy link
Copy Markdown
Contributor Author

@coderabbitai please do an equivalence audit of this PR's recent rework.

Compare commit dac1f56 (the earlier verified state of this branch) against the current head 38958c3 and confirm the observable behavior is unchanged: for the same deployed API, identical routing results (which backend each operation reaches on main and sandbox vhosts, per-op ref reuse of definition clusters, fallbacks), identical path rewriting (definition basePath handling), identical timeout propagation, identical hostRewrite behavior, identical precedence (op-policy > api-policy > per-op ref > api-level upstream), and an identical validation accept/reject set.

The following changes in that range are INTENTIONAL, please verify they are implemented consistently but do not report them as regressions:

  1. API-level cluster names moved from a truncated 24-hex hash to the full sha256(apiID) hex via clusterkey.Hash/HashedName; the hash input is still only the API id (URL-stable), main/sandbox share the fragment, and both xDS builders use the shared helper. Definition cluster names (clusterkey.DefinitionName) are unchanged.
  2. OpenAPI schema renames: RestAPIOperationUpstream / RestAPIOperationUpstreamTarget consolidated into OperationUpstream with inline main/sandbox; the wire payload (operations[].upstream.main.ref etc.) and constraints (required ref, additionalProperties false, minProperties 1) are unchanged.
  3. Validator refactors: the name contract (max 100, ^[a-zA-Z0-9-_]+$) centralized into validateUpstreamRefName; error message labels now derive from the field path (wording changed, outcomes did not).
  4. Sandbox detection consolidated into upstreamref.HasContent/SandboxActive (the xDS translator moved from nil-check to content-check, aligning with the transformer; unreachable for validator-passing inputs).
  5. The latest commit consolidates the per-op override glue into routeSlot (transform) and routeParams (xDS) with a shared applyPerOpRef, changes the per-op ref parameters from *api.UpstreamReference to plain string (UpstreamReference is a string alias), drops a redundant Timeout nil-guard (the helper already returns nil for nil), and removes a duplicated nil-gate in the validator.

Please verify with file:line evidence and flag ONLY things outside this list: any route/cluster/policy-chain output that changed, any validation outcome that flipped, any error path lost, or any type/nil-safety issue introduced by the string-parameter change. If nothing is out of place, say explicitly that the range is behavior-preserving apart from the listed intentional changes.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "===VERIFY COMMITS EXIST==="
git cat-file -e dac1f564a 2>&1 && echo "base ok" || echo "base missing"
git cat-file -e 38958c316 2>&1 && echo "head ok" || echo "head missing"

echo ""
echo "===DIFF clusterkey.go==="
git diff dac1f564a 38958c316 -- gateway/gateway-controller/pkg/utils/clusterkey/clusterkey.go

echo ""
echo "===DIFF upstreamref.go==="
git diff dac1f564a 38958c316 -- gateway/gateway-controller/pkg/utils/upstreamref/upstreamref.go

echo ""
echo "===DIFF api_validator.go==="
git diff dac1f564a 38958c316 -- gateway/gateway-controller/pkg/config/api_validator.go > /tmp/validator.diff
wc -l /tmp/validator.diff
cat /tmp/validator.diff

echo ""
echo "===DIFF restapi.go==="
git diff dac1f564a 38958c316 -- gateway/gateway-controller/pkg/transform/restapi.go > /tmp/restapi.diff
wc -l /tmp/restapi.diff
cat /tmp/restapi.diff

echo ""
echo "===DIFF translator.go==="
git diff dac1f564a 38958c316 -- gateway/gateway-controller/pkg/xds/translator.go > /tmp/translator.diff
wc -l /tmp/translator.diff
cat /tmp/translator.diff

echo ""
echo "===DIFF management-openapi.yaml (upstream sections)==="
git diff dac1f564a 38958c316 -- gateway/gateway-controller/api/management-openapi.yaml

echo ""
echo "===DIFF generated.go (upstream types)==="
git diff dac1f564a 38958c316 -- gateway/gateway-controller/pkg/api/management/generated.go | head -300

Length of output: 50193


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "===resolveTimeoutFromDefinition definition (dac1f564a and 38958c316)==="
git show dac1f564a:gateway/gateway-controller/pkg/xds/translator.go | grep -n "func resolveTimeoutFromDefinition" -A 20
echo "---HEAD---"
git show 38958c316:gateway/gateway-controller/pkg/xds/translator.go | grep -n "func resolveTimeoutFromDefinition" -A 20

echo ""
echo "===check for any other diff hunks touching hostRewrite/timeout/precedence not yet reviewed==="
git diff dac1f564a 38958c316 --stat -- gateway/gateway-controller/pkg gateway/gateway-controller/api

echo ""
echo "===full diff of any remaining files under pkg (excluding tests) to be thorough==="
git diff dac1f564a 38958c316 --name-only -- gateway/gateway-controller/pkg gateway/gateway-controller/api | grep -v _test.go

Length of output: 9315


@mehara-rothila — audit complete. The range is behavior-preserving apart from the five listed intentional changes. No regressions found in routing, cluster reuse, basePath handling, timeout propagation, hostRewrite behavior, precedence, or validation outcomes.

Verification of each intentional change

1. clusterkey.Hash/HashedNameclusterkey.go Lines 31–39. APILevel/APILevelName (12-byte/24-hex truncated hash) removed; Hash/HashedName (full 32-byte/64-hex) added. Both xDS builders call the same helper with the same input shape (apiID-only, URL excluded):

  • restapi.go Line 387: clusterkey.HashedName(upstreamName, rdc.Metadata.UUID)
  • translator.go Line 1022: clusterkey.HashedName(upstreamName, apiID)

clusterkey.DefinitionName is untouched (only the file's top doc-comment wording changed). Confirmed consistent, no divergence.

2. OpenAPI consolidationmanagement-openapi.yaml Lines 4319–4341: RestAPIOperationUpstream/RestAPIOperationUpstreamTarget merged into OperationUpstream with inline main/sandbox objects. required: [ref], additionalProperties: false, minProperties: 1 all preserved verbatim. generated.go Lines 1246–1254: Main/Sandbox become anonymous *struct{ Ref UpstreamReference }Ref was already a non-pointer plain string in the old RestAPIOperationUpstreamTarget, so the "*api.UpstreamReferencestring" change you described introduces no pointer/nil-safety delta; both before and after, call sites pass op.Upstream.Main.Ref as a plain string.

3. Validator centralizationapi_validator.go Lines 238–260 (validateUpstreamRefName) is called from validateUpstreamRef (Line 282), the validateUpstreamDefinitions name loop (Line 336), and validateOperationUpstreamRef (Line 728). Checks (max-100, ^[a-zA-Z0-9\-_]+$) are byte-identical to the three inlined copies they replaced. Only the message text changed — from hardcoded "Upstream ref must..." / "Upstream definition name must..." to fieldName(field) + " must..." (trailing path segment, e.g. "ref must match pattern ..."). Field paths and pass/fail outcomes are unchanged.

4. upstreamref.HasContent/SandboxActiveupstreamref.go Lines 73–95. Logic is a direct extraction of the prior inline nil+TrimSpace checks that existed separately in restapi.go and translator.go; both call sites now share one implementation with the identical predicate.

5. routeSlot/routeParams + applyPerOpRef consolidation:

  • restapi.go Lines 160–192: sandboxSlot := mainSlot correctly reproduces the old sandboxVhostClusterKey := mainUpstream.ClusterKey seeding pattern before any per-op override is applied.
  • translator.go Line 823: useClusterHeader is now computed once before the operations loop instead of recomputed identically every iteration — this was loop-invariant in the old code (same apiData.UpstreamDefinitions check every time), so hoisting it is a pure no-op refactor.
  • resolvePerOpDefinitionCluster's dropped if definition.Timeout != nil guard: confirmed safe — resolveTimeoutFromDefinition (Line 3096 at head) already starts with if def == nil || def.Timeout == nil { return nil, nil }, so the outer guard was strictly redundant.
  • validateOperations' dropped if op.Upstream != nil guard: confirmed safe — validateOperationUpstream (the function it calls) has its own if up == nil { return errors } as its first line.

Nothing flagged outside the list

No route/cluster/policy-chain output changed, no validation outcome flipped, no error path lost, and no type/nil-safety issue from the string-parameter change (the field was never a pointer to begin with).

One scope note (not a regression, just flagging for accuracy)

The full dac1f564a..38958c316 diffstat touches ~72 files, including many with no connection to per-op-upstream (e.g., a gin→net/http handler-signature migration across pkg/api/handlers/*, pkg/controlplane/client.go, pkg/secrets/service.go, pkg/storage/sql_store.go, DB DDL files). This indicates the range also picked up an unrelated upstream merge/rebase, not just the per-op-upstream rework. Those files are outside your five-item list and outside this PR's stated objectives, so I did not audit them for equivalence — flagging only so the audit scope is clear if you intended to diff a narrower range.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

@mehara-rothila
mehara-rothila force-pushed the feat/per-op-upstream-gateway branch from 38958c3 to f3672a6 Compare July 10, 2026 05:36
@mehara-rothila

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 10, 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.

@mehara-rothila
mehara-rothila force-pushed the feat/per-op-upstream-gateway branch from 3a96a42 to dc1a079 Compare July 13, 2026 05:34
@mehara-rothila
mehara-rothila force-pushed the feat/per-op-upstream-gateway branch from dc1a079 to ca2f800 Compare July 13, 2026 05:37
@mehara-rothila

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@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: 2

🧹 Nitpick comments (2)
gateway/it/features/api-level-url-stable.feature (1)

220-259: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Scenario title doesn't match its content.

Title says "API-level upstream with cluster_header routing", but the scenario only exercises an API-level upstream.main.ref pointing at upstreamDefinitions — there's no cluster_header config or assertion anywhere in the scenario. This looks like leftover/copy-paste wording that will confuse future maintainers about what's actually covered.

✏️ Suggested rename
-  Scenario: API-level upstream with cluster_header routing (default upstream cluster resolves correctly)
+  Scenario: API-level upstream ref resolves to the referenced upstreamDefinitions entry
🤖 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/it/features/api-level-url-stable.feature` around lines 220 - 259,
Rename the scenario title to describe the API-level upstream using the default
upstream cluster resolution via upstreamDefinitions and upstream.main.ref.
Remove the inaccurate “cluster_header routing” wording while leaving the
scenario steps unchanged.
gateway/gateway-controller/pkg/config/api_validator.go (1)

41-43: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Narrow this comment or reuse the shared helper

upstreamRefRegex only backs the API/per-op ref checks. Definition names still use the separate upstreamDefinitionNameRegex and its own messages, so this doc comment overstates the field’s scope and leaves two copies of the same rule to drift. Either route definition-name validation through validateUpstreamRefName too, or trim the comment to refs only.

🤖 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/config/api_validator.go` around lines 41 - 43,
Trim the comment for upstreamRefRegex to describe only API-level and
per-operation upstream reference validation. Keep upstreamDefinitionNameRegex
and its distinct validation messages unchanged.
🤖 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/upstreamref/upstreamref.go`:
- Around line 53-72: Update ParseConnectTimeout to accept a zero duration such
as 0s, while continuing to reject negative durations and invalid formats. Change
the non-positive duration validation in ParseConnectTimeout to enforce only the
shared contract’s disallowed values.

In `@gateway/it/features/api-level-url-stable.feature`:
- Around line 450-480: The API update scenario should wait for configuration
propagation before inspecting Envoy clusters. After the successful update in the
scheme/port scenario, add the same endpoint-readiness wait used by the other
update scenarios; if that endpoint cannot become ready, replace it with a short
retry around the `/clusters` check and document the exception.

---

Nitpick comments:
In `@gateway/gateway-controller/pkg/config/api_validator.go`:
- Around line 41-43: Trim the comment for upstreamRefRegex to describe only
API-level and per-operation upstream reference validation. Keep
upstreamDefinitionNameRegex and its distinct validation messages unchanged.

In `@gateway/it/features/api-level-url-stable.feature`:
- Around line 220-259: Rename the scenario title to describe the API-level
upstream using the default upstream cluster resolution via upstreamDefinitions
and upstream.main.ref. Remove the inaccurate “cluster_header routing” wording
while leaving the scenario steps unchanged.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7434504b-6d83-434d-bd29-35fb352d1c74

📥 Commits

Reviewing files that changed from the base of the PR and between 0143658 and ca2f800.

📒 Files selected for processing (22)
  • gateway/gateway-controller/api/management-openapi.yaml
  • gateway/gateway-controller/pkg/api/management/generated.go
  • gateway/gateway-controller/pkg/config/api_validator.go
  • gateway/gateway-controller/pkg/config/validator_test.go
  • gateway/gateway-controller/pkg/transform/llm_test.go
  • gateway/gateway-controller/pkg/transform/restapi.go
  • gateway/gateway-controller/pkg/transform/restapi_test.go
  • gateway/gateway-controller/pkg/utils/clusterkey/clusterkey.go
  • gateway/gateway-controller/pkg/utils/clusterkey/clusterkey_test.go
  • gateway/gateway-controller/pkg/utils/upstreamref/upstreamref.go
  • gateway/gateway-controller/pkg/utils/upstreamref/upstreamref_test.go
  • gateway/gateway-controller/pkg/xds/translator.go
  • gateway/gateway-controller/pkg/xds/translator_test.go
  • gateway/gateway-controller/tests/integration/storage_test.go
  • gateway/gateway-runtime/policy-engine/internal/kernel/translator_test.go
  • gateway/it/features/api-level-url-stable.feature
  • gateway/it/features/per-op-upstream-basic.feature
  • gateway/it/features/per-op-upstream-ref.feature
  • gateway/it/features/per-op-upstream-validation.feature
  • gateway/it/steps_envoy_admin.go
  • gateway/it/suite_test.go
  • gateway/spec/impls/1-basic-gateway-with-controller/data-model.md

Comment thread gateway/gateway-controller/pkg/utils/upstreamref/upstreamref.go
Comment thread gateway/it/features/api-level-url-stable.feature
@mehara-rothila
mehara-rothila force-pushed the feat/per-op-upstream-gateway branch from f707aad to afa9253 Compare July 13, 2026 06:14
…ted contract

The management validator deliberately accepts a zero connect timeout and enforces only the duration format, so deploying a definition with connect 0s returns 201. The scenario previously expected a 400 from the stricter pre-rebase contract and failed once CI ran the suite against the merged main.
…gateway

# Conflicts:
#	gateway/gateway-controller/pkg/api/management/generated.go
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.

5 participants