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
62 changes: 61 additions & 1 deletion server/internal/api/apiv1/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,10 @@ func validateDatabaseSpec(spec *api.DatabaseSpec) error {
// Validate orchestrator_opts (spec-level)
errs = append(errs, validateOrchestratorOpts(spec.OrchestratorOpts, []string{"orchestrator_opts"})...)

// Validate services
// Validate services — seed portOwner with Postgres ports so services can't collide with the database.
portOwner := make(servicePortOwnerMap)
seedPostgresPorts(spec, portOwner)

seenServiceIDs := make(ds.Set[string], len(spec.Services))
for i, svc := range spec.Services {
svcPath := []string{"services", arrayIndexPath(i)}
Expand All @@ -141,6 +144,7 @@ func validateDatabaseSpec(spec *api.DatabaseSpec) error {
}
seenServiceIDs.Add(string(svc.ServiceID))

errs = append(errs, validateServicePortConflicts(svc, svcPath, portOwner)...)
Comment thread
rshoemaker marked this conversation as resolved.
errs = append(errs, validateServiceSpec(svc, svcPath, false, seenNodeNames)...)
}

Expand Down Expand Up @@ -196,12 +200,18 @@ func validateDatabaseUpdate(old *database.Spec, new *api.DatabaseSpec) error {
existingServiceIDs.Add(svc.ServiceID)
}

// Seed portOwner with Postgres ports so services can't collide with the database.
portOwner := make(servicePortOwnerMap)
seedPostgresPorts(new, portOwner)

// Validate each service. Pass isUpdate=false for services being added for the
// first time so that bootstrap-only fields are accepted. For service types that
// have no bootstrap fields (e.g. postgrest) the flag has no effect.
for i, svc := range new.Services {
svcPath := []string{"services", arrayIndexPath(i)}
isExistingService := existingServiceIDs.Has(string(svc.ServiceID))

errs = append(errs, validateServicePortConflicts(svc, svcPath, portOwner)...)
errs = append(errs, validateServiceSpec(svc, svcPath, isExistingService, newNodeNames)...)
}

Expand Down Expand Up @@ -478,6 +488,56 @@ func validateUsers(users []*api.DatabaseUserSpec, path []string) []error {
return errs
}

// seedPostgresPorts registers each node's effective Postgres port in the
// portOwner map so that service port validation can detect collisions with
// the database. A node-level port override (node.Port) takes precedence
// over the spec-level default (spec.Port).
func seedPostgresPorts(spec *api.DatabaseSpec, owner servicePortOwnerMap) {
for _, node := range spec.Nodes {
pgPort := utils.FromPointer(spec.Port)
if node.Port != nil {
pgPort = *node.Port
}
if pgPort > 0 {
for _, hostID := range node.HostIds {
owner[hostPort{hostID: string(hostID), port: pgPort}] = "postgres"
}
}
}
}

// hostPort identifies a unique (host, port) binding for cross-service
// port conflict detection.
type hostPort struct {
hostID string
port int
}

// servicePortOwnerMap tracks which service owns a given (host, port) pair.
// Callers create one map and pass it to validateServicePortConflicts for
// each service in the spec.
type servicePortOwnerMap map[hostPort]string

// validateServicePortConflicts checks that the service's explicit port (if any)
// does not collide with a port already claimed by another service on the same host.
func validateServicePortConflicts(svc *api.ServiceSpec, path []string, owner servicePortOwnerMap) []error {
if svc.Port == nil || *svc.Port <= 0 {
return nil
}

var errs []error
for _, hostID := range svc.HostIds {
key := hostPort{hostID: string(hostID), port: *svc.Port}
if prev, exists := owner[key]; exists {
err := fmt.Errorf("port %d conflicts with service %q on the same host", *svc.Port, prev)
errs = append(errs, newValidationError(err, appendPath(path, "port")))
} else {
owner[key] = string(svc.ServiceID)
}
}
return errs
}

func validateBackupConfig(cfg *api.BackupConfigSpec, path []string) []error {
var errs []error

Expand Down
Loading