diff --git a/sdk/core/policy/v1alpha2/action.go b/sdk/core/policy/v1alpha2/action.go index 6b84a80c56..a0dd9878e7 100644 --- a/sdk/core/policy/v1alpha2/action.go +++ b/sdk/core/policy/v1alpha2/action.go @@ -43,6 +43,7 @@ type UpstreamRequestHeaderModifications struct { // These are valid at the header phase because routing decisions do not require // the request body to be available. UpstreamName *string // route to a named upstream definition (nil = no change) + UpstreamSlot *UpstreamSlot // route to the API's main or sandbox upstream slot instead of the slot implied by the current vhost (nil = no change); requesting a slot the API doesn't have configured is a no-op (logged, not an error) Path *string // rewrite the request path (nil = no change) Host *string // rewrite the :authority header (nil = no change) Method *string // rewrite the request method (nil = no change) @@ -113,6 +114,7 @@ type UpstreamRequestModifications struct { // Routing mutations — applied before the request is forwarded to upstream. UpstreamName *string // route to a named upstream definition (nil = no change) + UpstreamSlot *UpstreamSlot // route to the API's main or sandbox upstream slot instead of the slot implied by the current vhost (nil = no change); requesting a slot the API doesn't have configured is a no-op (logged, not an error) Path *string // rewrite the request path (nil = no change) Host *string // rewrite the :authority header (nil = no change) Method *string // rewrite the request method (nil = no change) diff --git a/sdk/core/policy/v1alpha2/context.go b/sdk/core/policy/v1alpha2/context.go index 8dab21f1d2..8cd784e943 100644 --- a/sdk/core/policy/v1alpha2/context.go +++ b/sdk/core/policy/v1alpha2/context.go @@ -1,5 +1,7 @@ package policyv1alpha2 +import policyenginev1 "github.com/wso2/api-platform/sdk/core/policyengine" + // Body represents HTTP request or response body data type Body struct { // Content is the body payload (may be nil) @@ -90,6 +92,13 @@ type RequestContext struct { Authority string Scheme string Vhost string + + // UpstreamInfo identifies the route's resolved upstream target (cluster name, URL, + // base path) for this request. Nil if no upstream has been resolved for the route. + // Authority/Scheme above reflect the inbound client-facing request and must not be + // used to address the actual upstream (e.g. for request signing) — use + // UpstreamInfo.URL instead. + UpstreamInfo *policyenginev1.UpstreamInfo } // ─── Response-phase contexts ───────────────────────────────────────────────── diff --git a/sdk/core/policy/v1alpha2/types.go b/sdk/core/policy/v1alpha2/types.go index c68ba9d168..b65e5ead32 100644 --- a/sdk/core/policy/v1alpha2/types.go +++ b/sdk/core/policy/v1alpha2/types.go @@ -13,6 +13,14 @@ const ( APIKindWebSubApi APIKind = "WebSubApi" ) +// UpstreamSlot identifies one of an API's built-in upstream slots. +type UpstreamSlot string + +const ( + UpstreamSlotMain UpstreamSlot = "main" + UpstreamSlotSandbox UpstreamSlot = "sandbox" +) + // ParameterType defines the type of a policy parameter type ParameterType string diff --git a/sdk/core/policyengine/upstream.go b/sdk/core/policyengine/upstream.go new file mode 100644 index 0000000000..9d663a0ae3 --- /dev/null +++ b/sdk/core/policyengine/upstream.go @@ -0,0 +1,39 @@ +package policyengine + +// UpstreamInfo identifies the resolved upstream target for one of an API's +// upstream slots (main or sandbox) or a named upstream definition: the Envoy +// cluster name (used for cluster_header dynamic routing) and the raw resolved +// URL. This is the shared wire shape used on both sides of the xDS route-config +// channel between gateway-controller and policy-engine. +type UpstreamInfo struct { + ClusterName string `json:"cluster_name" yaml:"cluster_name"` + URL string `json:"url" yaml:"url"` + BasePath string `json:"base_path" yaml:"base_path"` +} + +// ToMap converts UpstreamInfo into a structpb-compatible map for embedding as a +// nested object in xDS route-config metadata (structpb.Struct only accepts +// string/bool/number/nil/slice/map values, not arbitrary structs). +func (u UpstreamInfo) ToMap() map[string]interface{} { + return map[string]interface{}{ + "cluster_name": u.ClusterName, + "url": u.URL, + "base_path": u.BasePath, + } +} + +// UpstreamInfoFromMap decodes an UpstreamInfo from the generic map produced when +// the xDS metadata struct is unmarshalled back to JSON on the consumer side. +func UpstreamInfoFromMap(m map[string]interface{}) UpstreamInfo { + get := func(k string) string { + if s, ok := m[k].(string); ok { + return s + } + return "" + } + return UpstreamInfo{ + ClusterName: get("cluster_name"), + URL: get("url"), + BasePath: get("base_path"), + } +} diff --git a/sdk/core/policyengine/upstream_test.go b/sdk/core/policyengine/upstream_test.go new file mode 100644 index 0000000000..6a530240cd --- /dev/null +++ b/sdk/core/policyengine/upstream_test.go @@ -0,0 +1,33 @@ +package policyengine + +import "testing" + +func TestUpstreamInfoRoundTrip(t *testing.T) { + info := UpstreamInfo{ + ClusterName: "cluster_https_backend_example_com", + URL: "https://backend.example.com", + BasePath: "/v1", + } + + got := UpstreamInfoFromMap(info.ToMap()) + + if got != info { + t.Errorf("round trip mismatch: got %+v, want %+v", got, info) + } +} + +func TestUpstreamInfoFromMapMissingFields(t *testing.T) { + got := UpstreamInfoFromMap(map[string]interface{}{"cluster_name": "c1"}) + want := UpstreamInfo{ClusterName: "c1"} + if got != want { + t.Errorf("got %+v, want %+v", got, want) + } +} + +func TestUpstreamInfoFromMapWrongType(t *testing.T) { + got := UpstreamInfoFromMap(map[string]interface{}{"cluster_name": 42}) + want := UpstreamInfo{} + if got != want { + t.Errorf("got %+v, want %+v", got, want) + } +}