Skip to content

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

Merged
tharindu1st merged 9 commits into
wso2:feature/operation-level-epfrom
mehara-rothila:feat/per-op-upstream-gateway
Jul 31, 2026
Merged

Add per-operation upstream override for REST API operations#2012
tharindu1st merged 9 commits into
wso2:feature/operation-level-epfrom
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

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e6a8b117-3bb9-40a7-a0ce-f372614e7c76

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

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, shared validation and lookup utilities, deterministic API-scoped cluster names, REST/xDS routing support, persistence coverage, runtime tests, integration scenarios, and updated cluster identity documentation.

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 types 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 naming, upstream lookup, timeout parsing, content detection, and sandbox activation helpers with unit tests.
Definition and per-operation validation
gateway/gateway-controller/pkg/config/api_validator.go, gateway/gateway-controller/pkg/config/validator_test.go
Validates shared reference rules, definition existence, operation-level overrides, timeout formats, and operation-scoped errors.
REST transformer route wiring
gateway/gateway-controller/pkg/transform/restapi.go, gateway/gateway-controller/pkg/transform/restapi_test.go
Routes per-operation overrides to definition clusters, preserves API-level fallback, handles sandbox routes, and applies base-path and host-rewrite behavior.
XDS translation and cluster resolution
gateway/gateway-controller/pkg/xds/translator.go, gateway/gateway-controller/pkg/xds/translator_test.go
Uses API-scoped hashed cluster names, reuses definition clusters, propagates timeouts and base paths, and enables dynamic cluster-header routing.
Persistence, runtime behavior, and end-to-end coverage
gateway/gateway-controller/tests/integration/storage_test.go, gateway/gateway-runtime/policy-engine/internal/kernel/translator_test.go, gateway/gateway-controller/pkg/transform/llm_test.go, gateway/it/*, gateway/spec/impls/1-basic-gateway-with-controller/data-model.md
Covers storage round trips, dynamic-endpoint metadata, LLM route identity, Envoy cluster stability scenarios, and updated cluster naming documentation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • wso2/api-platform#2783: Overlaps with connect-timeout parsing and propagation for upstream definitions and generated runtime clusters.

Suggested reviewers: renuka-fernando, krishanx92, anugayan, lasanthas, thushani-jayasekera

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant APIValidator
  participant RESTTransformer
  participant XDSTranslator
  participant Envoy
  Client->>APIValidator: Submit operation upstream reference
  APIValidator->>RESTTransformer: Accept validated configuration
  RESTTransformer->>XDSTranslator: Pass referenced cluster and base path
  XDSTranslator->>Envoy: Emit cluster-header route
  Envoy-->>Client: Route request to referenced upstream
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.60% 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
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.
Title check ✅ Passed The title clearly names the main change: per-operation upstream overrides for REST API operations.
Description check ✅ Passed Covers purpose, goals, approach, tests, and examples well; only template sections like security, related PRs, and test environment are missing.
✨ 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
O-sura
O-sura previously approved these changes Jul 28, 2026
@mehara-rothila

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 28, 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[bot]
coderabbitai Bot previously approved these changes Jul 28, 2026
@mehara-rothila

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

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

🧹 Nitpick comments (2)
gateway/gateway-controller/pkg/config/api_validator.go (2)

34-54: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Two parallel regexes enforce the same UpstreamReference contract.

upstreamRefRegex (used by validateUpstreamRefName for API-level and per-op refs) and the pre-existing upstreamDefinitionNameRegex (used inline in validateUpstreamDefinitionsList, ~line 300) both encode ^[a-zA-Z0-9\-_]+$, but are separate compiled regexes with separate, differently-worded error messages ("must not exceed 100 characters" / "must match pattern ..." vs. "must be 1-100 characters" / "must match ^[a-zA-Z0-9\-_]+$ ..."). This is exactly the kind of contract-drift risk a prior review flagged for this ref/name pair — if one pattern is updated later, the other can silently diverge.

Consider having validateUpstreamDefinitionsList call validateUpstreamRefName for def.Name too, so there is a single source of truth for the ref/name contract (accepting the resulting message-text change, and updating TestValidateUpstreamDefinitions_NameRules's expected substrings accordingly).

🤖 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 34 - 54,
Update validateUpstreamDefinitionsList to validate each def.Name through
validateUpstreamRefName instead of applying upstreamDefinitionNameRegex inline,
making upstreamRefRegex the single source of truth for upstream reference and
definition-name validation. Remove the redundant definition-name validation path
if no longer needed, and update TestValidateUpstreamDefinitions_NameRules
expected error substrings to match the shared validator messages.

436-447: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused upstreamRefResolves helper

upstreamRefResolves is no longer referenced in the repository, and its former MCP/LLM callers can move to the shared upstreamref.FindByName path already used by validateUpstreamRef.

🤖 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 436 -
447, Remove the unused upstreamRefResolves helper from api_validator.go. Update
its former MCP/LLM callers to use the shared upstreamref.FindByName path already
used by validateUpstreamRef, preserving the existing upstream-name resolution
behavior.
🤖 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/config/api_validator.go`:
- Around line 34-54: Update validateUpstreamDefinitionsList to validate each
def.Name through validateUpstreamRefName instead of applying
upstreamDefinitionNameRegex inline, making upstreamRefRegex the single source of
truth for upstream reference and definition-name validation. Remove the
redundant definition-name validation path if no longer needed, and update
TestValidateUpstreamDefinitions_NameRules expected error substrings to match the
shared validator messages.
- Around line 436-447: Remove the unused upstreamRefResolves helper from
api_validator.go. Update its former MCP/LLM callers to use the shared
upstreamref.FindByName path already used by validateUpstreamRef, preserving the
existing upstream-name resolution behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6e1adef5-8064-4f6a-a288-91eab9c5da26

📥 Commits

Reviewing files that changed from the base of the PR and between 833cbd6 and bde4caf.

📒 Files selected for processing (9)
  • 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/xds/translator.go
  • gateway/gateway-controller/pkg/xds/translator_test.go
  • gateway/gateway-runtime/policy-engine/internal/kernel/translator_test.go
  • gateway/it/features/per-op-upstream.feature
  • gateway/it/suite_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • gateway/gateway-runtime/policy-engine/internal/kernel/translator_test.go
  • gateway/gateway-controller/pkg/api/management/generated.go
  • gateway/gateway-controller/pkg/xds/translator.go
  • gateway/gateway-controller/pkg/xds/translator_test.go

@mehara-rothila
mehara-rothila requested a review from O-sura July 29, 2026 22:12
@tharindu1st
tharindu1st changed the base branch from main to feature/operation-level-ep July 30, 2026 06:13
…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.
…ne feature file

Merge per-op-upstream-basic, per-op-upstream-ref, per-op-upstream-validation, and api-level-url-stable into a single features/per-op-upstream.feature and register it once in suite_test.go. All 26 scenarios are preserved verbatim; the four files already ran together in the same suite, so this is behavior-neutral.
Add scenarios proving per-operation upstream refs apply to Gateway-API-style match operations (method + path.value + header matchers): a main ref on a match operation with a sibling match op falling back to the API-level upstream, a header matcher selecting the per-op ref on a shared path, and a sandbox ref on a match operation. Confirms the transform resolves the route from the match block and still applies the per-operation cluster.
Note that the ref pattern backs only API-level and per-operation upstream refs,
and that definition names are validated separately, so the two checks are not
mistaken for one shared rule.
The management validator accepted a zero connect timeout while the transformer
requires a positive value, so a definition carrying "0s" was accepted at deploy
time and then failed when the deployment was translated. Because runtime configs
are rebuilt from stored configurations on startup, such a definition also stopped
the controller from starting again.

Validate the connect timeout as positive alongside the existing format check, and
cover the rejection in the validator unit test and the integration scenario.
@mehara-rothila
mehara-rothila force-pushed the feat/per-op-upstream-gateway branch from bde4caf to c400a6f Compare July 31, 2026 08:24
@tharindu1st
tharindu1st merged commit 485272a into wso2:feature/operation-level-ep Jul 31, 2026
2 checks passed
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.

7 participants