Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 35 additions & 8 deletions gateway/gateway-controller/api/management-openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3016,6 +3016,14 @@ components:
default: deployed
example: deployed

UpstreamReference:
type: string
description: Name of a predefined upstreamDefinition.
minLength: 1
maxLength: 100
pattern: '^[a-zA-Z0-9\-_]+$'
example: my-upstream-1

UpstreamDefinition:
type: object
required:
Expand All @@ -3024,12 +3032,7 @@ components:
description: Reusable upstream configuration with optional timeout and load balancing settings
properties:
name:
type: string
description: Unique identifier for this upstream definition
minLength: 1
maxLength: 100
pattern: '^[a-zA-Z0-9\-_]+$'
example: my-upstream-1
$ref: "#/components/schemas/UpstreamReference"
basePath:
type: string
description: Base path prefix for all endpoints in this upstream (e.g., /api/v2). All requests to this upstream will have this path prepended. Must start with '/' and must not end with '/'; omit for root.
Expand Down Expand Up @@ -3100,8 +3103,7 @@ components:
description: Direct backend URL to route traffic to
example: http://prod-backend:5000/api/v2
ref:
type: string
description: Reference to a predefined upstreamDefinition
$ref: "#/components/schemas/UpstreamReference"
hostRewrite:
type: string
enum:
Expand Down Expand Up @@ -3140,6 +3142,8 @@ components:
$ref: "#/components/schemas/Policy"
resilience:
$ref: "#/components/schemas/Resilience"
upstream:
$ref: "#/components/schemas/OperationUpstream"

OperationMethod:
type: string
Expand Down Expand Up @@ -3209,6 +3213,29 @@ components:
enum: [Exact, RegularExpression]
default: Exact

OperationUpstream:
type: object
additionalProperties: false
description: Per-operation upstream override. Each sub-field must reference a named entry in spec.upstreamDefinitions. Missing sub-fields fall back to API-level upstream. At least one of main or sandbox must be set.
minProperties: 1
properties:
main:
type: object
additionalProperties: false
required:
- ref
properties:
ref:
$ref: "#/components/schemas/UpstreamReference"
sandbox:
type: object
additionalProperties: false
required:
- ref
properties:
ref:
$ref: "#/components/schemas/UpstreamReference"

Policy:
type: object
required:
Expand Down
546 changes: 283 additions & 263 deletions gateway/gateway-controller/pkg/api/management/generated.go

Large diffs are not rendered by default.

112 changes: 99 additions & 13 deletions gateway/gateway-controller/pkg/config/api_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (

api "github.com/wso2/api-platform/gateway/gateway-controller/pkg/api/management"
"github.com/wso2/api-platform/gateway/gateway-controller/pkg/constants"
"github.com/wso2/api-platform/gateway/gateway-controller/pkg/utils/upstreamref"
)

// APIValidator validates API configurations using rule-based validation
Expand All @@ -37,6 +38,9 @@ type APIValidator struct {
versionRegex *regexp.Regexp
// urlFriendlyNameRegex matches URL-safe characters for API names
urlFriendlyNameRegex *regexp.Regexp
// upstreamRefRegex enforces the schema pattern for API-level and per-op
// upstream refs; definition names are checked by upstreamDefinitionNameRegex
upstreamRefRegex *regexp.Regexp
// policyValidator validates policy references and parameters
policyValidator *PolicyValidator
}
Expand All @@ -47,6 +51,7 @@ func NewAPIValidator() *APIValidator {
pathParamRegex: regexp.MustCompile(`\{[a-zA-Z0-9_]+\}`),
versionRegex: regexp.MustCompile(`^v?\d+(\.\d+)?(\.\d+)?$`),
urlFriendlyNameRegex: regexp.MustCompile(`^[a-zA-Z0-9\-_\. ]+$`),
upstreamRefRegex: regexp.MustCompile(`^[a-zA-Z0-9\-_]+$`),
}
}

Expand Down Expand Up @@ -178,6 +183,36 @@ func (v *APIValidator) validateUpstreamUrl(label string, upUrl *string) []Valida
return errors
}

// validateUpstreamRefName enforces the shared UpstreamReference name contract
// (max 100 characters, ^[a-zA-Z0-9\-_]+$) on definition names and refs. The
// message names the field from the trailing segment of its path.
func (v *APIValidator) validateUpstreamRefName(field, value string) []ValidationError {
name := fieldName(field)
if len(value) > 100 {
return []ValidationError{{
Field: field,
Message: fmt.Sprintf("%s must not exceed %d characters", name, 100),
}}
}
if !v.upstreamRefRegex.MatchString(value) {
return []ValidationError{{
Field: field,
Message: name + " must match pattern " + v.upstreamRefRegex.String(),
}}
}
return nil
}

// fieldName returns the trailing path segment of a validation field path
// (for example "spec.operations[2].upstream.main.ref" yields "ref") so error
// messages can name the field without a caller-supplied label.
func fieldName(field string) string {
if i := strings.LastIndex(field, "."); i >= 0 {
return field[i+1:]
}
return field
}

