Skip to content

feat(platform-api): per-op upstream routing via upstreamDefinitions#2108

Draft
mehara-rothila wants to merge 2 commits into
wso2:feature/operation-level-epfrom
mehara-rothila:feat/per-op-upstream-platform-api
Draft

feat(platform-api): per-op upstream routing via upstreamDefinitions#2108
mehara-rothila wants to merge 2 commits into
wso2:feature/operation-level-epfrom
mehara-rothila:feat/per-op-upstream-platform-api

Conversation

@mehara-rothila

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

Copy link
Copy Markdown
Contributor

Summary

Adds control-plane (platform-api) support for 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 upstreamDefinitions rather than carrying an inline URL. The platform-api accepts, validates, persists, and emits this configuration in the deployment YAML the gateway resolves. Operations without a per-op upstream fall back to the API-level upstream, exactly as before.

Backends are declared once in an API-level upstreamDefinitions pool and referenced by name from both API-level and operation-level upstreams. The per-op upstream and the pool live inside the existing API configuration blob, so there is no database migration.

Why

Real-world APIs often need individual operations to reach different backend services. Today that needs path-based workarounds or splitting one API into several. This lets the API definition declare per-operation routing directly, while backend URLs stay defined once in upstreamDefinitions and are referenced by name. Because the platform-api is the publisher-facing front door, it validates the configuration up front and fails fast with HTTP 400, so a config the control plane accepts is one the gateway will accept on deploy instead of surfacing as a later deployment failure.

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. Backends are declared once in upstreamDefinitions; the connect timeout lives on the definition. The wrapper and the leaf are both locked with additionalProperties: false, and at least one of main / sandbox is required. API-level upstream still accepts either url or ref, unchanged.

Backends declared once in a named pool. upstreamDefinitions is an array of named entries (name, optional basePath, optional connect timeout, and one or more weighted upstreams). Both API-level and operation-level refs resolve against this pool, so a backend URL is defined in exactly one place and reused by name.

Validate at the front door, aligned with the gateway. The platform-api now rejects the same shapes the gateway rejects, so the two layers cannot disagree: a ref must resolve to a unique, well-formed definition; and an empty or absent per-op ref, an empty upstream: {} wrapper, an upstreamDefinition name that breaks the name contract (^[a-zA-Z0-9\-_]+$) or exceeds 100 characters, an upstreamDefinition url that is not host-only http/https, a weight outside 0..100, a connect timeout that is non-positive or not expressed in ms, s, m, or h units, and an invalid HTTP method are all rejected. Invalid upstream-definition and operation errors map to HTTP 400, and the same checks run on both the create and the update path.

No schema migration. The per-operation upstream and the upstreamDefinitions pool are stored inside the existing API configuration JSON, so persistence is purely additive with no database migration.

What changed (platform-api / control-plane layer)

  • OpenAPI schema (resources/openapi.yaml): a ReusableUpstream + UpstreamTimeout pool on the API, and a ref-only OperationUpstream / OperationUpstreamTarget on the operation. The operation wrapper and target are locked with additionalProperties: false; the wrapper requires at least one of main / sandbox; the ref is constrained to ^[a-zA-Z0-9\-_]+$, max 100 characters. Go types regenerated (api/generated.go).
  • Model / DTO / utils: carry the pool and the per-operation upstream through create, import, merge, and deployment-YAML emission. BuildAPIDeploymentYAML emits spec.upstreamDefinitions and operations[].upstream.main/.sandbox.ref in the gateway's shape, with the operation upstream emitted ref-only.
  • Service (internal/service/api.go): ref resolution plus all the gateway-aligned validation rules above, applied on both the create and the update path.
  • Handler (internal/handler/api.go): map invalid upstream-definition and operation errors to HTTP 400 on both create and update.

Test coverage

  • Unit tests across internal/service, internal/utils, and internal/repository: ref resolution; every validation rule (name pattern and length, host-only url with scheme and host checks, weight 0..100, connect timeout positive and restricted to ms, s, m, or h units, required and well-formed per-op ref, non-empty operation upstream wrapper, HTTP method enum); and DTO/model conversion across create, import, merge, and deployment-YAML emission (operation upstream emitted ref-only).
  • Integration tests against a real SQLite database: a create then persist then read round-trip of the upstreamDefinitions pool and the per-operation upstream, and a deployment-YAML contract test asserting the emitted operations[].upstream.main.ref and spec.upstreamDefinitions match the gateway shape (the per-op ref is attached to the right operation and does not leak to others).

Example

