Skip to content

feat(platform-api): support upstreamDefinitions pool and per-operation upstream overrides#2155

Open
mehara-rothila wants to merge 8 commits into
wso2:mainfrom
mehara-rothila:feat/platform-api-upstream-definitions
Open

feat(platform-api): support upstreamDefinitions pool and per-operation upstream overrides#2155
mehara-rothila wants to merge 8 commits into
wso2:mainfrom
mehara-rothila:feat/platform-api-upstream-definitions

Conversation

@mehara-rothila

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

Copy link
Copy Markdown
Contributor

Purpose

Add control-plane support for reusable upstream definitions and per-operation upstream routing on REST APIs, so individual operations of an API can route to different backends. This is the platform-api counterpart of the gateway-side change in PR #2012 (per-operation upstream routing in the gateway controller), which this PR pairs with: the platform stores, validates, and emits the configuration; the gateway enforces and routes it.

Approach

  • Schema: RESTAPI (and the create/update requests) gain an optional upstreamDefinitions pool of named, reusable upstreams (name, basePath, timeout.connect, weighted upstreams[]), and each operation gains an optional upstream override whose main/sandbox reference a pool entry by name (ref only, no inline URLs). The API-level upstream.main/upstream.sandbox become a url-or-ref union. Field names and constraints mirror the gateway's management OpenAPI contract exactly.
  • No DB migration: the new fields ride in the existing rest_apis.configuration JSON blob.
  • Validation (validateUpstreamRefs, run on create and on the merged config on update): definition names must be unique and match the shared UpstreamReference contract (1-100 chars, ^[a-zA-Z0-9\-_]+$); definition URLs must be http/https host[:port] only (no path, query, or fragment); basePath must start with / and must not end with / (same regex as the gateway validator; empty is treated as unset since omitempty drops it from the deployment YAML); weights must be 0-100; timeout.connect must be a single unsigned ms/s/m/h unit and positive; every API-level and per-operation ref must resolve to a declared definition; the API-level union must set exactly one of url or ref. Violations map to HTTP 400.
  • Deployment YAML: definitions and per-operation refs are emitted with the exact field names and casing the gateway parses; omitted optionals are omitted, zero weights are preserved. WebSub artifacts never emit definitions.
  • Update semantics: partial updates merge first, then revalidate, so removing a pool entry that a stored operation still references is rejected rather than persisted as a dangling ref.

Example

A REST API with a reusable pool, an API-level default upstream (url-or-ref), and one operation overridden to a pool entry:

upstream:
  main:
    url: http://main:8080          # API-level default (a direct url, or a ref into the pool)

upstreamDefinitions:                # reusable, named upstream pool
  - name: alt-backend
    basePath: /api/v2
    timeout:
      connect: 5s
    upstreams:
      - url: http://alt:9090        # host and port only; path comes from basePath
        weight: 80

operations:
  - method: GET
    path: /whoami
    upstream:
      main:
        ref: alt-backend            # this operation routes to alt-backend
  - method: GET
    path: /ping                     # no override, falls back to the API-level upstream

Validation parity with the gateway