func (v *APIValidator) validateUpstreamRef(label string, ref *string, upstreamDefinitions *[]api.UpstreamDefinition) []ValidationError {
var errors []ValidationError

Expand All @@ -192,7 +227,10 @@ func (v *APIValidator) validateUpstreamRef(label string, ref *string, upstreamDe

refName := strings.TrimSpace(*ref)

// Check if upstream definitions are provided
if errs := v.validateUpstreamRefName("spec.upstream."+label+".ref", refName); errs != nil {
return errs
}

if upstreamDefinitions == nil || len(*upstreamDefinitions) == 0 {
errors = append(errors, ValidationError{
Field: "spec.upstream." + label + ".ref",
Expand All @@ -201,16 +239,8 @@ func (v *APIValidator) validateUpstreamRef(label string, ref *string, upstreamDe
return errors
}

// Check if the referenced definition exists
found := false
for _, def := range *upstreamDefinitions {
if def.Name == refName {
found = true
break
}
}

if !found {
// Resolve via the shared upstreamref helper so API-level, per-op, and translator lookups match.
if _, err := upstreamref.FindByName(refName, upstreamDefinitions); err != nil {
errors = append(errors, ValidationError{
Field: "spec.upstream." + label + ".ref",
Message: fmt.Sprintf("Referenced upstream definition '%s' not found in upstreamDefinitions", refName),
Expand Down Expand Up @@ -466,7 +496,7 @@ func (v *APIValidator) validateRestData(spec *api.APIConfigData) []ValidationErr
errors = append(errors, v.validateResilience("spec.resilience", spec.Resilience)...)

// Validate operations
errors = append(errors, v.validateOperations(spec.Context, spec.Operations)...)
errors = append(errors, v.validateOperations(spec.Context, spec.Operations, spec.UpstreamDefinitions)...)

return errors
}
Expand Down Expand Up @@ -565,7 +595,7 @@ func (v *APIValidator) ValidateContext(context string) []ValidationError {
// gateway-controller/pkg/xds/translator.go) — that namespace is reserved for the
// gateway's own /ready and /healthy direct-response routes, and must never be
// reachable by anything an API defines.
func (v *APIValidator) validateOperations(context string, operations []api.Operation) []ValidationError {
func (v *APIValidator) validateOperations(context string, operations []api.Operation, upstreamDefinitions *[]api.UpstreamDefinition) []ValidationError {
var errors []ValidationError

if len(operations) == 0 {
Expand Down Expand Up @@ -637,6 +667,9 @@ func (v *APIValidator) validateOperations(context string, operations []api.Opera

// Validate operation-level resilience block
errors = append(errors, v.validateResilience(fmt.Sprintf("spec.operations[%d].resilience", i), op.Resilience)...)

// Validate per-operation upstream override (main / sandbox)
errors = append(errors, v.validateOperationUpstream(i, op.Upstream, upstreamDefinitions)...)
}

return errors
Expand All @@ -651,6 +684,59 @@ func joinContextPath(context, opPath string) string {
return strings.TrimSuffix(context, "/") + "/" + strings.TrimPrefix(opPath, "/")
}

// validateOperationUpstream validates the ref-only per-operation main/sandbox
// overrides; each present ref must name an entry in upstreamDefinitions.
func (v *APIValidator) validateOperationUpstream(opIdx int, up *api.OperationUpstream, upstreamDefinitions *[]api.UpstreamDefinition) []ValidationError {
var errors []ValidationError
if up == nil {
return errors
}
if up.Main == nil && up.Sandbox == nil {
errors = append(errors, ValidationError{
Field: fmt.Sprintf("spec.operations[%d].upstream", opIdx),
Message: "At least one of 'main' or 'sandbox' must be set",
})
return errors
}
if up.Main != nil {
errs := v.validateOperationUpstreamRef(opIdx, "main", up.Main.Ref, upstreamDefinitions)
errors = append(errors, errs...)
}
if up.Sandbox != nil {
errs := v.validateOperationUpstreamRef(opIdx, "sandbox", up.Sandbox.Ref, upstreamDefinitions)
errors = append(errors, errs...)
}
return errors
}

// validateOperationUpstreamRef validates a single operation-level upstream ref.
// The ref must resolve to a named entry in upstreamDefinitions.
func (v *APIValidator) validateOperationUpstreamRef(opIdx int, env, ref string, upstreamDefinitions *[]api.UpstreamDefinition) []ValidationError {
field := fmt.Sprintf("spec.operations[%d].upstream.%s.ref", opIdx, env)

refName := strings.TrimSpace(ref)
if refName == "" {
return []ValidationError{{
Field: field,
Message: "Upstream ref is required",
}}
}

if errs := v.validateUpstreamRefName(field, refName); errs != nil {
return errs
}

// Resolve via the shared upstreamref helper (same lookup as the translators).
if _, err := upstreamref.FindByName(refName, upstreamDefinitions); err != nil {
return []ValidationError{{
Field: field,
Message: fmt.Sprintf("Referenced upstream definition '%s' not found in upstreamDefinitions", refName),
}}
}

return nil
}

// validatePathParameters checks if path parameters have balanced braces
func (v *APIValidator) validatePathParameters(path string) bool {
openCount := strings.Count(path, "{")
Expand Down
Loading