Skip to content

Commit 6595bc3

Browse files
committed
fix: include postgres instance ports in validation check
This ensures all instances (database and service) avoid port conflicts.
1 parent f5075a1 commit 6595bc3

File tree

2 files changed

+118
-3
lines changed

2 files changed

+118
-3
lines changed

server/internal/api/apiv1/validate.go

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,11 @@ func validateDatabaseSpec(spec *api.DatabaseSpec) error {
129129
// Validate orchestrator_opts (spec-level)
130130
errs = append(errs, validateOrchestratorOpts(spec.OrchestratorOpts, []string{"orchestrator_opts"})...)
131131

132-
// Validate services
133-
seenServiceIDs := make(ds.Set[string], len(spec.Services))
132+
// Validate services — seed portOwner with Postgres ports so services can't collide with the database.
134133
portOwner := make(servicePortOwnerMap)
134+
seedPostgresPorts(spec, portOwner)
135135

136+
seenServiceIDs := make(ds.Set[string], len(spec.Services))
136137
for i, svc := range spec.Services {
137138
svcPath := []string{"services", arrayIndexPath(i)}
138139

@@ -199,10 +200,13 @@ func validateDatabaseUpdate(old *database.Spec, new *api.DatabaseSpec) error {
199200
existingServiceIDs.Add(svc.ServiceID)
200201
}
201202

203+
// Seed portOwner with Postgres ports so services can't collide with the database.
204+
portOwner := make(servicePortOwnerMap)
205+
seedPostgresPorts(new, portOwner)
206+
202207
// Validate each service. Pass isUpdate=false for services being added for the
203208
// first time so that bootstrap-only fields are accepted. For service types that
204209
// have no bootstrap fields (e.g. postgrest) the flag has no effect.
205-
portOwner := make(servicePortOwnerMap)
206210
for i, svc := range new.Services {
207211
svcPath := []string{"services", arrayIndexPath(i)}
208212
isExistingService := existingServiceIDs.Has(string(svc.ServiceID))
@@ -484,6 +488,24 @@ func validateUsers(users []*api.DatabaseUserSpec, path []string) []error {
484488
return errs
485489
}
486490

491+
// seedPostgresPorts registers each node's effective Postgres port in the
492+
// portOwner map so that service port validation can detect collisions with
493+
// the database. A node-level port override (node.Port) takes precedence
494+
// over the spec-level default (spec.Port).
495+
func seedPostgresPorts(spec *api.DatabaseSpec, owner servicePortOwnerMap) {
496+
for _, node := range spec.Nodes {
497+
pgPort := utils.FromPointer(spec.Port)
498+
if node.Port != nil {
499+
pgPort = *node.Port
500+
}
501+
if pgPort > 0 {
502+
for _, hostID := range node.HostIds {
503+
owner[hostPort{hostID: string(hostID), port: pgPort}] = "postgres"
504+
}
505+
}
506+
}
507+
}
508+
487509
// hostPort identifies a unique (host, port) binding for cross-service
488510
// port conflict detection.
489511
type hostPort struct {

server/internal/api/apiv1/validate_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,99 @@ func TestValidateDatabaseSpec(t *testing.T) {
817817
},
818818
},
819819
},
820+
{
821+
name: "invalid service port conflicts with postgres port",
822+
spec: &api.DatabaseSpec{
823+
DatabaseName: "testdb",
824+
PostgresVersion: utils.PointerTo("17.6"),
825+
Port: utils.PointerTo(5432),
826+
Nodes: []*api.DatabaseNodeSpec{
827+
{
828+
Name: "n1",
829+
HostIds: []api.Identifier{"host-1"},
830+
},
831+
},
832+
Services: []*api.ServiceSpec{
833+
{
834+
ServiceID: "mcp-server",
835+
ServiceType: "mcp",
836+
Version: "1.0.0",
837+
HostIds: []api.Identifier{"host-1"},
838+
Port: utils.PointerTo(5432),
839+
Config: map[string]any{
840+
"llm_enabled": true,
841+
"llm_provider": "anthropic",
842+
"llm_model": "claude-sonnet-4-5",
843+
"anthropic_api_key": "sk-ant-...",
844+
},
845+
},
846+
},
847+
},
848+
expected: []string{
849+
`port 5432 conflicts with service "postgres" on the same host`,
850+
},
851+
},
852+
{
853+
name: "invalid service port conflicts with node-level postgres port override",
854+
spec: &api.DatabaseSpec{
855+
DatabaseName: "testdb",
856+
PostgresVersion: utils.PointerTo("17.6"),
857+
Nodes: []*api.DatabaseNodeSpec{
858+
{
859+
Name: "n1",
860+
HostIds: []api.Identifier{"host-1"},
861+
Port: utils.PointerTo(5433),
862+
},
863+
},
864+
Services: []*api.ServiceSpec{
865+
{
866+
ServiceID: "mcp-server",
867+
ServiceType: "mcp",
868+
Version: "1.0.0",
869+
HostIds: []api.Identifier{"host-1"},
870+
Port: utils.PointerTo(5433),
871+
Config: map[string]any{
872+
"llm_enabled": true,
873+
"llm_provider": "anthropic",
874+
"llm_model": "claude-sonnet-4-5",
875+
"anthropic_api_key": "sk-ant-...",
876+
},
877+
},
878+
},
879+
},
880+
expected: []string{
881+
`port 5433 conflicts with service "postgres" on the same host`,
882+
},
883+
},
884+
{
885+
name: "valid service port on different host than postgres",
886+
spec: &api.DatabaseSpec{
887+
DatabaseName: "testdb",
888+
PostgresVersion: utils.PointerTo("17.6"),
889+
Port: utils.PointerTo(5432),
890+
Nodes: []*api.DatabaseNodeSpec{
891+
{
892+
Name: "n1",
893+
HostIds: []api.Identifier{"host-1"},
894+
},
895+
},
896+
Services: []*api.ServiceSpec{
897+
{
898+
ServiceID: "mcp-server",
899+
ServiceType: "mcp",
900+
Version: "1.0.0",
901+
HostIds: []api.Identifier{"host-2"},
902+
Port: utils.PointerTo(5432),
903+
Config: map[string]any{
904+
"llm_enabled": true,
905+
"llm_provider": "anthropic",
906+
"llm_model": "claude-sonnet-4-5",
907+
"anthropic_api_key": "sk-ant-...",
908+
},
909+
},
910+
},
911+
},
912+
},
820913
{
821914
name: "invalid with service validation errors",
822915
spec: &api.DatabaseSpec{

0 commit comments

Comments
 (0)