@@ -130,7 +130,10 @@ func validateDatabaseSpec(orchestrator config.Orchestrator, spec *api.DatabaseSp
130130 // Validate orchestrator_opts (spec-level)
131131 errs = append (errs , validateOrchestratorOpts (spec .OrchestratorOpts , []string {"orchestrator_opts" })... )
132132
133- // Validate services
133+ // Validate services — seed portOwner with Postgres ports so services can't collide with the database.
134+ portOwner := make (servicePortOwnerMap )
135+ seedPostgresPorts (spec , portOwner )
136+
134137 seenServiceIDs := make (ds.Set [string ], len (spec .Services ))
135138 for i , svc := range spec .Services {
136139 svcPath := []string {"services" , arrayIndexPath (i )}
@@ -142,6 +145,7 @@ func validateDatabaseSpec(orchestrator config.Orchestrator, spec *api.DatabaseSp
142145 }
143146 seenServiceIDs .Add (string (svc .ServiceID ))
144147
148+ errs = append (errs , validateServicePortConflicts (svc , svcPath , portOwner )... )
145149 errs = append (errs , validateServiceSpec (svc , svcPath , false , seenNodeNames )... )
146150 }
147151
@@ -197,12 +201,18 @@ func validateDatabaseUpdate(old *database.Spec, new *api.DatabaseSpec) error {
197201 existingServiceIDs .Add (svc .ServiceID )
198202 }
199203
204+ // Seed portOwner with Postgres ports so services can't collide with the database.
205+ portOwner := make (servicePortOwnerMap )
206+ seedPostgresPorts (new , portOwner )
207+
200208 // Validate each service. Pass isUpdate=false for services being added for the
201209 // first time so that bootstrap-only fields are accepted. For service types that
202210 // have no bootstrap fields (e.g. postgrest) the flag has no effect.
203211 for i , svc := range new .Services {
204212 svcPath := []string {"services" , arrayIndexPath (i )}
205213 isExistingService := existingServiceIDs .Has (string (svc .ServiceID ))
214+
215+ errs = append (errs , validateServicePortConflicts (svc , svcPath , portOwner )... )
206216 errs = append (errs , validateServiceSpec (svc , svcPath , isExistingService , newNodeNames )... )
207217 }
208218
@@ -504,6 +514,56 @@ func validateUsers(users []*api.DatabaseUserSpec, path []string) []error {
504514 return errs
505515}
506516
517+ // seedPostgresPorts registers each node's effective Postgres port in the
518+ // portOwner map so that service port validation can detect collisions with
519+ // the database. A node-level port override (node.Port) takes precedence
520+ // over the spec-level default (spec.Port).
521+ func seedPostgresPorts (spec * api.DatabaseSpec , owner servicePortOwnerMap ) {
522+ for _ , node := range spec .Nodes {
523+ pgPort := utils .FromPointer (spec .Port )
524+ if node .Port != nil {
525+ pgPort = * node .Port
526+ }
527+ if pgPort > 0 {
528+ for _ , hostID := range node .HostIds {
529+ owner [hostPort {hostID : string (hostID ), port : pgPort }] = "postgres"
530+ }
531+ }
532+ }
533+ }
534+
535+ // hostPort identifies a unique (host, port) binding for cross-service
536+ // port conflict detection.
537+ type hostPort struct {
538+ hostID string
539+ port int
540+ }
541+
542+ // servicePortOwnerMap tracks which service owns a given (host, port) pair.
543+ // Callers create one map and pass it to validateServicePortConflicts for
544+ // each service in the spec.
545+ type servicePortOwnerMap map [hostPort ]string
546+
547+ // validateServicePortConflicts checks that the service's explicit port (if any)
548+ // does not collide with a port already claimed by another service on the same host.
549+ func validateServicePortConflicts (svc * api.ServiceSpec , path []string , owner servicePortOwnerMap ) []error {
550+ if svc .Port == nil || * svc .Port <= 0 {
551+ return nil
552+ }
553+
554+ var errs []error
555+ for _ , hostID := range svc .HostIds {
556+ key := hostPort {hostID : string (hostID ), port : * svc .Port }
557+ if prev , exists := owner [key ]; exists {
558+ err := fmt .Errorf ("port %d conflicts with service %q on the same host" , * svc .Port , prev )
559+ errs = append (errs , newValidationError (err , appendPath (path , "port" )))
560+ } else {
561+ owner [key ] = string (svc .ServiceID )
562+ }
563+ }
564+ return errs
565+ }
566+
507567func validateBackupConfig (cfg * api.BackupConfigSpec , path []string ) []error {
508568 var errs []error
509569
0 commit comments