Independent cross-checks of this branch against the gateway branch confirmed the accept/reject sets match for names, refs, URLs, weights, basePath, and timeout format, with two deliberate, safe differences: the platform rejects non-positive connect timeouts such as 0s (the gateway's management validation accepts them but its translation layer then fails, so the platform enforces the effective contract), and the platform rejects an API-level upstream with neither url nor ref (previously accepted and then failed at deploy). A known gateway-internal item (weighted targets applying only on one translation path) is pre-existing on main and tracked separately.

Testing

  • Unit: validateUpstreamRefs covers names, duplicates, URLs (path/query/fragment/scheme/host), weights (0, 100, out-of-range), basePath (leading/trailing slash, bare /, space, query, empty-as-unset), timeout formats, and ref resolution for API-level and per-operation targets.
  • Handler integration (real route/handler/service/repository stack over SQLite): unresolved refs and invalid basePath return 400 on create; a valid create returns 201; an update that dangles a stored ref returns 400.
  • Deployment YAML round-trip tests for definitions, per-operation refs, WebSub omission, and model round-trip.

Note on strictness

The service now rejects an API-level upstream with neither url nor ref on create and on the merged configuration of updates, closing a previously accepted configuration that could only fail at deploy time. Practical HTTP behavior is unchanged, since the handlers already required at least one upstream endpoint on both create and update.

@mehara-rothila mehara-rothila changed the title feat(platform-api): add upstreamDefinitions pool to the REST API Open… feat(platform-api): support API-level upstreamDefinitions pool Jun 11, 2026
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This change adds reusable upstream definitions and per-operation upstream overrides across the OpenAPI contract, generated API models, internal models, YAML DTOs, service validation, API handlers, conversion utilities, repository persistence, and deployment YAML generation. Upstream definitions support named backend pools, optional base paths, weighted targets, and connect timeouts. Operations can reference named definitions for main and sandbox upstreams. Create and update flows validate definitions and references, while tests cover conversion, merging, persistence, and deployment output.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant APIHandler
  participant APIService
  participant Repository
  participant DeploymentYAML
  Client->>APIHandler: submit API with upstream definitions
  APIHandler->>APIService: create or update request
  APIService->>APIService: validate definitions and operation references
  APIService->>Repository: persist validated configuration
  Repository-->>APIService: stored API configuration
  APIService->>DeploymentYAML: build deployment specification
  DeploymentYAML-->>Client: upstream pools and operation references
Loading

Suggested reviewers: RakhithaRR, Tharsanan1, VirajSalaka, renuka-fernando, AnuGayan, chamilaadhi, Arshardh, dushaniw, tgtshanika, senthuran16, CrowleyRajapakse, hisanhunais, HiranyaKavishani, ashera96, pubudu538

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 81.25% which is sufficient. The required threshold is 80.00%.
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 summarizes the main change: reusable upstream pools and per-operation overrides.
Description check ✅ Passed The description is detailed and covers purpose, approach, validation, examples, and testing; some template sections are omitted.
✨ 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.

@mehara-rothila

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@platform-api/src/resources/openapi.yaml`:
- Around line 8455-8464: UpstreamTimeout.connect's OpenAPI regex only allows a
single unit but the runtime uses time.ParseDuration (see
spec.upstreamDefinitions[%d].timeout.connect), so update the OpenAPI schema for
UpstreamTimeout.connect to either (A) broaden the pattern to match Go
time.ParseDuration format (allow multiple sequential value+unit segments,
decimals, and units like ns|us|µs|ms|s|m|h, optional leading minus) or (B)
explicitly document/validate that only single-unit durations are accepted;
locate the UpstreamTimeout.connect property in the OpenAPI file and change the
pattern or description accordingly.
- Around line 8427-8447: Update the ReusableUpstream schema so its basePath
property is constrained to a path-only form and each upstream.url is constrained
to host[:port]-only (no path/query/fragment) to match gateway validation:
tighten basePath (property name basePath) to require a leading "/" and a path
pattern (e.g., path segment characters only) and change
upstreams.items.properties.url to validate host[:port] (e.g., restrict
format/pattern to host with optional port) instead of generic uri; keep timeout
($ref: '`#/components/schemas/UpstreamTimeout`') 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c978ac83-b305-4917-85d3-8826d792c30e

📥 Commits

Reviewing files that changed from the base of the PR and between 66e4674 and e6e8d18.

📒 Files selected for processing (1)
  • platform-api/src/resources/openapi.yaml

Comment thread platform-api/resources/openapi.yaml
Comment thread platform-api/resources/openapi.yaml
@renuka-fernando
renuka-fernando changed the base branch from main to feature/operation-level-ep June 12, 2026 11:52
@renuka-fernando renuka-fernando self-assigned this Jun 12, 2026
Comment thread platform-api/resources/openapi.yaml
Comment thread platform-api/resources/openapi.yaml
Comment thread platform-api/src/resources/openapi.yaml Outdated
@mehara-rothila mehara-rothila changed the title feat(platform-api): support API-level upstreamDefinitions pool feat(platform-api): support upstreamDefinitions pool and per-operation upstream overrides Jul 7, 2026
@mehara-rothila
mehara-rothila force-pushed the feat/platform-api-upstream-definitions branch 2 times, most recently from e27ea92 to 3cb7216 Compare July 7, 2026 15:57
@mehara-rothila

Copy link
Copy Markdown
Contributor Author

@coderabbitai a naming sanity-check on the upstream schemas in this PR, please.

Context: this PR (#2155) is the platform-api / control-plane side of the per-operation upstream feature. Its gateway / data-plane companion is #2012 (#2012), which adds the matching schemas to gateway/gateway-controller/api/management-openapi.yaml. The two sides must use matching schema shapes and, ideally, matching names. Please review the naming with both in mind.

Current state in platform-api/src/resources/openapi.yaml (this PR):

  • Upstream , the {main, sandbox} wrapper, shared by RESTAPI, LLMProvider, MCPServer and WebSub, so it cannot be renamed.
  • UpstreamDefinition , a single endpoint (url or ref + auth), used only by Upstream.main/sandbox.
  • ReusableUpstream , a named pool entry (name, basePath, timeout, upstreams[]), held by the new upstreamDefinitions array.

How the gateway side names the same things:

The clash: our field is upstreamDefinitions, but its items are ReusableUpstream, while a different schema here is named UpstreamDefinition. The gateway's pool name (UpstreamDefinition) is already taken on our side by the endpoint schema.

Proposed rename, keeping Upstream as-is:

  • ReusableUpstream -> UpstreamDefinition (so the upstreamDefinitions items match both the field name and the gateway pool name)
  • endpoint UpstreamDefinition -> UpstreamEndpoint

Please advise:

  1. Is this the clearest naming, or do you have a better suggestion?
  2. Any $ref, collision, or backward-compatibility risk we are missing?
  3. Does this align the control-plane (feat(platform-api): support upstreamDefinitions pool and per-operation upstream overrides #2155) and data-plane (Add per-operation upstream override for REST API operations #2012) naming as closely as the shared Upstream wrapper allows?

@coderabbitai

coderabbitai Bot commented Jul 8, 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 "== Find the OpenAPI files =="
fd -p 'openapi.yaml|management-openapi.yaml'

echo
echo "== Outline current platform-api upstream-related schema names =="
rg -n -C2 '^\s{4}(Upstream|UpstreamDefinition|ReusableUpstream|UpstreamTimeout|RESTAPI):|upstreamDefinitions:|`#/components/schemas/`(Upstream|UpstreamDefinition|ReusableUpstream|UpstreamTimeout)\b' \
  platform-api/src/resources/openapi.yaml || true

echo
echo "== Read the relevant platform-api slice =="
start=$(rg -n '^\s{4}RESTAPI:' platform-api/src/resources/openapi.yaml | cut -d: -f1 | head -n1)
end=$(rg -n '^\s{4}UpstreamTimeout:' platform-api/src/resources/openapi.yaml | cut -d: -f1 | head -n1)
if [[ -n "${start:-}" && -n "${end:-}" ]]; then
  sed -n "$((start-10)),$((end+80))p" platform-api/src/resources/openapi.yaml | nl -ba
fi

echo
echo "== Current branch: all refs to UpstreamDefinition / ReusableUpstream in platform-api =="
rg -n -C2 '`#/components/schemas/`(UpstreamDefinition|ReusableUpstream)\b|^\s{4}(UpstreamDefinition|ReusableUpstream):' platform-api/src/resources/openapi.yaml || true

echo
echo "== Companion PR `#2012`: inspect gateway management-openapi upstream naming =="
gh pr diff 2012 --repo wso2/api-platform -- gateway/gateway-controller/api/management-openapi.yaml | \
  rg -n -C3 'UpstreamDefinition|UpstreamReference|OperationUpstream|upstreamDefinitions|timeout|basePath|main:|sandbox:' || true

echo
echo "== Companion PR `#2012` metadata =="
gh pr view 2012 --repo wso2/api-platform --json number,title,headRefName,baseRefName,state,url

Length of output: 4416


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== platform-api: RESTAPI and upstream schema slice =="
sed -n '6318,8498p' platform-api/src/resources/openapi.yaml

echo
echo "== platform-api: precise refs/usages of UpstreamDefinition and ReusableUpstream =="
rg -n -C2 '`#/components/schemas/`(UpstreamDefinition|ReusableUpstream)\b|^\s{4}(UpstreamDefinition|ReusableUpstream):' \
  platform-api/src/resources/openapi.yaml

echo
echo "== gateway companion PR `#2012`: files changed =="
gh pr view 2012 --repo wso2/api-platform --json files \
  --jq '.files[].path'

echo
echo "== gateway companion PR `#2012`: upstream-related diff excerpt from management-openapi.yaml =="
gh pr diff 2012 --repo wso2/api-platform | \
  sed -n '/gateway\/gateway-controller\/api\/management-openapi.yaml/,$p' | \
  rg -n -C4 'UpstreamDefinition|UpstreamReference|OperationUpstream|upstreamDefinitions|`#/components/schemas/`' || true

echo
echo "== gateway main branch: current upstream naming in management-openapi.yaml =="
rg -n -C3 '^\s{4}(UpstreamDefinition|UpstreamReference|OperationUpstream|API|RESTAPI):|upstreamDefinitions:|`#/components/schemas/`(UpstreamDefinition|UpstreamReference|OperationUpstream)\b' \
  gateway/gateway-controller/api/management-openapi.yaml || true

Length of output: 131926


@mehara-rothila I think your proposed rename is the clearest option.

  1. Recommended naming
    I would go with exactly this:

    • ReusableUpstream -> UpstreamDefinition
    • current endpoint-level UpstreamDefinition -> UpstreamEndpoint

    That gives a cleaner model:

    • Upstream = API-level {main, sandbox} wrapper
    • OperationUpstream = per-operation {main, sandbox} wrapper
    • UpstreamEndpoint = a single direct target or ref (url / ref / auth)
    • UpstreamDefinition = a named reusable pool entry in upstreamDefinitions
    • UpstreamReference = the reusable name token
    • UpstreamTimeout = timeout block used by the reusable definition

    Why this is better:

    • In this PR, upstreamDefinitions currently contains ReusableUpstream (Line 6329-Line 6333), while UpstreamDefinition means something else entirely (Line 8376 onward). That is the main source of confusion.
    • The endpoint-level schema is only used under Upstream.main / Upstream.sandbox (Line 8372, Line 8374), so UpstreamEndpoint is a more accurate name.
    • On the gateway side, upstreamDefinitions.items already points to UpstreamDefinition (Line 4155-Line 4159), so this change aligns the pool-entry concept across control plane and data plane.
  2. $ref, collision, and compatibility risk
    I do not see a structural $ref risk as long as all local refs are updated consistently.

    The concrete updates would be:

    • RESTAPI.upstreamDefinitions.items:
      • #/components/schemas/ReusableUpstream -> #/components/schemas/UpstreamDefinition
    • Upstream.main and Upstream.sandbox:
      • #/components/schemas/UpstreamDefinition -> #/components/schemas/UpstreamEndpoint
    • rename the component keys themselves accordingly
    • update descriptions that still say “ReusableUpstream” to say “UpstreamDefinition”
    • keep UpstreamReference pointing at the reusable-definition name contract

    Compatibility caveat:

    • Wire format: no payload-field change, so API JSON/YAML shape stays the same.
    • Generated code/docs: yes, schema symbol names will change in generated types, DTO names, docs, and any downstream code that refers to component names rather than payload fields. Since this PR is still draft and explicitly schema-only, this is the best time to do it, before regeneration and follow-up wiring land.
  3. Alignment with #2012
    Yes — this is probably the closest practical alignment possible given the shared Upstream wrapper on the platform-api side.

    After the rename, the mapping would be coherent across both sides:

    • reusable pool entry: UpstreamDefinition on both sides
    • reference token: UpstreamReference on both sides
    • per-operation wrapper: OperationUpstream on both sides
    • API-level wrapper: only platform-api has Upstream, which is fine and reflects the existing shared model constraint

    So the only unavoidable naming divergence would be the presence of Upstream in platform-api, not the reusable-definition terminology.

Bottom line: I recommend making the rename now. It reduces local ambiguity and brings the control-plane terminology into line with the gateway model.

One small follow-up suggestion: if you do this rename, also update the UpstreamReference description from “Name of a ReusableUpstream entry...” to “Name of an UpstreamDefinition entry...”, so the docs stay consistent.


✏️ Learnings added
Learnt from: mehara-rothila
URL: https://github.com/wso2/api-platform/pull/2155

Timestamp: 2026-07-08T01:21:26.025Z
Learning: In `platform-api/src/resources/openapi.yaml`, the shared `Upstream` `{main, sandbox}` wrapper is reused by multiple artifact types and should remain unchanged, while naming for the reusable upstream pool should align with the gateway model from pull request `#2012`: use `UpstreamDefinition` for entries in `upstreamDefinitions`, `UpstreamReference` for the ref-name token, `OperationUpstream` for per-operation overrides, and rename the current endpoint-level `UpstreamDefinition` leaf schema to `UpstreamEndpoint`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@mehara-rothila

Copy link
Copy Markdown
Contributor Author

@coderabbitai we're renaming the ReusableUpstream schema in platform-api/src/resources/openapi.yaml (the pool entry held by upstreamDefinitions: name, basePath, timeout, weighted upstreams[]). The gateway-controller names this same concept UpstreamDefinition. Note platform-api already uses UpstreamDefinition for a different schema (the url/ref endpoint under Upstream.main/sandbox), so adopting UpstreamDefinition for the pool would rename that endpoint schema to UpstreamEndpoint.

Our candidates:

  1. UpstreamDefinition (matches the gateway pool name; requires endpoint -> UpstreamEndpoint)
  2. ReusableUpstream (current)
  3. NamedUpstream
  4. UpstreamPoolEntry
  5. SharedUpstream

Please (a) add any stronger options, then (b) rank ALL of them best-to-worst with a one-line rationale each. Judge by: cross-plane consistency with the gateway (which calls this UpstreamDefinition), clarity, and fit with the sibling schemas (Upstream, UpstreamReference, OperationUpstream, UpstreamTimeout).

@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 ranking, including two additional candidates for completeness.

Additional candidates to consider:
6. PooledUpstream — conveys "member of a reusable pool" while keeping the two-word pattern used by every sibling schema.
7. UpstreamPool — rejected as a schema name for the entry itself; this name reads as the collection/array type, not a single item, so it would be confusing next to upstreamDefinitions: [UpstreamPool].

Ranking (best to worst):

Rank Name Rationale
1 UpstreamDefinition Exact match with the gateway's pool-entry name; zero cross-plane ambiguity; only cost is the one-time endpoint rename to UpstreamEndpoint, which you've already accepted.
2 NamedUpstream Clear standalone meaning (an upstream identified by a name) and fits the two-word sibling pattern, but has no cross-plane equivalent and slightly overlaps conceptually with UpstreamReference (the name token itself).
3 PooledUpstream Clearly conveys "reusable pool member," consistent two-word style, but still a local-only term with no gateway counterpart.
4 UpstreamPoolEntry Unambiguous and descriptive, but three words breaks the naming rhythm of Upstream / UpstreamReference / OperationUpstream / UpstreamTimeout.
5 ReusableUpstream (current) Accurately describes intent, already implemented, but diverges from gateway terminology, the concern that started this thread.
6 SharedUpstream Weakest option — "shared" is ambiguous (could imply shared across APIs/tenants rather than "one of several named upstreams"), and it doesn't align with the gateway naming either.

Recommendation unchanged: UpstreamDefinition remains the strongest choice given the explicit goal of cross-plane consistency with #2012, and it composes cleanly with UpstreamReference, OperationUpstream, UpstreamTimeout, and the new UpstreamEndpoint.


✏️ Learnings added
Learnt from: mehara-rothila
URL: https://github.com/wso2/api-platform/pull/2155

Timestamp: 2026-07-08T05:19:54.263Z
Learning: In `platform-api/src/resources/openapi.yaml`, when naming the reusable upstream pool-entry schema (currently `ReusableUpstream`, held by `RESTAPI.upstreamDefinitions`), the preferred name is `UpstreamDefinition` to match the gateway-controller's naming in `gateway/gateway-controller/api/management-openapi.yaml` (from PR `#2012`), with the existing endpoint-level `UpstreamDefinition` (used under `Upstream.main`/`Upstream.sandbox`) renamed to `UpstreamEndpoint` to avoid collision. Alternatives considered and ranked below `UpstreamDefinition`: `NamedUpstream`, `PooledUpstream`, `UpstreamPoolEntry`, `ReusableUpstream`, `SharedUpstream`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@mehara-rothila

Copy link
Copy Markdown
Contributor Author

@coderabbitai for the agreed rename, please show a focused before/after of ONLY the affected schema blocks, not the full file: the Upstream wrapper's main/sandbox refs, the endpoint schema (UpstreamDefinition -> UpstreamEndpoint), the pool schema (ReusableUpstream -> UpstreamDefinition), and the RESTAPI.upstreamDefinitions.items ref. Keep it to those blocks so the concrete diff is easy to review.

@mehara-rothila

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

This is the control-plane (platform-api) side of per-operation upstream routing. The data-plane contract it must match is in PR #2012 (gateway-controller/pkg/config/api_validator.go and management-openapi.yaml). Please verify each of these and flag any divergence:

  • Validation parity with the gateway validator: upstreamDefinition name pattern/length, duplicate names, backend URL host-only http/https (path, query, or fragment rejected), weight 0-100, connect timeout format + positive + blank-treated-as-unset, API-level url-xor-ref, and that every API-level and per-operation ref resolves to a declared pool entry.
  • Validation runs on create and on the merged result of an update (an update must not be able to remove a pool entry a surviving operation still references), and surfaces as HTTP 400 on create, update, project import, and OpenAPI import.
  • Persistence round-trip through the rest_apis.configuration JSON blob: no field dropped, and rows written before this change still decode.
  • Deployment-YAML emission matches the gateway shape (spec.upstreamDefinitions and operations[].upstream.main/sandbox.ref) and is emitted for REST APIs only, not WebSub.
  • Nil-safety on the generated OperationUpstream main/sandbox pointers, and that api/generated.go is regeneration-only (no hand edits).
  • Whether the added tests actually exercise these paths.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "== PR 2155 files changed =="
gh pr view 2155 --repo wso2/api-platform --json files --jq '.files[].path'
echo
echo "== PR 2155 diff stat =="
gh pr diff 2155 --repo wso2/api-platform --stat
echo
echo "== Confirm no generated.go / validator / test files touched =="
gh pr diff 2155 --repo wso2/api-platform | rg -n '^diff --git' || true

Length of output: 1675


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Full diff PR 2155 (patch) =="
gh pr diff 2155 --repo wso2/api-platform --patch > /tmp/pr2155.diff
wc -l /tmp/pr2155.diff

echo
echo "== Show diff per file with line counts =="
rg -n '^diff --git' /tmp/pr2155.diff

echo
echo "== upstream.go full diff =="
awk '/^diff --git a\/platform-api\/src\/internal\/model\/upstream.go/,/^diff --git a\/platform-api\/src\/internal\/(model\/api|repository)/' /tmp/pr2155.diff | head -300

echo
echo "== utils/api.go diff =="
awk '/^diff --git a\/platform-api\/src\/internal\/utils\/api\.go/,/^diff --git a\/platform-api\/src\/internal\/utils\/api_test\.go/' /tmp/pr2155.diff | head -400

Length of output: 18754


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== constants/error.go diff =="
sed -n '353,365p' /tmp/pr2155.diff
awk '/^diff --git a\/platform-api\/src\/internal\/constants\/error\.go/,/^diff --git a\/platform-api\/src\/internal\/dto\/api\.go/' /tmp/pr2155.diff

echo
echo "== dto/api.go diff =="
awk '/^diff --git a\/platform-api\/src\/internal\/dto\/api\.go/,/^diff --git a\/platform-api\/src\/internal\/handler\/api\.go/' /tmp/pr2155.diff

echo
echo "== handler/api.go diff =="
awk '/^diff --git a\/platform-api\/src\/internal\/handler\/api\.go/,/^diff --git a\/platform-api\/src\/internal\/model\/api\.go/' /tmp/pr2155.diff

echo
echo "== model/api.go diff =="
awk '/^diff --git a\/platform-api\/src\/internal\/model\/api\.go/,/^diff --git a\/platform-api\/src\/internal\/model\/upstream\.go/' /tmp/pr2155.diff

echo
echo "== service/api.go diff (validation logic) =="
awk '/^diff --git a\/platform-api\/src\/internal\/service\/api\.go/,/^diff --git a\/platform-api\/src\/internal\/service\/api_test\.go/' /tmp/pr2155.diff

Length of output: 43390


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== PR 2012 files changed =="
gh pr view 2012 --repo wso2/api-platform --json files --jq '.files[].path' || true

echo
echo "== PR 2012 diff for api_validator.go =="
gh pr diff 2012 --repo wso2/api-platform --patch -- gateway/gateway-controller/pkg/config/api_validator.go > /tmp/pr2012_validator.diff || true
wc -l /tmp/pr2012_validator.diff
cat /tmp/pr2012_validator.diff | head -400

echo
echo "== generated.go diff (nil safety on OperationUpstream) =="
awk '/^diff --git a\/platform-api\/src\/api\/generated\.go/,/^diff --git a\/platform-api\/src\/internal\/constants\/error\.go/' /tmp/pr2155.diff | head -200

echo
echo "== repository/api_test.go and service/api_test.go and utils/api_test.go: search upstream-related test names =="
awk '/^diff --git a\/platform-api\/src\/internal\/repository\/api_test\.go/,/^diff --git a\/platform-api\/src\/internal\/service\/api\.go/' /tmp/pr2155.diff | rg -n '^\+func Test|^\+\s*t\.Run' 
awk '/^diff --git a\/platform-api\/src\/internal\/service\/api_test\.go/,/^diff --git a\/platform-api\/src\/internal\/utils\/api\.go/' /tmp/pr2155.diff | rg -n '^\+func Test|^\+\s*\{name:|^\+\s*name:'
awk '/^diff --git a\/platform-api\/src\/internal\/utils\/api_test\.go/,0' /tmp/pr2155.diff | rg -n '^\+func Test|^\+\s*\{name:|^\+\s*name:'

Length of output: 12110


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== PR 2012 head branch =="
head_ref=$(gh pr view 2012 --repo wso2/api-platform --json headRefName --jq .headRefName)
echo "$head_ref"

echo
echo "== Fetch api_validator.go content from PR2012 head =="
gh api repos/wso2/api-platform/contents/gateway/gateway-controller/pkg/config/api_validator.go?ref="$head_ref" --jq .content | base64 -d > /tmp/api_validator.go
wc -l /tmp/api_validator.go
rg -n 'func Validate|upstream|Upstream|weight|Weight|timeout|Timeout|regexp|MustCompile' /tmp/api_validator.go | head -100

echo
echo "== Fetch upstreamref.go content =="
gh api repos/wso2/api-platform/contents/gateway/gateway-controller/pkg/utils/upstreamref/upstreamref.go?ref="$head_ref" --jq .content | base64 -d > /tmp/upstreamref.go
cat /tmp/upstreamref.go

Length of output: 1301


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Try git fetch branch directly =="
git fetch origin feat/per-op-upstream-gateway:refs/remotes/origin/feat/per-op-upstream-gateway 2>&1 | tail -20 || true
git show origin/feat/per-op-upstream-gateway:gateway/gateway-controller/pkg/config/api_validator.go > /tmp/api_validator.go 2>&1 || echo "FAILED git show validator"
git show origin/feat/per-op-upstream-gateway:gateway/gateway-controller/pkg/utils/upstreamref/upstreamref.go > /tmp/upstreamref.go 2>&1 || echo "FAILED git show upstreamref"
wc -l /tmp/api_validator.go /tmp/upstreamref.go 2>&1 || true

echo
echo "== grep upstream-related validation in api_validator.go =="
rg -n 'upstream|Upstream|weight|Weight|timeout|Timeout|regexp|MustCompile|func ' /tmp/api_validator.go 2>/dev/null | head -150

echo
echo "== upstreamref.go content =="
cat /tmp/upstreamref.go 2>/dev/null

Length of output: 1443


🏁 Script executed:

#!/bin/bash
set -euo pipefail

git fetch origin pull/2012/head:refs/remotes/origin/pr-2012 2>&1 | tail -20
git show origin/pr-2012:gateway/gateway-controller/pkg/config/api_validator.go > /tmp/api_validator.go 2>&1 || echo "FAILED validator"
git show origin/pr-2012:gateway/gateway-controller/pkg/utils/upstreamref/upstreamref.go > /tmp/upstreamref.go 2>&1 || echo "FAILED upstreamref"
wc -l /tmp/api_validator.go /tmp/upstreamref.go

echo
echo "== grep validation logic =="
rg -n 'upstream|Upstream|weight|Weight|timeout|Timeout|regexp|MustCompile|func ' /tmp/api_validator.go 2>/dev/null | head -200

echo
echo "== upstreamref.go =="
cat /tmp/upstreamref.go 2>/dev/null

Length of output: 13864


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '158,260p' /tmp/api_validator.go

Length of output: 3206


@mehara-rothila Here's the full review against the gateway contract (PR #2012, gateway/gateway-controller/pkg/config/api_validator.go and pkg/utils/upstreamref/upstreamref.go). Note that this PR has grown well past the "schema-only" scope described in its description — it now also touches platform-api/src/api/generated.go, internal/dto/api.go, internal/handler/api.go, internal/model/api.go, internal/model/upstream.go, internal/service/api.go, and three test files, so I reviewed the full implementation rather than just the OpenAPI diff.

1. Validation parity with the gateway validator — Matches closely.

  • Name pattern/length: platform-api/src/internal/service/api.go's upstreamRefNameRe (^[a-zA-Z0-9\-_]+$) and 100-char cap in validateUpstreamReferenceName are identical to the gateway's upstreamRefRegex and validateUpstreamRefName.
  • Duplicate names: both reject via a defined/seen-name map.
  • Backend URL: both require http/https scheme, a non-empty host, and reject a non-root path, query string, or fragment (comments in validateUpstreamRefs explicitly call out matching the gateway validator).
  • Weight: both enforce 0–100.
  • Connect timeout: identical regex ^\d+(\.\d+)?(ms|s|m|h)$, time.ParseDuration for the error message, positive-duration check, and strings.TrimSpace before checking emptiness (blank treated as unset) on both sides.
  • API-level url-xor-ref: validateAPIUpstreamEndpoint mirrors the gateway's validateUpstream (reject both set, require one, then validate).
  • Ref resolution: resolveUpstreamRef against the defined map is functionally equivalent to the gateway's upstreamref.FindByName.

2. Validation on create and merged update, surfaced as 400 — Confirmed.

  • validateCreateAPIRequest calls validateUpstreamRefs on create.
  • applyAPIUpdates re-validates the merged existingAPI (after applying req.Upstream/req.UpstreamDefinitions), so a surviving per-operation ref pointing at a removed pool entry is caught — covered by TestAPIService_UpdateRejectsPoolRemovalWithDanglingRef.
  • handler/api.go maps constants.ErrInvalidUpstreamDefinition to HTTP 400 in CreateAPI, UpdateAPI, ImportAPIProject, and ImportOpenAPI — all four paths are covered.

3. Persistence round-tripmodel.RestAPIConfig.UpstreamDefinitions (in internal/model/api.go) rides through the same JSON configuration blob as the other nested config fields (db:"-" tag), and TestAPIRepo_CreateAndRead_PreservesUpstreamDefinitionsAndPerOp in internal/repository/api_test.go exercises the round trip. I didn't find a decode test specifically for rows written before this change; worth double-checking that an existing JSON blob without upstreamDefinitions/operation upstream still unmarshals cleanly (both are optional/omitempty, so it should, but an explicit regression test would be good defensive coverage).

4. Deployment YAML emission — Matches the gateway shape. BuildAPIDeploymentYAML only sets apiYAMLData.UpstreamDefinitions inside case constants.RestApi:, and TestBuildAPIDeploymentYAML_WebSubOmitsUpstreamDefinitions confirms WebSub is excluded. spec.upstreamDefinitions (dto.UpstreamDefinitionYAML: name/basePath/timeout/upstreams[url,weight]) and operations[].upstream.main/sandbox.ref are both emitted and covered by TestBuildAPIDeploymentYAML_EmitsUpstreamDefinitions, TestBuildAPIDeploymentYAML_EmitsPerOpUpstreamRef, and TestBuildAPIDeploymentYAML_FullPerOpRefContract.

5. Nil-safety and generated.gooperationUpstreamAPIToModel/operationUpstreamModelToAPI/overlayPerOpUpstream all guard Main/Sandbox with nil checks before dereferencing. api/generated.go's diff is limited to schema-driven additions (ReusableUpstream, UpstreamTimeout, UpstreamReference type alias, OperationUpstream, RESTAPI.UpstreamDefinitions, retyping UpstreamDefinition.Ref to *UpstreamReference) — no hand-written logic; the anonymous-struct constructor helpers (newAPIOperationUpstreamTarget, newAPIUpstreamBackend) correctly live in internal/utils/api.go, not in generated.go.

6. Tests — Yes, they exercise the intended paths: repository round-trip, validateUpstreamRefs unit cases, create-rejects-unresolved-ref, update-rejects-pool-removal-with-dangling-ref, YAML emission (including the WebSub-omission case and the full per-op ref contract), and merge/overlay behavior for per-operation upstream overrides during OpenAPI/project import.

One open item from earlier in this thread: the agreed rename (ReusableUpstreamUpstreamDefinition, endpoint-level UpstreamDefinitionUpstreamEndpoint) has not been applied yet — openapi.yaml, generated.go, and model/upstream.go still use the old names. Now that PR #2012 has landed UpstreamDefinition (pool) and UpstreamReference (type alias) on the gateway side, this is the natural point to apply that rename before this branch grows further, since every file above (dto, model, service, utils, and their tests) would need the same symbol renames.

✅ Action performed

Review finished.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (3)
platform-api/src/internal/utils/api_test.go (1)

672-679: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Apply the agreed upstream type rename.

These tests continue to encode ReusableUpstream; rename the reusable pool entry to UpstreamDefinition and reserve UpstreamEndpoint for direct endpoint shapes across the schema, generated code, models, and tests. As noted in the PR objectives, this is the agreed naming model.

🤖 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 `@platform-api/src/internal/utils/api_test.go` around lines 672 - 679, Rename
the reusable pool type represented by UpstreamDefinitions and its entries from
ReusableUpstream to UpstreamDefinition across the schema, generated code,
models, and tests. Reserve UpstreamEndpoint for direct endpoint shapes, updating
all references and generated artifacts consistently while preserving the
existing behavior and structure.
platform-api/src/internal/handler/api.go (1)

162-163: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use Warn for expected 400-level validation failures.

Invalid upstream definitions are client-input errors, not unexpected server failures. Reserve Error for 5xx conditions and use Warn here.

Based on learnings, expected 4xx handler errors should use slog.Warn, while slog.Error is reserved for unexpected server-side failures.

Also applies to: 319-320

🤖 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 `@platform-api/src/internal/handler/api.go` around lines 162 - 163, Change the
logging call for constants.ErrInvalidUpstreamDefinition in the handler to
h.slogger.Warn, preserving the existing message and structured fields. Apply the
same severity change to the corresponding handler branch around lines 319-320,
while leaving unexpected server-side failures logged at Error.

Source: Learnings

platform-api/src/internal/utils/api.go (1)

1539-1545: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Canonicalize methods before constructing lookup keys.

Normalize the method once at the conversion boundary and use that canonical value throughout merging. The current inline normalization only affects the temporary key; the underlying operation can retain user-supplied casing.

As per coding guidelines, user-supplied HTTP methods must be uppercased before map-key creation and downstream route processing.

Also applies to: 1552-1554

🤖 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 `@platform-api/src/internal/utils/api.go` around lines 1539 - 1545, In the
override-operation merge logic around byKey, uppercase each user-supplied
op.Request.Method at the conversion boundary before constructing lookup keys,
and use that canonical method for downstream route processing as well. Update
the corresponding logic at the additional referenced range so the underlying
operation no longer retains mixed-case methods while preserving the existing
upstream lookup behavior.

Source: Coding guidelines

🤖 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 `@platform-api/src/internal/handler/api.go`:
- Around line 162-166: Sanitize client responses in the
invalid-upstream-definition branches, including the corresponding occurrences
near lines 319, 702, and 925: keep logging the detailed err server-side, but
replace err.Error() passed to utils.NewErrorResponse with the fixed message
“Invalid upstream definition”.

In `@platform-api/src/internal/service/api_test.go`:
- Around line 844-846: Update the schema fixture setup around runtime.Caller and
schemaPath to resolve the path to an absolute, cleaned value, then verify it
remains within the intended test root before calling os.ReadFile. Reject or fail
the test for paths outside that root while preserving the existing
schema-loading behavior for valid paths.

In `@platform-api/src/internal/service/api.go`:
- Around line 694-701: Update resolveUpstreamRef to reject whitespace-padded
references or consistently normalize the reference before it is persisted and
emitted; ensure validation, resolution, and deployment use the same canonical
value rather than only validating strings.TrimSpace(ref).
- Around line 723-731: Update both URL validation paths around url.Parse to
validate parsed.Hostname() rather than only parsed.Host, rejecting empty
hostnames such as http://:8080. Preserve the existing http/https scheme
requirement, and only accept a port when a non-empty hostname is present.

---

Nitpick comments:
In `@platform-api/src/internal/handler/api.go`:
- Around line 162-163: Change the logging call for
constants.ErrInvalidUpstreamDefinition in the handler to h.slogger.Warn,
preserving the existing message and structured fields. Apply the same severity
change to the corresponding handler branch around lines 319-320, while leaving
unexpected server-side failures logged at Error.

In `@platform-api/src/internal/utils/api_test.go`:
- Around line 672-679: Rename the reusable pool type represented by
UpstreamDefinitions and its entries from ReusableUpstream to UpstreamDefinition
across the schema, generated code, models, and tests. Reserve UpstreamEndpoint
for direct endpoint shapes, updating all references and generated artifacts
consistently while preserving the existing behavior and structure.

In `@platform-api/src/internal/utils/api.go`:
- Around line 1539-1545: In the override-operation merge logic around byKey,
uppercase each user-supplied op.Request.Method at the conversion boundary before
constructing lookup keys, and use that canonical method for downstream route
processing as well. Update the corresponding logic at the additional referenced
range so the underlying operation no longer retains mixed-case methods while
preserving the existing upstream lookup behavior.
🪄 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: 9aa52fdf-c292-4869-ad6a-295d40587ab3

📥 Commits

Reviewing files that changed from the base of the PR and between e6e8d18 and 13d47a5.

📒 Files selected for processing (12)
  • platform-api/src/api/generated.go
  • platform-api/src/internal/constants/error.go
  • platform-api/src/internal/dto/api.go
  • platform-api/src/internal/handler/api.go
  • platform-api/src/internal/model/api.go
  • platform-api/src/internal/model/upstream.go
  • platform-api/src/internal/repository/api_test.go
  • platform-api/src/internal/service/api.go
  • platform-api/src/internal/service/api_test.go
  • platform-api/src/internal/utils/api.go
  • platform-api/src/internal/utils/api_test.go
  • platform-api/src/resources/openapi.yaml
✅ Files skipped from review due to trivial changes (1)
  • platform-api/src/api/generated.go

Comment thread platform-api/src/internal/handler/api.go Outdated
Comment thread platform-api/src/internal/service/api_test.go Outdated
Comment thread platform-api/src/internal/service/api.go Outdated
Comment thread platform-api/src/internal/service/api.go Outdated
@mehara-rothila

Copy link
Copy Markdown
Contributor Author

@coderabbitai we've decided to keep the pool entry schema named ReusableUpstream and are not renaming it to UpstreamDefinition. My earlier comment that called the rename "agreed" was premature; on reflection we are not doing it in this PR.

Reasoning: adopting UpstreamDefinition for the pool would also force renaming the pre-existing UpstreamDefinition endpoint schema (the url/ref endpoint under Upstream.main/sandbox) to UpstreamEndpoint, which churns a large amount of pre-existing code (dto, model, service, utils, and their tests) that is unrelated to this feature. This PR is scoped to adding the upstreamDefinitions pool and per-operation upstream overrides, and we want to keep that scope tight. The schema shapes already match the gateway contract in #2012; only the pool entry name differs, and ReusableUpstream reads clearly for a reusable named entry in the pool.

Please note this decision and do not re-flag the ReusableUpstream -> UpstreamDefinition rename in future reviews of this PR. If the maintainers prefer the rename, we will do it as a separate follow-up.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Tip

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

@mehara-rothila Understood — noted. I will not re-flag the ReusableUpstreamUpstreamDefinition rename in future reviews of this PR. Keeping the pool-entry schema as ReusableUpstream and deferring the rename (along with the associated UpstreamDefinitionUpstreamEndpoint change) to a separate follow-up is a reasonable way to keep this PR's scope focused on adding the upstreamDefinitions pool and per-operation upstream overrides, especially since the wire-level schema shapes already match the gateway contract in #2012 and only the pool-entry symbol name differs.


✏️ Learnings added
Learnt from: mehara-rothila
URL: https://github.com/wso2/api-platform/pull/2155

Timestamp: 2026-07-11T22:44:52.702Z
Learning: In `platform-api/src/resources/openapi.yaml` (PR `#2155`), the team decided NOT to rename the reusable upstream pool-entry schema `ReusableUpstream` to `UpstreamDefinition` within this PR, reversing an earlier tentative agreement. Rationale: adopting `UpstreamDefinition` for the pool would force renaming the pre-existing endpoint-level `UpstreamDefinition` schema (used under `Upstream.main`/`Upstream.sandbox`) to `UpstreamEndpoint`, causing unrelated churn across `internal/dto`, `internal/model`, `internal/service`, `internal/utils`, and their tests. The schema shapes already match the gateway contract in PR `#2012`; only the pool-entry name differs. Do not re-flag this rename suggestion in future reviews of this PR; it may be done as a separate follow-up PR if maintainers prefer.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@mehara-rothila
mehara-rothila changed the base branch from feature/operation-level-ep to main July 12, 2026 04:38
@mehara-rothila
mehara-rothila force-pushed the feat/platform-api-upstream-definitions branch from 13d47a5 to 47ed679 Compare July 12, 2026 04:38
@mehara-rothila

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 12, 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 commented Jul 22, 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

repo="wso2/api-platform"

git fetch origin \
  pull/2012/head:refs/remotes/origin/pr-2012 \
  main:refs/remotes/origin/main >/dev/null 2>&1 || true

for ref in origin/pr-2012 origin/main; do
  echo "===== $ref: candidate files ====="
  git ls-tree -r --name-only "$ref" -- gateway/gateway-controller |
    rg '(api_validator\.go|restapi\.go|translator\.go|resilience|cluster|deploy|config)' || true

  echo
  echo "===== $ref: all connect-timeout / timeout parsing references (with paths/lines) ====="
  git grep -n -i -E \
    'connect.?timeout|timeout\.connect|ConnectTimeout|ParseDuration|Parse.*Duration|ResilienceDuration|createWeightedCluster|UpstreamCluster|Route.*Timeout|timeout:' \
    "$ref" -- gateway/gateway-controller || true
  echo
done

echo "===== PR `#2012`: map likely files before targeted reads ====="
for f in \
  gateway/gateway-controller/pkg/config/api_validator.go \
  gateway/gateway-controller/pkg/transform/restapi.go \
  gateway/gateway-controller/pkg/xds/translator.go
do
  echo "--- $f"
  git show "origin/pr-2012:$f" > "/tmp/$(basename "$f")"
  ast-grep outline "/tmp/$(basename "$f")" --items all 2>/dev/null | head -300 || true
done

echo "===== PR `#2012`: numbered context for all relevant symbols ====="
for f in /tmp/api_validator.go /tmp/restapi.go /tmp/translator.go; do
  echo "--- $f"
  rg -n -i -C5 \
    'connect.?timeout|timeout\.connect|ConnectTimeout|ParseDuration|ResilienceDuration|createWeightedCluster|UpstreamCluster|validate.*[Uu]pstream|validate.*[Tt]imeout|resilience' \
    "$f" || true
done

echo "===== Find checked-in Envoy proto/validation evidence, if any ====="
git ls-tree -r --name-only origin/pr-2012 |
  rg -i '(cluster.*\.proto$|cds.*\.proto$|envoy.*proto|validate.*proto|go-control-plane)' | head -100 || true

git grep -n -i -E 'connect_timeout.*(greater|gt|required|min|zero)|ConnectTimeout.*(greater|gt|required|min|zero)' \
  origin/pr-2012 -- ':!vendor' ':!node_modules' || true

Length of output: 212788


🏁 Script executed:

#!/bin/bash
set -euo pipefail

git fetch origin pull/2012/head:refs/remotes/origin/pr-2012 main:refs/remotes/origin/main >/dev/null 2>&1 || true

echo "===== Exact PR `#2012` numbered source slices ====="
for spec in \
  "gateway/gateway-controller/pkg/config/api_validator.go:400:430" \
  "gateway/gateway-controller/pkg/transform/restapi.go:84:115" \
  "gateway/gateway-controller/pkg/transform/restapi.go:285:345" \
  "gateway/gateway-controller/pkg/transform/restapi.go:529:585" \
  "gateway/gateway-controller/pkg/transform/restapi.go:649:670" \
  "gateway/gateway-controller/pkg/xds/translator.go:521:615" \
  "gateway/gateway-controller/pkg/xds/translator.go:625:700" \
  "gateway/gateway-controller/pkg/xds/translator.go:2969:3065" \
  "gateway/gateway-controller/pkg/utils/upstreamref/upstreamref.go:1:100" \
  "gateway/gateway-controller/pkg/models/runtime_deploy_config.go:75:135" \
  "gateway/gateway-controller/pkg/config/validator_test.go:805:880" \
  "gateway/gateway-controller/pkg/utils/upstreamref/upstreamref_test.go:145:175" \
  "gateway/gateway-controller/pkg/transform/restapi_test.go:1270:1340" \
  "gateway/gateway-controller/pkg/xds/translator_test.go:2490:2545"
do
  path="${spec%%:*}"
  rest="${spec#*:}"
  start="${rest%%:*}"
  end="${rest##*:}"
  echo "--- $path (Lines $start-$end)"
  git show "origin/pr-2012:$path" | nl -ba | sed -n "${start},${end}p"
done

echo
echo "===== Dispatch order: transformer path versus legacy fallback ====="
git show origin/pr-2012:gateway/gateway-controller/pkg/xds/translator.go |
  nl -ba | sed -n '625,940p'

echo
echo "===== Same relevant functions on main (for divergence check) ====="
for spec in \
  "gateway/gateway-controller/pkg/transform/restapi.go:550:590" \
  "gateway/gateway-controller/pkg/xds/translator.go:1788:1825" \
  "gateway/gateway-controller/pkg/xds/translator.go:2890:3000" \
  "gateway/gateway-controller/pkg/utils/upstreamref/upstreamref.go:45:85"
do
  path="${spec%%:*}"
  rest="${spec#*:}"
  start="${rest%%:*}"
  end="${rest##*:}"
  echo "--- $path (Lines $start-$end)"
  git show "origin/main:$path" | nl -ba | sed -n "${start},${end}p"
done

echo
echo "===== Envoy Go dependency version ====="
for f in go.mod gateway/gateway-controller/go.mod; do
  if git cat-file -e "origin/pr-2012:$f" 2>/dev/null; then
    echo "--- $f"
    git show "origin/pr-2012:$f" | rg -n 'go-control-plane|envoy' || true
  fi
done

Length of output: 2873


🤖 web_search executed:

Envoy v3 Cluster connect_timeout protobuf field constraint must be greater than zero API reference

Length of output: 10441


🏁 Script executed:

#!/bin/bash
set -euo pipefail

git fetch origin pull/2012/head:refs/remotes/origin/pr-2012 main:refs/remotes/origin/main >/dev/null 2>&1 || true

show_slice() {
  local ref="$1" path="$2" start="$3" end="$4"
  echo "--- $ref:$path (Lines $start-$end)"
  git show "$ref:$path" | cat -n | sed -n "${start},${end}p"
}

echo "===== PR `#2012`: validation and primary RestAPITransformer flow ====="
show_slice origin/pr-2012 gateway/gateway-controller/pkg/config/api_validator.go 409 429
show_slice origin/pr-2012 gateway/gateway-controller/pkg/transform/restapi.go 84 110
show_slice origin/pr-2012 gateway/gateway-controller/pkg/transform/restapi.go 285 345
show_slice origin/pr-2012 gateway/gateway-controller/pkg/transform/restapi.go 529 585
show_slice origin/pr-2012 gateway/gateway-controller/pkg/transform/restapi.go 649 669

echo "===== PR `#2012`: RuntimeDeployConfig -> Envoy cluster emission ====="
show_slice origin/pr-2012 gateway/gateway-controller/pkg/models/runtime_deploy_config.go 82 130
show_slice origin/pr-2012 gateway/gateway-controller/pkg/xds/translator.go 236 265
show_slice origin/pr-2012 gateway/gateway-controller/pkg/xds/translator.go 521 615
show_slice origin/pr-2012 gateway/gateway-controller/pkg/xds/translator.go 625 745

echo "===== PR `#2012`: legacy xDS path, shared connect parser, and resilience parser ====="
show_slice origin/pr-2012 gateway/gateway-controller/pkg/xds/translator.go 940 970
show_slice origin/pr-2012 gateway/gateway-controller/pkg/xds/translator.go 1112 1161
show_slice origin/pr-2012 gateway/gateway-controller/pkg/xds/translator.go 1896 1925
show_slice origin/pr-2012 gateway/gateway-controller/pkg/xds/translator.go 2969 3065
show_slice origin/pr-2012 gateway/gateway-controller/pkg/utils/upstreamref/upstreamref.go 45 85

echo "===== PR `#2012`: tests that pin zero behavior ====="
show_slice origin/pr-2012 gateway/gateway-controller/pkg/config/validator_test.go 819 875
show_slice origin/pr-2012 gateway/gateway-controller/pkg/utils/upstreamref/upstreamref_test.go 145 175
show_slice origin/pr-2012 gateway/gateway-controller/pkg/transform/restapi_test.go 1277 1335
show_slice origin/pr-2012 gateway/gateway-controller/pkg/xds/translator_test.go 2423 2458
show_slice origin/pr-2012 gateway/gateway-controller/pkg/xds/translator_test.go 2503 2542

echo "===== Confirm `main` keeps the primary rejection ====="
show_slice origin/main gateway/gateway-controller/pkg/transform/restapi.go 559 580
show_slice origin/main gateway/gateway-controller/pkg/utils/upstreamref/upstreamref.go 45 85

Length of output: 48117


@mehara-rothila You were right to request the end-to-end trace. My earlier conclusion stopped at the gateway management validator and was incomplete.

Conclusion

A definition with timeout.connect: "0s" is accepted by the gateway management validator, but it does not deploy successfully. Both gateway translation paths reject it before a usable Envoy cluster is emitted.

Therefore, platform-api rejecting "0s" on create/update matches the gateway’s effective deploy-time behavior, although it does not mirror the gateway validator’s current admission behavior. The gateway has an internal validation/translation inconsistency that should be resolved gateway-side.

1. End-to-end trace for timeout.connect: "0s"

A. Management validation: accepts "0s"

In PR #2012, validateUpstreamDefinitionsList validates timeout.connect in gateway/gateway-controller/pkg/config/api_validator.go.

  • Lines 412–429 trim the value, require the single-unit duration regex, then call time.ParseDuration.
  • "0s" matches ^\d+(\.\d+)?(ms|s|m|h)$ and time.ParseDuration("0s") succeeds.
  • There is no positive-duration check.

The test confirms this intent:

// gateway/gateway-controller/pkg/config/validator_test.go
// Lines 819–845
// Zero disables the timeout...
for _, zeroTimeout := range []string{"0s", "0ms"} {
    ...
    errors := validator.validateUpstreamDefinitions(definitions)
    assert.Empty(t, errors, "timeout %q must be accepted ...", zeroTimeout)
}

So the management validator accepts "0s".

B. Primary deployment path: RestAPITransformer.Transform rejects it

Translator.TranslateConfigs uses the RuntimeDeployConfig transformer path first:

// gateway/gateway-controller/pkg/xds/translator.go
// Lines 662–679
if transformer, ok := t.transformers[cfg.Kind]; ok {
    rdc, transformErr := transformer.Transform(cfg)
    if transformErr != nil {
        // ... falling back to legacy path
    } else {
        routesList, clusterList, err = t.translateRuntimeConfig(rdc)
        ...
    }
}

For an upstream definition, RestAPITransformer.Transform calls definitionConnectTimeout while creating the definition’s UpstreamCluster:

// gateway/gateway-controller/pkg/transform/restapi.go
// Lines 287–340
defConnectTimeout, err := definitionConnectTimeout(&def)
if err != nil {
    return nil, fmt.Errorf("upstream definition '%s': %w", def.Name, err)
}
...
ConnectTimeout: defConnectTimeout,

The same helper is used for an API-level main/sandbox ref to a definition:

// gateway/gateway-controller/pkg/transform/restapi.go
// Lines 559–568
if up != nil && up.Ref != nil && strings.TrimSpace(*up.Ref) != "" {
    ct, terr := definitionConnectTimeout(
        lookupUpstreamDefinition(*up.Ref, upstreamDefinitions),
    )
    if terr != nil {
        return nil, fmt.Errorf("%s upstream: %w", upstreamName, terr)
    }
    connectTimeout = ct
}

The decisive parser is:

// gateway/gateway-controller/pkg/transform/restapi.go
// Lines 649–669
func definitionConnectTimeout(def *api.UpstreamDefinition) (*time.Duration, error) {
    if def == nil || def.Timeout == nil || def.Timeout.Connect == nil {
        return nil, nil
    }
    s := strings.TrimSpace(*def.Timeout.Connect)
    if s == "" {
        return nil, nil
    }
    d, err := time.ParseDuration(s)
    if err != nil {
        return nil, fmt.Errorf("invalid connect timeout %q: %w", s, err)
    }
    if d <= 0 {
        return nil, fmt.Errorf("connect timeout must be positive, got %v", d)
    }
    return &d, nil
}

time.ParseDuration("0s") yields 0; Line 665 rejects it. Thus the primary transformer returns an error and does not build RuntimeDeployConfig.UpstreamClusters for that API.

This same check exists on gateway main in gateway/gateway-controller/pkg/transform/restapi.go, Lines 559–579.

C. Primary xDS cluster builders: would preserve zero, but are unreachable for valid transformed config

If the transformer had supplied a non-nil zero duration, translateRuntimeConfig would pass it to the cluster builders unchanged:

// gateway/gateway-controller/pkg/xds/translator.go
// Lines 240–264
connectTimeout := uc.ConnectTimeout
...
c := t.createCluster(clusterName, parsedURL, nil, connectTimeout)
// or:
c := t.createWeightedCluster(clusterName, uc.Endpoints, uc.TLS, connectTimeout)

Both builders use a supplied pointer verbatim:

// gateway/gateway-controller/pkg/xds/translator.go
// createWeightedCluster, Lines 594–606
if connectTimeout != nil {
    effectiveConnectTimeout = *connectTimeout
} else {
    effectiveConnectTimeout = ...
}
...
ConnectTimeout: durationpb.New(effectiveConnectTimeout),

createCluster has the same behavior at Lines 1904–1916.

So a non-nil 0 would become durationpb.New(0) in Cluster.ConnectTimeout; however, the primary transformer makes that state unreachable for an upstreamDefinitions timeout.

D. Legacy fallback path: also rejects "0s"

When the primary transformer returns an error, TranslateConfigs falls through to translateAPIConfig:

// gateway/gateway-controller/pkg/xds/translator.go
// Lines 682–699
if routesList == nil {
    ...
    routesList, clusterList, err = t.translateAPIConfig(cfg, configs)
    if err != nil {
        log.Error("Failed to translate config", ...)
        continue
    }
}

The legacy path calls resolveTimeoutFromDefinition for each definition:

// gateway/gateway-controller/pkg/xds/translator.go
// Lines 1137–1150
resolved, err := resolveTimeoutFromDefinition(&def)
if err != nil {
    return nil, nil, fmt.Errorf(
        "invalid timeout in upstream definition '%s': %w", def.Name, err,
    )
}
...
defCluster := t.createCluster(defClusterName, parsedURL, nil, defConnectTimeout)

That ultimately uses parseTimeout:

// gateway/gateway-controller/pkg/xds/translator.go
// Lines 2973–2977
func parseTimeout(timeoutStr *string) (*time.Duration, error) {
    return upstreamref.ParseConnectTimeout(timeoutStr)
}

And the shared parser rejects zero:

// gateway/gateway-controller/pkg/utils/upstreamref/upstreamref.go
// Lines 53–72
func ParseConnectTimeout(timeoutStr *string) (*time.Duration, error) {
    ...
    d, err := time.ParseDuration(trimmed)
    if err != nil {
        return nil, fmt.Errorf("invalid timeout format: %w", err)
    }
    if d <= 0 {
        return nil, fmt.Errorf("timeout must be positive, got: %v", d)
    }
    return &d, nil
}

TestParseConnectTimeout_Zero in the same package explicitly expects "0s" to fail (Lines 156–161).

So the fallback translation also fails; TranslateConfigs logs the failure and continues without adding routes or clusters for that API.

2. Function that parses definition timeout.connect for the deployed cluster

For the active RuntimeDeployConfig path, it is:

  • RestAPITransformer.definitionConnectTimeout
  • gateway/gateway-controller/pkg/transform/restapi.go, Lines 653–669

It parses with time.ParseDuration and returns an error for d <= 0. Thus, it rejects "0s".

The xDS builders, Translator.createCluster and Translator.createWeightedCluster, do not parse the definition string. They only consume the already-parsed *time.Duration.

3. Connect timeout and route/resilience timeout use different paths

They are intentionally different.

Setting Parser "0s" behavior
upstreamDefinitions[].timeout.connect definitionConnectTimeout in the primary path; upstreamref.ParseConnectTimeout in legacy fallback Rejected as non-positive
resilience.timeout / resilience.idleTimeout parseDurationAllowZero, called by ResolveResilience Accepted as explicit disablement

The resilience path is explicit:

// gateway/gateway-controller/pkg/xds/translator.go
// Lines 2979–3005
func parseDurationAllowZero(timeoutStr *string) (*time.Duration, error) {
    ...
    duration, err := time.ParseDuration(s)
    ...
    return &duration, nil
}

// "0s" yields a non-nil zero duration (meaning "explicitly disabled").
func ResolveResilience(...) ...

TestParseDurationAllowZero_MatchesCRDPattern includes "0s" and "0ms" among accepted inputs (Lines 2425–2441).

That zero then reaches Envoy RouteAction.Timeout through routeTimeoutOrDefault; the route test explicitly verifies that a zero route timeout disables it. This is appropriate for route timeouts, but it is not the behavior for cluster connection timeouts.

4. Envoy constraint

Envoy’s v3 Cluster.connect_timeout is a google.protobuf.Duration with validation:

google.protobuf.Duration connect_timeout = 4 [
  (validate.rules).duration = {gt {}}
];

So an explicitly configured value must be strictly greater than zero. Envoy uses a default of 5 seconds only when the field is not set; a present zero duration is not equivalent to omission and violates the constraint.
Source: Envoy cluster.proto.

This matches the gateway’s transformer/shared-parser requirement for positive definition connect timeouts.

5. Practical result and platform-api parity

A gateway deployment containing a definition with:

timeout:
  connect: 0s

does not produce an Envoy cluster:

  1. Gateway management validation admits it.
  2. The primary RestAPITransformer rejects it before cluster construction.
  3. TranslateConfigs tries the legacy fallback.
  4. The legacy parser rejects it as non-positive too.
  5. TranslateConfigs logs the translation error and excludes that API’s routes/clusters from the generated snapshot.

So platform-api rejecting "0s" at create and update is consistent with the gateway’s effective deployment behavior and with Envoy’s strict-positive Cluster.connect_timeout requirement.

The actual gateway inconsistency is narrower: its management validator and TestValidateUpstreamDefinitions_ZeroConnectTimeout currently advertise "0s" as valid even though both deployment paths reject it. The gateway validator/test should be changed to reject zero for upstreamDefinitions[].timeout.connect; route resilience should continue accepting zero because it has different semantics.

I’m withdrawing my earlier recommendation that platform-api should accept "0s".


✏️ Learnings added
Learnt from: mehara-rothila
URL: https://github.com/wso2/api-platform/pull/2155

Timestamp: 2026-07-22T17:35:16.166Z
Learning: For upstream definition connection timeouts in wso2/api-platform gateway-controller PR `#2012` and main, `timeout.connect: "0s"` is accepted by `gateway/gateway-controller/pkg/config/api_validator.go::validateUpstreamDefinitionsList`, but deployment rejects it in both paths: the primary `gateway/gateway-controller/pkg/transform/restapi.go::definitionConnectTimeout` rejects `d <= 0`, and legacy xDS translation reaches `gateway/gateway-controller/pkg/utils/upstreamref/upstreamref.go::ParseConnectTimeout`, which also rejects `d <= 0`. Envoy v3 `Cluster.connect_timeout` has a strict-positive duration constraint. Platform-api should reject non-positive upstream definition connect timeouts to match actual gateway deployment behavior; gateway admission validation/tests accepting zero are inconsistent and should be fixed separately. Route resilience timeouts are different: `gateway/gateway-controller/pkg/xds/translator.go::parseDurationAllowZero` intentionally accepts `"0s"` as an explicit disabled `RouteAction` timeout.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copilot AI 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.

Pull request overview

Adds control-plane support in platform-api for reusable upstream definition pools and per-operation upstream overrides for REST APIs, including OpenAPI/schema updates, model/DTO mapping, deployment YAML emission, and create/update-time validation to prevent dangling refs.

Changes:

  • Extend the Platform API OpenAPI contract to support upstreamDefinitions plus per-operation upstream ref overrides and a shared UpstreamReference schema.
  • Add service-layer validation (validateUpstreamRefs) on create and merged-update to enforce name/url/basePath/weight/timeout constraints and ref resolution.
  • Update model/util/DTO conversions and add unit + integration tests covering persistence and YAML/HTTP round-trips.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
platform-api/resources/openapi.yaml Adds schemas and fields for reusable upstream definitions and per-operation upstream refs.
platform-api/internal/utils/api.go Maps upstreamDefinitions + per-op upstream between API/model and emits them into deployment YAML.
platform-api/internal/utils/api_upstream_test.go Unit tests for deployment YAML emission and model/API round-trips for new upstream fields.
platform-api/internal/service/api.go Adds create + update merged-config validation for upstreamDefinitions and ref resolution.
platform-api/internal/service/api_test.go Adjusts existing service tests to use a now-valid upstream on create.
platform-api/internal/service/upstream_validation_test.go Adds unit + SQLite-backed integration tests for upstream validation and update semantics.
platform-api/internal/repository/api_upstream_test.go Verifies upstreamDefinitions and per-op upstream survive DB JSON-blob round trip.
platform-api/internal/model/upstream.go Introduces internal model types for reusable upstreams, targets, timeouts, and per-op upstream refs.
platform-api/internal/model/api.go Extends RestAPIConfig and OperationRequest to carry upstreamDefinitions and per-op upstream.
platform-api/internal/handler/api_upstream_integration_test.go Handler-level tests asserting HTTP 400 mappings and GET/POST/PUT JSON round-trip for new fields.
platform-api/internal/dto/api.go Extends deployment YAML DTO to include upstreamDefinitions pool + timeout/backends.
platform-api/api/generated.go Regenerates API types to include upstreamDefinitions, per-op upstream, and UpstreamReference.
Files not reviewed (1)
  • platform-api/api/generated.go: Generated file

Comment on lines +334 to +336
if definition.Timeout != nil && definition.Timeout.Connect != nil {
m.Timeout = &model.UpstreamTimeout{Connect: *definition.Timeout.Connect}
}
properties:
connect:
type: string
description: Connection timeout duration (e.g., "5s", "500ms")
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.

3 participants