@@ -26,6 +26,7 @@ import (
2626 "time"
2727
2828 api "github.com/wso2/api-platform/gateway/gateway-controller/pkg/api/management"
29+ "github.com/wso2/api-platform/gateway/gateway-controller/pkg/utils/upstreamref"
2930)
3031
3132// APIValidator validates API configurations using rule-based validation
@@ -36,6 +37,8 @@ type APIValidator struct {
3637 versionRegex * regexp.Regexp
3738 // urlFriendlyNameRegex matches URL-safe characters for API names
3839 urlFriendlyNameRegex * regexp.Regexp
40+ // upstreamRefRegex enforces the schema pattern for per-op upstream refs
41+ upstreamRefRegex * regexp.Regexp
3942 // policyValidator validates policy references and parameters
4043 policyValidator * PolicyValidator
4144}
@@ -46,6 +49,7 @@ func NewAPIValidator() *APIValidator {
4649 pathParamRegex : regexp .MustCompile (`\{[a-zA-Z0-9_]+\}` ),
4750 versionRegex : regexp .MustCompile (`^v?\d+(\.\d+)?(\.\d+)?$` ),
4851 urlFriendlyNameRegex : regexp .MustCompile (`^[a-zA-Z0-9\-_\. ]+$` ),
52+ upstreamRefRegex : regexp .MustCompile (`^[a-zA-Z0-9\-_]+$` ),
4953 }
5054}
5155
@@ -244,16 +248,10 @@ func (v *APIValidator) validateUpstreamRef(label string, ref *string, upstreamDe
244248 return errors
245249 }
246250
247- // Check if the referenced definition exists
248- found := false
249- for _ , def := range * upstreamDefinitions {
250- if def .Name == refName {
251- found = true
252- break
253- }
254- }
255-
256- if ! found {
251+ // Check if the referenced definition exists. Use the shared upstreamref helper
252+ // for the membership lookup so API-level ref validation stays aligned with the
253+ // per-op validator and the translators (one source of truth for ref lookup).
254+ if _ , err := upstreamref .FindByName (refName , upstreamDefinitions ); err != nil {
257255 errors = append (errors , ValidationError {
258256 Field : "spec.upstream." + label + ".ref" ,
259257 Message : fmt .Sprintf ("Referenced upstream definition '%s' not found in upstreamDefinitions" , refName ),
@@ -294,6 +292,23 @@ func (v *APIValidator) validateUpstreamDefinitions(definitions *[]api.UpstreamDe
294292 }
295293 namesSeen [def .Name ] = true
296294
295+ // Enforce the same name contract the schema declares and that operation-level
296+ // refs are validated against (^[a-zA-Z0-9\-_]+$, max 100 chars), so any valid
297+ // definition name stays referenceable from a per-op upstream override.
298+ if len (def .Name ) > 100 {
299+ errors = append (errors , ValidationError {
300+ Field : fmt .Sprintf ("spec.upstreamDefinitions[%d].name" , i ),
301+ Message : "Upstream definition name must not exceed 100 characters" ,
302+ })
303+ continue
304+ } else if ! v .upstreamRefRegex .MatchString (def .Name ) {
305+ errors = append (errors , ValidationError {
306+ Field : fmt .Sprintf ("spec.upstreamDefinitions[%d].name" , i ),
307+ Message : "Upstream definition name must match pattern ^[a-zA-Z0-9\\ -_]+$" ,
308+ })
309+ continue
310+ }
311+
297312 // Validate upstreams array
298313 if len (def .Upstreams ) == 0 {
299314 errors = append (errors , ValidationError {
@@ -356,15 +371,23 @@ func (v *APIValidator) validateUpstreamDefinitions(definitions *[]api.UpstreamDe
356371
357372 // Timeout validation is limited to connect timeout; request and idle
358373 // timeouts are no longer supported at the upstream definition level.
374+ // Parsed inline rather than via upstreamref.ParseConnectTimeout so the two
375+ // distinct, tested messages below (invalid-format vs non-positive) are kept;
376+ // the shared helper collapses both into a single message.
359377 if def .Timeout != nil && def .Timeout .Connect != nil {
360378 timeoutStr := strings .TrimSpace (* def .Timeout .Connect )
361379 if timeoutStr != "" {
362- _ , err := time .ParseDuration (timeoutStr )
380+ d , err := time .ParseDuration (timeoutStr )
363381 if err != nil {
364382 errors = append (errors , ValidationError {
365383 Field : fmt .Sprintf ("spec.upstreamDefinitions[%d].timeout.connect" , i ),
366384 Message : fmt .Sprintf ("Invalid timeout format: %v (expected format: '30s', '1m', '500ms')" , err ),
367385 })
386+ } else if d <= 0 {
387+ errors = append (errors , ValidationError {
388+ Field : fmt .Sprintf ("spec.upstreamDefinitions[%d].timeout.connect" , i ),
389+ Message : "Connect timeout must be a positive duration" ,
390+ })
368391 }
369392 }
370393 }
@@ -421,7 +444,7 @@ func (v *APIValidator) validateRestData(spec *api.APIConfigData) []ValidationErr
421444 }
422445
423446 // Validate operations
424- errors = append (errors , v .validateOperations (spec .Operations )... )
447+ errors = append (errors , v .validateOperations (spec .Operations , spec . UpstreamDefinitions )... )
425448
426449 return errors
427450}
@@ -552,7 +575,7 @@ func (v *APIValidator) validatePathParametersForAsyncAPIs(path string) bool {
552575}
553576
554577// validateOperations validates the operations configuration
555- func (v * APIValidator ) validateOperations (operations []api.Operation ) []ValidationError {
578+ func (v * APIValidator ) validateOperations (operations []api.Operation , upstreamDefinitions * []api. UpstreamDefinition ) []ValidationError {
556579 var errors []ValidationError
557580
558581 if len (operations ) == 0 {
@@ -605,11 +628,83 @@ func (v *APIValidator) validateOperations(operations []api.Operation) []Validati
605628 Message : "Operation path has unbalanced braces in parameters" ,
606629 })
607630 }
631+
632+ // Validate per-operation upstream override (main / sandbox)
633+ if op .Upstream != nil {
634+ errors = append (errors , v .validateOperationUpstream (i , op .Upstream , upstreamDefinitions )... )
635+ }
608636 }
609637
610638 return errors
611639}
612640
641+ // validateOperationUpstream validates per-operation upstream main and sandbox
642+ // sub-fields. Operation-level upstreams are ref-only — direct URLs are not
643+ // permitted. Each present sub-field must reference a named entry in
644+ // spec.upstreamDefinitions. Error field paths are built as
645+ // spec.operations[N].upstream.<subfield>.ref.
646+ func (v * APIValidator ) validateOperationUpstream (opIdx int , up * api.RestAPIOperationUpstream , upstreamDefinitions * []api.UpstreamDefinition ) []ValidationError {
647+ var errors []ValidationError
648+ if up == nil {
649+ return errors
650+ }
651+ if up .Main == nil && up .Sandbox == nil {
652+ errors = append (errors , ValidationError {
653+ Field : fmt .Sprintf ("spec.operations[%d].upstream" , opIdx ),
654+ Message : "At least one of 'main' or 'sandbox' must be set" ,
655+ })
656+ return errors
657+ }
658+ if up .Main != nil {
659+ errs := v .validateOperationUpstreamTarget (opIdx , "main" , up .Main , upstreamDefinitions )
660+ errors = append (errors , errs ... )
661+ }
662+ if up .Sandbox != nil {
663+ errs := v .validateOperationUpstreamTarget (opIdx , "sandbox" , up .Sandbox , upstreamDefinitions )
664+ errors = append (errors , errs ... )
665+ }
666+ return errors
667+ }
668+
669+ // validateOperationUpstreamTarget validates a single ref-only operation-level
670+ // upstream target. The ref must resolve to a named entry in upstreamDefinitions.
671+ func (v * APIValidator ) validateOperationUpstreamTarget (opIdx int , sub string , target * api.RestAPIOperationUpstreamTarget , upstreamDefinitions * []api.UpstreamDefinition ) []ValidationError {
672+ field := fmt .Sprintf ("spec.operations[%d].upstream.%s.ref" , opIdx , sub )
673+
674+ refName := strings .TrimSpace (target .Ref )
675+ if refName == "" {
676+ return []ValidationError {{
677+ Field : field ,
678+ Message : "Upstream ref is required" ,
679+ }}
680+ }
681+
682+ if len (refName ) > 100 {
683+ return []ValidationError {{
684+ Field : field ,
685+ Message : "Upstream ref must not exceed 100 characters" ,
686+ }}
687+ }
688+
689+ if ! v .upstreamRefRegex .MatchString (refName ) {
690+ return []ValidationError {{
691+ Field : field ,
692+ Message : "Upstream ref must match pattern ^[a-zA-Z0-9\\ -_]+$" ,
693+ }}
694+ }
695+
696+ // Resolve through the shared upstreamref helper so the validator stays aligned
697+ // with the xDS translator and RDC transformer (one source of truth for ref lookup).
698+ if _ , err := upstreamref .FindByName (refName , upstreamDefinitions ); err != nil {
699+ return []ValidationError {{
700+ Field : field ,
701+ Message : fmt .Sprintf ("Referenced upstream definition '%s' not found in upstreamDefinitions" , refName ),
702+ }}
703+ }
704+
705+ return nil
706+ }
707+
613708// validatePathParameters checks if path parameters have balanced braces
614709func (v * APIValidator ) validatePathParameters (path string ) bool {
615710 openCount := strings .Count (path , "{" )
0 commit comments