API configuration sent to the platform-api. The per-operation upstream is ref-only; backends are declared once in upstreamDefinitions and referenced by name:

context: /shop/v1
upstream:
  main:
    url: http://default-backend:8080   # API-level default (accepts url or ref)
upstreamDefinitions:
  - name: user-service
    upstreams:
      - url: http://user-service:8080
  - name: order-service
    timeout:
      connect: 5s
    upstreams:
      - url: http://order-service:8080
operations:
  - method: GET
    path: /products
    # no per-op upstream -> uses the API-level default-backend
  - method: GET
    path: /users/{id}
    upstream:
      main:
        ref: user-service       # ref-only: points at an upstreamDefinition
  - method: POST
    path: /orders
    upstream:
      main:
        ref: order-service

Related PRs

Add control-plane support for resource-level (per-operation) upstreams.
An operation references a named entry in an API-level upstreamDefinitions
pool (ref-only); the pool and per-op refs are persisted in the API
configuration and emitted in the deployment YAML the gateway resolves.

- openapi: ReusableUpstream/UpstreamTimeout pool and ref-only OperationUpstream
- model/dto/utils: carry the pool and per-op upstream through create, import,
  merge and deployment-YAML emission
- service: validate refs resolve to unique definitions and reject the same
  shapes the gateway rejects (empty or absent per-op ref, empty operation
  upstream wrapper, invalid HTTP method, definition name that breaks the name
  contract or exceeds 100 characters, definition url that is not host-only
  http/https, weight outside 0..100, non-positive timeout)
- handler: map invalid upstream definitions and operations to HTTP 400
- tests: unit and real-SQLite integration coverage for each rule
@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Draft detected.

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

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 27ff3848-be82-4c32-8a23-61dca27027d8

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

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces support for named, reusable upstream definitions and per-operation upstream overrides to the API platform. APIs can now define a pool of reusable upstream targets and reference them by name at both the API level and within individual operations. The changes add upstream definition and per-operation override fields across request/response models, implement validation to ensure references resolve and conform to naming and URL constraints, extend mapping logic to preserve upstreams through YAML and database round-trips, and include comprehensive testing to verify both preservation and validation behavior.


Sequence Diagram

sequenceDiagram
  participant Client
  participant CreateAPI Handler
  participant APIService
  participant Validator
  participant APIUtil
  participant Repository
  Client->>CreateAPI Handler: POST create request with UpstreamDefinitions + OperationUpstream
  CreateAPI Handler->>APIService: CreateAPI(request)
  APIService->>Validator: validateOperationMethods(operations)
  APIService->>Validator: validateUpstreamRefs(definitions, operations)
  Validator-->>APIService: ✓ validation passed
  APIService->>APIUtil: ModelToRESTAPI(modelAPI)
  APIUtil-->>APIService: restAPI with UpstreamDefinitions
  APIService->>Repository: Create(restAPI)
  Repository-->>APIService: created API
  APIService-->>CreateAPI Handler: success
  CreateAPI Handler-->>Client: HTTP 200 + created API
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description lacks several required template sections: Purpose (missing issue links), Goals, User stories, Documentation links, and Automation tests details with coverage metrics. Add Purpose section with issue links, Goals outlining solutions, User stories, Documentation impact statement with links, detailed Automation tests section with coverage metrics for unit and integration tests, and Security checks confirmation.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(platform-api): per-op upstream routing via upstreamDefinitions' clearly and specifically summarizes the main feature being added—per-operation upstream routing using upstream definitions.
Docstring Coverage ✅ Passed Docstring coverage is 86.36% 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.

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

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

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

❤️ Share

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

@mehara-rothila

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 5, 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 main to feature/operation-level-ep June 5, 2026 06:39
@mehara-rothila mehara-rothila changed the base branch from feature/operation-level-ep to main June 8, 2026 17:35
…tion with the gateway

Restrict an upstreamDefinition connect timeout to ms, s, m, or h units. time.ParseDuration alone also accepts ns/us units and compound values like 1h30m that the gateway rejects at deploy, so without this an API saves at the control plane and then fails to deploy. This matches the connect pattern already published in the OpenAPI spec.

Set the upstream weight schema minimum to 0 to match the validator and the gateway, which both accept a 0..100 range.
@mehara-rothila mehara-rothila force-pushed the feat/per-op-upstream-platform-api branch from cc3b23a to ba21576 Compare June 8, 2026 17:36
@mehara-rothila mehara-rothila changed the base branch from main to feature/operation-level-ep June 8, 2026 17:52
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.

1 participant