Skip to content

Feat: add PostgREST as a service type (PLAT-499, PLAT-500,PLAT-501,PLAT-502,PLAT-503)#300

Merged
moizpgedge merged 7 commits intomainfrom
feat/PLAT-499/PLAT-500/PostgREST-prerequisites/image-wiring
Apr 3, 2026
Merged

Feat: add PostgREST as a service type (PLAT-499, PLAT-500,PLAT-501,PLAT-502,PLAT-503)#300
moizpgedge merged 7 commits intomainfrom
feat/PLAT-499/PLAT-500/PostgREST-prerequisites/image-wiring

Conversation

@moizpgedge
Copy link
Copy Markdown
Contributor

@moizpgedge moizpgedge commented Mar 17, 2026

Implements PostgREST as a deployable service type in the Control Plane, covering image registry, schema/role preflight validation, container spec, config file generation, and service user provisioning.

PLAT-499
PLAT-500
PLAT-501
PLAT-502
PLAT-503

Summary

  1. Adds postgrest as a valid service type alongside mcp
  2. Registers the PostgREST image pinned to 14.5 via ghcr.io/pgedge/postgrest:14.5
  3. Validates schemas and anonymous role exist in Postgres before deployment
  4. Generates a postgrest.conf via a Go template written to the service data directory
  5. Provisions a PostgreSQL authenticator role (LOGIN NOINHERIT) with the configured anon role granted to it, plus CONNECT on the database and USAGE/SELECT on all configured schemas
  6. Runs PostgREST as UID 1000 (matching the official Dockerfile) with PGRST_SERVER_HOST=0.0.0.0 so the postgrest --ready health check works correctly
  7. All PostgREST config fields are optional with sensible defaults: db_schemas → "public", db_anon_role → "pgedge_application_read_only", db_pool → 10, max_rows → 1000

A minimal deployment requires only:

"config": {}
Optional fields:

"config": {
  "db_schemas": "api",
  "db_anon_role": "web_anon"
}

Changes

  1. API & validation (PLAT-499): add postgrest to the service_type enum; extend serviceVersionPattern and semverPattern to accept two-part versions (\d+.\d+); add validatePostgRESTServiceConfig()
  2. Image registry (PLAT-501): register postgrest:14.5 in NewServiceVersions; consistent with MCP pattern
  3. Schema/role preflight (PLAT-500): PostgRESTPreflightResource connects to the primary (with Patroni-based primary discovery and reachable-instance fallback) and verifies all configured schemas and the anon role exist before the plan step; missing objects fail fast with a clear error, e.g. schema "api" does not exist in database "storefront"
  4. Container spec (PLAT-502): ServiceContainerSpec gains a PostgREST case — postgrest command with bind-mounted read-only config, PGRST_* env vars built from DatabaseHosts, postgrest --ready health check, UID 1000
  5. Config resource (PLAT-502): PostgRESTConfigResource writes postgrest.conf from a Go template on every apply, chowned to UID 1000
  6. Service user role (PLAT-503): ServiceUserRole extended with ServiceType and DBAnonRole; PostgREST path creates LOGIN NOINHERIT authenticator role and applies schema-level grants; MCP path unchanged (uses group role membership)
  7. Resource registration: PostgRESTConfigResource and PostgRESTPreflightResource registered in RegisterResourceTypes()
  8. Instance spec: Dependencies() and populateCredentials() switch on ServiceType to use the correct config resource identifier and credential role label
## Testing

go test ./server/internal/orchestrator/swarm/... -v
go test ./server/internal/api/apiv1/... -v
go test ./server/internal/workflows/...


# Unit tests

go test ./server/internal/orchestrator/swarm/... -run "TestGeneratePostgRESTConfig|TestServiceContainerSpec|TestRoleAttributesAndGrants" -v
go test ./server/internal/database/... -run TestParsePostgRESTServiceConfig -v

Deploy PostgREST service (once storefront database exists)

restish control-plane-local-1 update-database storefront '{
  "spec": {
    "database_name": "storefront",
    "database_users": [{"username": "admin", "password": "password", "db_owner": true, "attributes": ["LOGIN", "SUPERUSER"]}],
    "nodes": [
      {"name": "n1", "host_ids": ["host-1"]},
      {"name": "n2", "host_ids": ["host-2"]},
      {"name": "n3", "host_ids": ["host-3"]}
    ],
    "services": [{
      "service_id": "postgrest",
      "service_type": "postgrest",
      "version": "14.5",
      "host_ids": ["host-1"],
      "config": {}
    }]
  }
}'

Checklist

  • Tests added or updated (unit and/or e2e, as needed)
  • Documentation updated (if needed)
  • Issue is linked (branch name or URL in PR description)
  • Changelog entry added for user-facing behavior changes
  • Breaking changes (if any) are clearly called out in the PR description

Notes for Reviewers

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 17, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds PostgREST support to the swarm orchestrator: registers postgrest:14.5 image, accepts two-segment versions like 14.5, introduces Preflight/Config/Authenticator resources, extends service/container spec for PostgREST, adds config generation for postgrest.conf, and updates tests accordingly.

Changes

Cohort / File(s) Summary
Version & API validation
api/apiv1/design/database.go, server/internal/api/apiv1/validate.go, server/internal/api/apiv1/validate_test.go
Relaxed version regex to allow major.minor (e.g., 14.5) in addition to major.minor.patch and latest; updated docs and added validation test for postgrest 14.5.
Service images & registry
server/internal/orchestrator/swarm/service_images.go, server/internal/orchestrator/swarm/service_images_test.go, server/internal/orchestrator/swarm/resources.go
Registered postgrest:14.5 image (tag via serviceImageTag) and added test case; registered PostgREST resource types in the resource registry.
Service container spec & instance
server/internal/orchestrator/swarm/service_spec.go, server/internal/orchestrator/swarm/service_spec_test.go, server/internal/orchestrator/swarm/service_instance_spec.go
Added PostgREST container support (UID, env builder, healthcheck, command/args, bind mount mode); ServiceContainerSpec now switches on ServiceType; options gained DatabaseHosts and TargetSessionAttrs; tests updated/added; instance spec now includes service-specific dependencies.
PostgREST resources
server/internal/orchestrator/swarm/postgrest_preflight_resource.go, server/internal/orchestrator/swarm/postgrest_config_resource.go, server/internal/orchestrator/swarm/postgrest_authenticator_resource.go
Introduced PostgRESTPreflightResource (schema/role validation), PostgRESTConfigResource (generate/write postgrest.conf on host), and PostgRESTAuthenticatorResource (manage authenticator role and grants), with resource type constants and identifier helpers.
Orchestrator resource composition
server/internal/orchestrator/swarm/orchestrator.go
Refactored service resource generation to switch on ServiceType, building and appending service-specific resource lists for mcp and postgrest; added postgrest-specific resource creation and per-node authenticator handling.
User & role handling
server/internal/orchestrator/swarm/service_user_role.go, server/internal/orchestrator/swarm/service_user_role_test.go
Adjusted DiffIgnore to include /service_type and /db_anon_role; removed logger parameter from createUserRole internal call; minor test whitespace change.
PostgREST config generation & tests
server/internal/database/postgrest_service_config.go, server/internal/database/postgrest_service_config_test.go
Added PostgRESTConnParams and (*PostgRESTServiceConfig).GenerateConf; builds multi-host libpq db-uri with optional target_session_attrs, conditionally emits JWT/CORS directives; extensive tests added to validate output.

Poem

🐇 I nibble bytes and schema seeds,
I craft a conf where PostgREST feeds.
Fourteen dot five hops in a line,
Roles and grants arranged just fine,
I twitch my whiskers—swarm's set to shine.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.95% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding PostgREST as a new service type with relevant ticket references.
Description check ✅ Passed The description provides a comprehensive summary, detailed changes, testing instructions, and links all related issues, following the template structure well.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/PLAT-499/PLAT-500/PostgREST-prerequisites/image-wiring

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/internal/orchestrator/swarm/service_images_test.go`:
- Around line 109-114: The test case named "postgrest service has versions"
currently only checks the length; change it to assert the actual returned
versions from SupportedServiceVersions("postgrest") rather than just count:
collect the map keys returned by SupportedServiceVersions, sort them (or compare
as an unordered set) and assert they equal the expected set {"latest","v14.5"}
so the test fails if any other two-element combination is returned; update the
assertion in the test function in service_images_test.go accordingly (use the
test helper or testify/reflect-based equality as appropriate).

In `@server/internal/workflows/activities/validate_postgrest_prereqs.go`:
- Around line 134-150: The code currently falls back to db.Instances[0] when
Patroni discovery fails; instead, iterate db.Instances and pick the first
instance whose connection info successfully resolves via
a.DatabaseService.GetInstanceConnectionInfo (and optionally verify
reachability), then assign its InstanceID to primaryInstanceID. Update the
fallback block so that if primaryInstanceID == "" you loop through db.Instances,
call a.DatabaseService.GetInstanceConnectionInfo(ctx, databaseID,
inst.InstanceID), use the first call that returns no error (and/or a lightweight
reachability check) to set primaryInstanceID, rather than unconditionally using
db.Instances[0]; keep references to patroni.NewClient and
database.GetPrimaryInstanceID unchanged.

In `@server/internal/workflows/update_database.go`:
- Around line 106-117: The code currently ignores parseErrs from
ParsePostgRESTServiceConfig and only runs preflight when parseErrs is empty;
change this to fail fast when parseErrs is non-empty: if
ParsePostgRESTServiceConfig returns any parseErrs, return an error (wrap via
handleError) describing the invalid PostgREST config and include parseErrs
details so the workflow stops before planning; keep the existing preflight call
(ExecuteValidatePostgRESTPrereqs with ValidatePostgRESTPrereqsInput using
cfg.DBSchemas and cfg.DBAnonRole) for the valid-path case.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fc966f26-8fa1-4b2b-acdb-4b3e618ba438

📥 Commits

Reviewing files that changed from the base of the PR and between e35708d and b8b714d.

📒 Files selected for processing (5)
  • server/internal/orchestrator/swarm/service_images.go
  • server/internal/orchestrator/swarm/service_images_test.go
  • server/internal/workflows/activities/activities.go
  • server/internal/workflows/activities/validate_postgrest_prereqs.go
  • server/internal/workflows/update_database.go

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Registers PostgREST service images in the Swarm service image registry and adds a preflight workflow activity that validates required Postgres schemas and the configured anonymous role exist before provisioning PostgREST.

Changes:

  • Add ValidatePostgRESTPrereqs workflow activity to connect to the database and verify configured schemas + anon role exist.
  • Run the PostgREST preflight checks in UpdateDatabase before planning/provisioning.
  • Register PostgREST service image versions and extend Swarm service image tests accordingly.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
server/internal/workflows/update_database.go Runs PostgREST prerequisite validation before the plan/apply phases.
server/internal/workflows/activities/validate_postgrest_prereqs.go New activity that connects to a DB instance and checks schemas + anon role existence.
server/internal/workflows/activities/activities.go Registers the new ValidatePostgRESTPrereqs activity with the worker.
server/internal/orchestrator/swarm/service_images.go Adds PostgREST image entries (latest, v14.5) to the service image registry.
server/internal/orchestrator/swarm/service_images_test.go Adds/updates tests to cover PostgREST image resolution and version listing expectations.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/internal/workflows/activities/validate_postgrest_prereqs.go`:
- Around line 160-163: The current call to
DatabaseService.GetInstanceConnectionInfo(databaseID, primaryInstanceID) returns
an error and bails out immediately; instead, when that call fails, fall back to
the previously-captured reachable connection info (e.g., reachableConnInfo /
reachableInstanceID) for the read-only checks: attempt to assign connInfo =
reachableConnInfo and proceed (optionally log or warn that primary lookup failed
and reachable instance is being used); only return the wrapped error if no
reachableConnInfo is available. Update the logic around connInfo,
primaryInstanceID, and reachableConnInfo in the ValidatePostgrestPrereqs flow to
prefer the primary result but accept the reachable fallback on error.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b76c1ad0-1ff4-4562-aa87-401f010c42b8

📥 Commits

Reviewing files that changed from the base of the PR and between b8b714d and 389adf9.

📒 Files selected for processing (4)
  • server/internal/orchestrator/swarm/service_images.go
  • server/internal/orchestrator/swarm/service_images_test.go
  • server/internal/workflows/activities/validate_postgrest_prereqs.go
  • server/internal/workflows/update_database.go

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@moizpgedge moizpgedge requested a review from rshoemaker March 17, 2026 15:20
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
server/internal/api/apiv1/validate.go (1)

260-264: Consider updating the error message to reflect two-part version support.

The error message references "semver format (e.g., '1.0.0')" but the validation now also accepts two-part versions like "14.5". Consider updating the message to be more accurate:

-		err := errors.New("version must be in semver format (e.g., '1.0.0') or 'latest'")
+		err := errors.New("version must be in format 'X.Y' or 'X.Y.Z' (e.g., '14.5', '1.0.0') or 'latest'")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/internal/api/apiv1/validate.go` around lines 260 - 264, The validation
error message for svc.Version is out of date: update the string created where
svc.Version is checked (the block using semverPattern, newValidationError,
appendPath, and errs) to accurately state that versions may be semver (e.g.,
"1.0.0"), two-part versions (e.g., "14.5"), or "latest" so the message matches
the accepted formats.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@server/internal/api/apiv1/validate.go`:
- Around line 260-264: The validation error message for svc.Version is out of
date: update the string created where svc.Version is checked (the block using
semverPattern, newValidationError, appendPath, and errs) to accurately state
that versions may be semver (e.g., "1.0.0"), two-part versions (e.g., "14.5"),
or "latest" so the message matches the accepted formats.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 90ed306b-89ae-47e2-b987-2070738d8d56

📥 Commits

Reviewing files that changed from the base of the PR and between 389adf9 and e80ba4c.

⛔ Files ignored due to path filters (7)
  • api/apiv1/gen/control_plane/service.go is excluded by !**/gen/**
  • api/apiv1/gen/http/control_plane/client/types.go is excluded by !**/gen/**
  • api/apiv1/gen/http/control_plane/server/types.go is excluded by !**/gen/**
  • api/apiv1/gen/http/openapi.json is excluded by !**/gen/**
  • api/apiv1/gen/http/openapi.yaml is excluded by !**/gen/**
  • api/apiv1/gen/http/openapi3.json is excluded by !**/gen/**
  • api/apiv1/gen/http/openapi3.yaml is excluded by !**/gen/**
📒 Files selected for processing (6)
  • api/apiv1/design/database.go
  • server/internal/api/apiv1/validate.go
  • server/internal/api/apiv1/validate_test.go
  • server/internal/orchestrator/swarm/service_images.go
  • server/internal/orchestrator/swarm/service_images_test.go
  • server/internal/workflows/activities/validate_postgrest_prereqs.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • server/internal/orchestrator/swarm/service_images_test.go
  • server/internal/workflows/activities/validate_postgrest_prereqs.go

@moizpgedge moizpgedge force-pushed the feat/PLAT-499/PLAT-500/PostgREST-prerequisites/image-wiring branch from 99689f0 to e80ba4c Compare March 18, 2026 14:49
@moizpgedge
Copy link
Copy Markdown
Contributor Author

moizpgedge commented Mar 19, 2026

Post Pr for PLAT-501/502/503

PR:-#303

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
server/internal/orchestrator/swarm/postgrest_preflight_resource.go (1)

74-81: Logger obtained but never used; error silently discarded.

The logger is retrieved but not used anywhere in the validate method. Additionally, the error from do.Invoke is discarded with _, which could mask DI configuration issues.

Consider either:

  1. Using the logger to log validation progress/results (e.g., schemas being checked, successful validation)
  2. Removing the logger invocation if it's not needed
♻️ Option A: Remove unused logger
 func (r *PostgRESTPreflightResource) validate(ctx context.Context, rc *resource.Context) error {
-	logger, _ := do.Invoke[zerolog.Logger](rc.Injector)
-
-	conn, err := connectToPrimaryDB(ctx, rc, r.DatabaseID, r.DatabaseName, logger)
+	conn, err := connectToPrimaryDB(ctx, rc, r.DatabaseID, r.DatabaseName, zerolog.Nop())
 	if err != nil {
 		return fmt.Errorf("preflight: failed to connect to database: %w", err)
 	}
♻️ Option B: Use logger and handle error
 func (r *PostgRESTPreflightResource) validate(ctx context.Context, rc *resource.Context) error {
-	logger, _ := do.Invoke[zerolog.Logger](rc.Injector)
+	logger, err := do.Invoke[zerolog.Logger](rc.Injector)
+	if err != nil {
+		return fmt.Errorf("failed to get logger: %w", err)
+	}
+	logger = logger.With().
+		Str("service_id", r.ServiceID).
+		Str("database_id", r.DatabaseID).
+		Logger()
+	logger.Debug().Msg("running PostgREST preflight validation")

 	conn, err := connectToPrimaryDB(ctx, rc, r.DatabaseID, r.DatabaseName, logger)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/internal/orchestrator/swarm/postgrest_preflight_resource.go` around
lines 74 - 81, The call to do.Invoke[zerolog.Logger](rc.Injector) in
PostgRESTPreflightResource.validate either needs its error handled and the
logger used, or the unused invocation removed; update the validate function to
call do.Invoke and check the returned error (not discard it) and then use the
logger to emit at least a start/ok/failure message around connectToPrimaryDB and
validation steps (include the database id/name from r.DatabaseID/r.DatabaseName
in logs), or simply delete the do.Invoke line if no logger is required—ensure
any retained logger variable is referenced so the compiler won’t flag it and
that DI errors from do.Invoke are returned instead of ignored.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@server/internal/orchestrator/swarm/postgrest_preflight_resource.go`:
- Around line 74-81: The call to do.Invoke[zerolog.Logger](rc.Injector) in
PostgRESTPreflightResource.validate either needs its error handled and the
logger used, or the unused invocation removed; update the validate function to
call do.Invoke and check the returned error (not discard it) and then use the
logger to emit at least a start/ok/failure message around connectToPrimaryDB and
validation steps (include the database id/name from r.DatabaseID/r.DatabaseName
in logs), or simply delete the do.Invoke line if no logger is required—ensure
any retained logger variable is referenced so the compiler won’t flag it and
that DI errors from do.Invoke are returned instead of ignored.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 11b93e64-2be7-4519-ad38-e99af521a779

📥 Commits

Reviewing files that changed from the base of the PR and between 99689f0 and c1b42cd.

📒 Files selected for processing (3)
  • server/internal/orchestrator/swarm/postgrest_preflight_resource.go
  • server/internal/orchestrator/swarm/resources.go
  • server/internal/orchestrator/swarm/service_user_role.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/internal/orchestrator/swarm/resources.go

@moizpgedge moizpgedge force-pushed the feat/PLAT-499/PLAT-500/PostgREST-prerequisites/image-wiring branch from c1b42cd to f8de470 Compare March 24, 2026 13:26
Copy link
Copy Markdown
Member

@jason-lynch jason-lynch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! This is looking pretty close.

jason-lynch
jason-lynch previously approved these changes Mar 26, 2026
Copy link
Copy Markdown
Member

@jason-lynch jason-lynch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome!

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 22 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

// satisfied the resource is considered up-to-date (no permadrift).
func (r *PostgRESTPreflightResource) Refresh(ctx context.Context, rc *resource.Context) error {
if err := r.validate(ctx, rc); err != nil {
return fmt.Errorf("%w: %s", resource.ErrNotFound, err.Error())
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refresh wraps validation failures as fmt.Errorf("%w: %s", resource.ErrNotFound, err.Error()), which stringifies and drops the underlying error chain. Prefer wrapping the original error value (e.g., using %v or joining errors) so callers can inspect/unwrap and so multi-error context from errors.Join is preserved more faithfully.

Suggested change
return fmt.Errorf("%w: %s", resource.ErrNotFound, err.Error())
return errors.Join(resource.ErrNotFound, err)

Copilot uses AI. Check for mistakes.
Comment on lines +171 to +197
if r.ServiceType == "postgrest" {
attributes, grants := r.roleAttributesAndGrants()
statements, err := postgres.CreateUserRole(postgres.UserRoleOptions{
Name: r.Username,
Password: r.Password,
DBOwner: false,
Attributes: attributes,
})
if err != nil {
return fmt.Errorf("failed to generate create user role statements: %w", err)
}
if err := statements.Exec(ctx, conn); err != nil {
return fmt.Errorf("failed to create service user: %w", err)
}
if err := grants.Exec(ctx, conn); err != nil {
return fmt.Errorf("failed to grant service user permissions: %w", err)
}
} else {
var groupRole string
switch r.Mode {
case ServiceUserRoleRO:
groupRole = "pgedge_application_read_only"
case ServiceUserRoleRW:
groupRole = "pgedge_application"
default:
return fmt.Errorf("unknown service user role mode: %q", r.Mode)
}
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createUserRole treats any ServiceType other than exactly "postgrest" as the MCP/group-role path. This means typos or unexpected values in ServiceType will silently grant the wrong permissions. Consider switching on ServiceType and returning an explicit error for unknown non-empty values (and/or defaulting empty to "mcp" explicitly) to avoid misprovisioning.

Copilot uses AI. Check for mistakes.
}

func validateS3RepoProperties(props repoProperties, path []string) []error {
var errs []error
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

semverPattern now allows two-part versions (e.g. 14.5), but validateServiceSpec still produces the error message "version must be in semver format (e.g., '1.0.0') or 'latest'". Update the message to reflect the accepted formats (e.g. '1.0.0', '14.5', or 'latest') so API validation feedback matches the actual pattern.

Copilot uses AI. Check for mistakes.
@moizpgedge moizpgedge force-pushed the feat/PLAT-499/PLAT-500/PostgREST-prerequisites/image-wiring branch from 2f2e1bf to 069f53b Compare March 30, 2026 16:38
@moizpgedge moizpgedge changed the title feat: PostgREST image registry and schema preflight (PLAT-499, PLAT-500) eat: add PostgREST as a service type (PLAT-499, PLAT-500,PLAT-501,PLAT-502,PLAT-503) Mar 30, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
server/internal/orchestrator/swarm/service_spec_test.go (1)

384-400: Consider strengthening PGRST_DB_URI assertion.

The current check only verifies the value starts with "postgresql://". Consider using strings.HasPrefix or a more complete assertion to ensure the URI format is correct:

checks := map[string]string{
    // ...
}
// Special case for PGRST_DB_URI prefix check
if uri, ok := envMap["PGRST_DB_URI"]; !ok {
    t.Error("env var PGRST_DB_URI is missing")
} else if !strings.HasPrefix(uri, "postgresql://") {
    t.Errorf("PGRST_DB_URI should start with postgresql://, got %q", uri)
}

However, the current approach works since "postgresql://" != "postgresql://..." would fail the equality check. The test effectively validates the URI scheme is present.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/internal/orchestrator/swarm/service_spec_test.go` around lines 384 -
400, The PGRST_DB_URI equality check is weak; modify the test in
service_spec_test.go (within the envMap checks loop or right before it) to
special-case "PGRST_DB_URI": first assert the key exists in envMap, then assert
the value has the correct prefix using strings.HasPrefix(uri, "postgresql://")
(or a more complete URI format check) instead of strict equality; keep the
existing exact equality checks for the other keys ("PGRST_SERVER_PORT",
"PGHOST", etc.) and import "strings" if not already present.
server/internal/orchestrator/swarm/postgrest_preflight_resource.go (1)

47-53: Add database node dependency to ensure preflight validation runs after the Postgres instance is ready.

Dependencies() returns nil, but this resource accesses the database via database.GetPrimaryInstance(ctx, rc, r.NodeName) to validate schemas and roles. Other resources that access the database through a NodeName field declare this dependency (see pgbackrest_stanza.go and service_user_role.go). Add:

Suggested change
 func (r *PostgRESTPreflightResource) Dependencies() []resource.Identifier {
-	return nil
+	return []resource.Identifier{
+		database.NodeResourceIdentifier(r.NodeName),
+	}
 }

This aligns with the established pattern in the codebase and ensures the preflight check doesn't run before the Postgres instance is available.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/internal/orchestrator/swarm/postgrest_preflight_resource.go` around
lines 47 - 53, Dependencies() currently returns nil but
PostgRESTPreflightResource calls database.GetPrimaryInstance(ctx, rc,
r.NodeName), so change Dependencies() to declare a dependency on the database
node identified by r.NodeName using the same resource identifier pattern used by
pgbackrest_stanza and service_user_role (i.e., return a []resource.Identifier
containing the DB instance identifier for r.NodeName) so the preflight runs only
after the Postgres instance is ready.
server/internal/orchestrator/swarm/postgrest_config_test.go (1)

35-43: This test doesn't exercise the actual empty-config defaulting path.

TestGeneratePostgRESTConfig_Defaults pre-populates every field with the expected defaults, so it won't catch a regression where config: {} stops defaulting correctly. I'd either make this fixture empty or rename the test and cover default injection at the layer that owns it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/internal/orchestrator/swarm/postgrest_config_test.go` around lines 35
- 43, The test TestGeneratePostgRESTConfig_Defaults currently pre-populates
PostgRESTConfigParams.Config with the expected defaults
(database.PostgRESTServiceConfig fields), so it doesn't verify the defaulting
logic; update the test to pass an empty/nil Config (e.g.,
&PostgRESTConfigParams{Config: &database.PostgRESTServiceConfig{}} or nil) and
assert that GeneratePostgRESTConfig (or the function under test that applies
defaults) fills in DBSchemas, DBAnonRole, DBPool, MaxRows with the expected
default values, or alternatively rename the test to reflect it is a
"pre-populated values" case if you intend to keep it as-is.
server/internal/orchestrator/swarm/service_spec.go (1)

137-142: Fail fast when no Postgres hosts are available.

If DatabaseHosts is empty, buildPostgRESTEnvVars() emits PGHOST=/PGPORT= and the service only fails later inside the container. Since ServiceContainerSpec() already returns an error, it would be better to reject that state here and keep the failure actionable during provisioning.

💡 Proposed change
 	switch opts.ServiceSpec.ServiceType {
 	case "postgrest":
+		if len(opts.DatabaseHosts) == 0 {
+			return swarm.ServiceSpec{}, fmt.Errorf("postgrest service requires at least one database host")
+		}
 		user = fmt.Sprintf("%d", postgrestContainerUID)
 		command = []string{"postgrest"}
 		args = []string{"/app/data/postgrest.conf"}
 		env = buildPostgRESTEnvVars(opts)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/internal/orchestrator/swarm/service_spec.go` around lines 137 - 142,
When handling the "postgrest" branch in ServiceContainerSpec, fail fast if no
Postgres hosts are configured: before calling buildPostgRESTEnvVars(opts) check
opts.DatabaseHosts (or the field on opts that lists DB hosts) and return an
error indicating no Postgres hosts available; this prevents emitting empty
PGHOST/PGPORT env vars and surfaces the provisioning failure earlier. Reference
the "postgrest" case in ServiceContainerSpec, the opts parameter, and the
buildPostgRESTEnvVars function when adding the check and returning the error.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@server/internal/orchestrator/swarm/postgrest_config_test.go`:
- Around line 35-43: The test TestGeneratePostgRESTConfig_Defaults currently
pre-populates PostgRESTConfigParams.Config with the expected defaults
(database.PostgRESTServiceConfig fields), so it doesn't verify the defaulting
logic; update the test to pass an empty/nil Config (e.g.,
&PostgRESTConfigParams{Config: &database.PostgRESTServiceConfig{}} or nil) and
assert that GeneratePostgRESTConfig (or the function under test that applies
defaults) fills in DBSchemas, DBAnonRole, DBPool, MaxRows with the expected
default values, or alternatively rename the test to reflect it is a
"pre-populated values" case if you intend to keep it as-is.

In `@server/internal/orchestrator/swarm/postgrest_preflight_resource.go`:
- Around line 47-53: Dependencies() currently returns nil but
PostgRESTPreflightResource calls database.GetPrimaryInstance(ctx, rc,
r.NodeName), so change Dependencies() to declare a dependency on the database
node identified by r.NodeName using the same resource identifier pattern used by
pgbackrest_stanza and service_user_role (i.e., return a []resource.Identifier
containing the DB instance identifier for r.NodeName) so the preflight runs only
after the Postgres instance is ready.

In `@server/internal/orchestrator/swarm/service_spec_test.go`:
- Around line 384-400: The PGRST_DB_URI equality check is weak; modify the test
in service_spec_test.go (within the envMap checks loop or right before it) to
special-case "PGRST_DB_URI": first assert the key exists in envMap, then assert
the value has the correct prefix using strings.HasPrefix(uri, "postgresql://")
(or a more complete URI format check) instead of strict equality; keep the
existing exact equality checks for the other keys ("PGRST_SERVER_PORT",
"PGHOST", etc.) and import "strings" if not already present.

In `@server/internal/orchestrator/swarm/service_spec.go`:
- Around line 137-142: When handling the "postgrest" branch in
ServiceContainerSpec, fail fast if no Postgres hosts are configured: before
calling buildPostgRESTEnvVars(opts) check opts.DatabaseHosts (or the field on
opts that lists DB hosts) and return an error indicating no Postgres hosts
available; this prevents emitting empty PGHOST/PGPORT env vars and surfaces the
provisioning failure earlier. Reference the "postgrest" case in
ServiceContainerSpec, the opts parameter, and the buildPostgRESTEnvVars function
when adding the check and returning the error.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 337e97d9-e28a-4bc8-9d01-1a1daad7b821

📥 Commits

Reviewing files that changed from the base of the PR and between 99689f0 and 069f53b.

⛔ Files ignored due to path filters (7)
  • api/apiv1/gen/control_plane/service.go is excluded by !**/gen/**
  • api/apiv1/gen/http/control_plane/client/types.go is excluded by !**/gen/**
  • api/apiv1/gen/http/control_plane/server/types.go is excluded by !**/gen/**
  • api/apiv1/gen/http/openapi.json is excluded by !**/gen/**
  • api/apiv1/gen/http/openapi.yaml is excluded by !**/gen/**
  • api/apiv1/gen/http/openapi3.json is excluded by !**/gen/**
  • api/apiv1/gen/http/openapi3.yaml is excluded by !**/gen/**
📒 Files selected for processing (15)
  • api/apiv1/design/database.go
  • server/internal/api/apiv1/validate.go
  • server/internal/api/apiv1/validate_test.go
  • server/internal/orchestrator/swarm/postgrest_config.go
  • server/internal/orchestrator/swarm/postgrest_config_resource.go
  • server/internal/orchestrator/swarm/postgrest_config_test.go
  • server/internal/orchestrator/swarm/postgrest_preflight_resource.go
  • server/internal/orchestrator/swarm/resources.go
  • server/internal/orchestrator/swarm/service_images.go
  • server/internal/orchestrator/swarm/service_images_test.go
  • server/internal/orchestrator/swarm/service_instance_spec.go
  • server/internal/orchestrator/swarm/service_spec.go
  • server/internal/orchestrator/swarm/service_spec_test.go
  • server/internal/orchestrator/swarm/service_user_role.go
  • server/internal/orchestrator/swarm/service_user_role_test.go
✅ Files skipped from review due to trivial changes (3)
  • server/internal/orchestrator/swarm/resources.go
  • server/internal/api/apiv1/validate_test.go
  • server/internal/orchestrator/swarm/service_images_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/internal/api/apiv1/validate.go

Copy link
Copy Markdown
Contributor

@rshoemaker rshoemaker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're missing some diffs in other files.

This should allow "postgrest" as a service type:

// Only MCP service instance generation is currently implemented.
if spec.ServiceSpec.ServiceType != "mcp" {
return nil, fmt.Errorf("service type %q instance generation is not yet supported", spec.ServiceSpec.ServiceType)
}

These should set you new "ServiceType" field:

serviceUserRoleRO := &ServiceUserRole{
ServiceID: spec.ServiceSpec.ServiceID,
DatabaseID: spec.DatabaseID,
DatabaseName: spec.DatabaseName,
NodeName: spec.NodeName,
Mode: ServiceUserRoleRO,
}
serviceUserRoleRW := &ServiceUserRole{
ServiceID: spec.ServiceSpec.ServiceID,
DatabaseID: spec.DatabaseID,
DatabaseName: spec.DatabaseName,
NodeName: spec.NodeName,
Mode: ServiceUserRoleRW,
}

ServiceType: spec.ServiceSpec.ServiceType

I'm not sure about forcing DBAnon into the ServiceUserRole - it feels different enough for its own resource. Consider a PostgRESTAuthenticatorResource that depends on the generic ServiceUserRole and adds the NOINHERIT + grant on top. The generic ServiceUserRole would just create a normal LOGIN user for all service types, and PostgREST would alter it in its own resource. Keeps the shared path clean for RAG and whatever comes next.

Copy link
Copy Markdown
Contributor

@rshoemaker rshoemaker Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This func needs to be updated to do something like:

func (s *ServiceInstanceSpecResource) Dependencies() []resource.Identifier {
	deps := []resource.Identifier{
		NetworkResourceIdentifier(s.DatabaseNetworkID),
		ServiceUserRoleIdentifier(s.ServiceSpec.ServiceID, ServiceUserRoleRO),
		ServiceUserRoleIdentifier(s.ServiceSpec.ServiceID, ServiceUserRoleRW),
	}

	switch s.ServiceSpec.ServiceType {
	case "mcp":
		deps = append(deps, MCPConfigResourceIdentifier(s.ServiceInstanceID))
	case "postgrest":
		deps = append(deps, PostgRESTConfigResourceIdentifier(s.ServiceInstanceID))
	default:
		log.Warn().Str("service_type", s.ServiceSpec.ServiceType).Msg("unknown service type in dependencies")
	}

	return deps
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In next Pr for PLAT-504I will solve this issue.

if err := grants.Exec(ctx, conn); err != nil {
return fmt.Errorf("failed to grant service user permissions: %w", err)
}
} else {
Copy link
Copy Markdown
Contributor

@rshoemaker rshoemaker Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This "else" has to be an explicit check:

else if r.ServiceType == "mcp"

The "else" (default) case should throw an error for unknown service.

}

// GeneratePostgRESTConfig generates the postgrest.conf file content.
// Credentials are not written here; they are injected as libpq env vars at the container level.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this comment accurate? We don't want to use environment variables for credentials. They should be passed in via this configuration file.


// GeneratePostgRESTConfig generates the postgrest.conf file content.
// Credentials are not written here; they are injected as libpq env vars at the container level.
func GeneratePostgRESTConfig(params *PostgRESTConfigParams) ([]byte, error) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like it should be a method on database.PostgRESTServiceConfig rather than a standalone function in this package.

// See: https://github.com/PostgREST/postgrest/blob/main/Dockerfile (USER 1000)
const postgrestContainerUID = 1000

func buildPostgRESTEnvVars(opts *ServiceContainerSpecOptions) []string {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please change it so that we pass all of these connection parameters via the configuration file? Environment variables are visible to other users on the machine, whereas the configuration file is much more restricted.

@jason-lynch jason-lynch dismissed their stale review March 31, 2026 20:57

re-reviewing after merging stacked PRs

@moizpgedge moizpgedge changed the title eat: add PostgREST as a service type (PLAT-499, PLAT-500,PLAT-501,PLAT-502,PLAT-503) Feat: add PostgREST as a service type (PLAT-499, PLAT-500,PLAT-501,PLAT-502,PLAT-503) Apr 1, 2026
@codacy-production
Copy link
Copy Markdown

codacy-production bot commented Apr 1, 2026

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 89 complexity · 19 duplication

Metric Results
Complexity 89
Duplication 19

View in Codacy

TIP This summary will be updated as you push new changes. Give us feedback

@moizpgedge moizpgedge force-pushed the feat/PLAT-499/PLAT-500/PostgREST-prerequisites/image-wiring branch from ddbbc1c to 069f53b Compare April 1, 2026 05:31
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
server/internal/orchestrator/swarm/service_user_role.go (1)

171-188: ⚠️ Potential issue | 🔴 Critical

Finish the service-type split; the old MCP block is still live.

Line 188 should be an explicit else if r.ServiceType == "mcp", but the previous MCP block is still present below it. As written, Line 212 redeclares groupRole, Line 222 reuses := with no new variable in scope, and unknown service types still fall through the MCP path instead of failing fast.

🛠️ Suggested cleanup
-	} else {
+	} else if r.ServiceType == "mcp" {
 		var groupRole string
 		switch r.Mode {
 		case ServiceUserRoleRO:
 			groupRole = "pgedge_application_read_only"
 		case ServiceUserRoleRW:
 			groupRole = "pgedge_application"
 		default:
 			return fmt.Errorf("unknown service user role mode: %q", r.Mode)
 		}
 		statements, err := postgres.CreateUserRole(postgres.UserRoleOptions{
 			Name:       r.Username,
 			Password:   r.Password,
 			DBOwner:    false,
 			Attributes: []string{"LOGIN"},
 			Roles:      []string{groupRole},
 		})
 		if err != nil {
 			return fmt.Errorf("failed to generate create user role statements: %w", err)
 		}
 		if err := statements.Exec(ctx, conn); err != nil {
 			return fmt.Errorf("failed to create service user: %w", err)
 		}
+	} else {
+		return fmt.Errorf("unknown service type: %q", r.ServiceType)
 	}
-
-	// Determine group role based on mode
-	var groupRole string
-	switch r.Mode {
-	case ServiceUserRoleRO:
-		groupRole = "pgedge_application_read_only"
-	case ServiceUserRoleRW:
-		groupRole = "pgedge_application"
-	default:
-		return fmt.Errorf("unknown service user role mode: %q", r.Mode)
-	}
-
-	statements, err := postgres.CreateUserRole(postgres.UserRoleOptions{
-		Name:       r.Username,
-		Password:   r.Password,
-		Attributes: []string{"LOGIN"},
-		Roles:      []string{groupRole},
-	})
-	if err != nil {
-		return fmt.Errorf("failed to generate create user role statements: %w", err)
-	}
-
-	if err := statements.Exec(ctx, conn); err != nil {
-		return fmt.Errorf("failed to create service user: %w", err)
-	}
 
 	return nil

This should show the duplicated declarations still sitting inside createUserRole():

#!/bin/bash
sed -n '160,237p' server/internal/orchestrator/swarm/service_user_role.go
printf '\n---- duplicate declarations inside createUserRole() ----\n'
rg -n 'var groupRole string|statements, err :=' server/internal/orchestrator/swarm/service_user_role.go

Also applies to: 211-234

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/internal/orchestrator/swarm/service_user_role.go` around lines 171 -
188, In createUserRole(), stop treating the MCP branch as the default: change
the existing else into an explicit else if r.ServiceType == "mcp" and add a
final else that returns an error for unknown r.ServiceType; also remove the
duplicated declarations by reusing variables instead of redeclaring (e.g., don’t
redeclare groupRole or use := for statements/err when those names are already in
scope), and ensure the MCP block and the postgrest block each only declare their
own unique variables so there are no shadowing/redefinition issues.
🧹 Nitpick comments (1)
server/internal/api/apiv1/validate.go (1)

283-285: Consider updating the error message to reflect the new format.

The validation now accepts two-part versions (e.g., 14.5), but the error message still references only the three-part semver format. A clearer message would help users understand valid inputs.

💡 Suggested improvement
 	// Validate version (semver pattern or "latest")
 	if svc.Version != "latest" && !semverPattern.MatchString(svc.Version) {
-		err := errors.New("version must be in semver format (e.g., '1.0.0') or 'latest'")
+		err := errors.New("version must be in 'major.minor' or 'major.minor.patch' format (e.g., '14.5', '1.0.0') or 'latest'")
 		errs = append(errs, newValidationError(err, appendPath(path, "version")))
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/internal/api/apiv1/validate.go` around lines 283 - 285, The error
message for version validation is outdated: in validate.go the check using
svc.Version and semverPattern appends an error saying "version must be in semver
format (e.g., '1.0.0') or 'latest'"; update that message to reflect the accepted
two-part or three-part formats (e.g., "'14.5' or '1.0.0'") so users know both
are valid. Modify the string passed into errors.New where the validation failure
is created (the code that builds err and calls newValidationError with
appendPath(path, "version") and appends to errs) to include examples of both
two-segment and three-segment version formats and mention 'latest'.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/internal/orchestrator/swarm/service_user_role.go`:
- Around line 68-71: The Update() implementation for ServiceUserRole must handle
changes to the ServiceType and DBAnonRole fields instead of being a no-op: in
the ServiceUserRole.Update(...) method compare the incoming desired ServiceType
(service_type) and DBAnonRole (db_anon_role) against the current stored values,
and if they differ either (preferred) reconcile by invoking the same idempotent
provisioning logic used by ProvisionServices/ProvisionServiceInstance to update
grants/roles in the authenticator for the existing service user, or (if
reconciliation is not feasible) return a signal that forces recreation of the
service (i.e., mark the resource as requiring replacement and ensure the service
identifier changes when service_type or db_anon_role change). Ensure the chosen
path updates the identifier used for routing/authentication or triggers
teardown-and-recreate so PostgREST won't attempt SET ROLE to an ungranted role.
- Around line 249-252: The Delete() path drops the service role without revoking
the CONNECT privilege that role was granted in roleAttributesAndGrants() (which
grants CONNECT on r.DatabaseName to r.Username and grants anonRole to
r.Username), causing DROP ROLE to fail; before calling DROP ROLE IF EXISTS in
Delete(), execute a REVOKE CONNECT ON DATABASE <r.DatabaseName> FROM
<r.Username> using sanitizeIdentifier for both identifiers via conn.Exec to
remove the privilege, then proceed to DROP ROLE IF EXISTS <r.Username> (keep
using conn.Exec and handle errors as existing code does).

---

Duplicate comments:
In `@server/internal/orchestrator/swarm/service_user_role.go`:
- Around line 171-188: In createUserRole(), stop treating the MCP branch as the
default: change the existing else into an explicit else if r.ServiceType ==
"mcp" and add a final else that returns an error for unknown r.ServiceType; also
remove the duplicated declarations by reusing variables instead of redeclaring
(e.g., don’t redeclare groupRole or use := for statements/err when those names
are already in scope), and ensure the MCP block and the postgrest block each
only declare their own unique variables so there are no shadowing/redefinition
issues.

---

Nitpick comments:
In `@server/internal/api/apiv1/validate.go`:
- Around line 283-285: The error message for version validation is outdated: in
validate.go the check using svc.Version and semverPattern appends an error
saying "version must be in semver format (e.g., '1.0.0') or 'latest'"; update
that message to reflect the accepted two-part or three-part formats (e.g.,
"'14.5' or '1.0.0'") so users know both are valid. Modify the string passed into
errors.New where the validation failure is created (the code that builds err and
calls newValidationError with appendPath(path, "version") and appends to errs)
to include examples of both two-segment and three-segment version formats and
mention 'latest'.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0ca46920-8b97-48f4-b977-a9da1ef590ee

📥 Commits

Reviewing files that changed from the base of the PR and between 069f53b and ddbbc1c.

⛔ Files ignored due to path filters (4)
  • api/apiv1/gen/http/openapi.json is excluded by !**/gen/**
  • api/apiv1/gen/http/openapi.yaml is excluded by !**/gen/**
  • api/apiv1/gen/http/openapi3.json is excluded by !**/gen/**
  • api/apiv1/gen/http/openapi3.yaml is excluded by !**/gen/**
📒 Files selected for processing (6)
  • api/apiv1/design/database.go
  • server/internal/api/apiv1/validate.go
  • server/internal/api/apiv1/validate_test.go
  • server/internal/orchestrator/swarm/service_images.go
  • server/internal/orchestrator/swarm/service_images_test.go
  • server/internal/orchestrator/swarm/service_user_role.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • server/internal/orchestrator/swarm/service_images.go
  • server/internal/orchestrator/swarm/service_images_test.go

@moizpgedge moizpgedge force-pushed the feat/PLAT-499/PLAT-500/PostgREST-prerequisites/image-wiring branch from 4632965 to 4005f53 Compare April 1, 2026 06:21
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
server/internal/database/postgrest_service_config.go (2)

166-189: GenerateConf always returns nil error.

The method signature includes an error return but always returns nil. Consider either:

  1. Removing the error return if validation is handled upstream
  2. Adding validation (e.g., empty DatabaseHosts)

Given that callers like writeConfigFile (context snippet 1) already wrap the error, keeping the signature for future extensibility is reasonable, but documenting this would help.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/internal/database/postgrest_service_config.go` around lines 166 - 189,
GenerateConf currently always returns a nil error; add input validation and
return meaningful errors instead of always nil: inside
PostgRESTServiceConfig.GenerateConf (which calls buildPostgRESTDBURI and reads
fields like DBSchemas, DBAnonRole, DBPool, MaxRows) validate required fields on
the receiver and the PostgRESTConnParams (e.g., ensure conn.DatabaseHosts is not
empty, DBAnonRole and DBSchemas are non-empty, DBPool > 0, MaxRows >= 0) and
return errors describing the missing/invalid field(s) before writing to the
buffer; keep the existing signature so callers don’t change, and use the
buildPostgRESTDBURI, PostgRESTServiceConfig, and PostgRESTConnParams symbols to
locate where to add these checks.

193-212: Handle empty DatabaseHosts defensively.

If conn.DatabaseHosts is empty, buildPostgRESTDBURI will produce an invalid URI like postgresql://user:pass@/dbname. Consider returning an error or at least documenting the precondition.

🛡️ Proposed defensive check
 func buildPostgRESTDBURI(conn PostgRESTConnParams) string {
+	if len(conn.DatabaseHosts) == 0 {
+		// Caller must ensure at least one host; this is a programming error.
+		panic("buildPostgRESTDBURI: DatabaseHosts cannot be empty")
+	}
 	userInfo := url.UserPassword(conn.Username, conn.Password)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/internal/database/postgrest_service_config.go` around lines 193 - 212,
The function buildPostgRESTDBURI should defensively handle empty
conn.DatabaseHosts: change its signature to return (string, error), then add a
guard at the start that if len(conn.DatabaseHosts) == 0 returns "",
fmt.Errorf("PostgRESTConnParams.DatabaseHosts is empty") (or a similarly
descriptive error), otherwise proceed as before; update all callers of
buildPostgRESTDBURI to handle the returned error. This uses the
PostgRESTConnParams and buildPostgRESTDBURI symbols to locate where to add the
check and where to propagate the error.
server/internal/orchestrator/swarm/service_user_role.go (1)

266-273: Revoking all role memberships may be overly aggressive.

The loop revokes any role membership that doesn't match desiredAnon, but this could revoke legitimate memberships (e.g., if the user was intentionally granted additional roles outside of this system). Consider whether the revocation should be limited to only roles that were previously granted by this resource.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/internal/orchestrator/swarm/service_user_role.go` around lines 266 -
273, The current loop over currentRoles revokes any role not equal to
desiredAnon, which may remove legitimate external grants; change the logic in
the block that iterates currentRoles to only revoke roles that this resource
actually manages (e.g., compute toRevoke := intersection(currentRoles,
rolesManagedByResource) or filter by a managed-role naming convention/tag) and
then call conn.Exec for each role in toRevoke using the same
sanitizeIdentifier(r.Username) and sanitizeIdentifier(role) pattern; locate the
loop using identifiers currentRoles, desiredAnon, r.Username, conn.Exec and
sanitizeIdentifier and replace the unconditional revoke with the filtered revoke
set (or add a check that the role exists in a persisted "granted by this system"
list before revoking).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@server/internal/database/postgrest_service_config.go`:
- Around line 166-189: GenerateConf currently always returns a nil error; add
input validation and return meaningful errors instead of always nil: inside
PostgRESTServiceConfig.GenerateConf (which calls buildPostgRESTDBURI and reads
fields like DBSchemas, DBAnonRole, DBPool, MaxRows) validate required fields on
the receiver and the PostgRESTConnParams (e.g., ensure conn.DatabaseHosts is not
empty, DBAnonRole and DBSchemas are non-empty, DBPool > 0, MaxRows >= 0) and
return errors describing the missing/invalid field(s) before writing to the
buffer; keep the existing signature so callers don’t change, and use the
buildPostgRESTDBURI, PostgRESTServiceConfig, and PostgRESTConnParams symbols to
locate where to add these checks.
- Around line 193-212: The function buildPostgRESTDBURI should defensively
handle empty conn.DatabaseHosts: change its signature to return (string, error),
then add a guard at the start that if len(conn.DatabaseHosts) == 0 returns "",
fmt.Errorf("PostgRESTConnParams.DatabaseHosts is empty") (or a similarly
descriptive error), otherwise proceed as before; update all callers of
buildPostgRESTDBURI to handle the returned error. This uses the
PostgRESTConnParams and buildPostgRESTDBURI symbols to locate where to add the
check and where to propagate the error.

In `@server/internal/orchestrator/swarm/service_user_role.go`:
- Around line 266-273: The current loop over currentRoles revokes any role not
equal to desiredAnon, which may remove legitimate external grants; change the
logic in the block that iterates currentRoles to only revoke roles that this
resource actually manages (e.g., compute toRevoke := intersection(currentRoles,
rolesManagedByResource) or filter by a managed-role naming convention/tag) and
then call conn.Exec for each role in toRevoke using the same
sanitizeIdentifier(r.Username) and sanitizeIdentifier(role) pattern; locate the
loop using identifiers currentRoles, desiredAnon, r.Username, conn.Exec and
sanitizeIdentifier and replace the unconditional revoke with the filtered revoke
set (or add a check that the role exists in a persisted "granted by this system"
list before revoking).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5861b72c-4517-47d1-8ffc-58dd6b89cbef

📥 Commits

Reviewing files that changed from the base of the PR and between ddbbc1c and 4005f53.

⛔ Files ignored due to path filters (7)
  • api/apiv1/gen/control_plane/service.go is excluded by !**/gen/**
  • api/apiv1/gen/http/control_plane/client/types.go is excluded by !**/gen/**
  • api/apiv1/gen/http/control_plane/server/types.go is excluded by !**/gen/**
  • api/apiv1/gen/http/openapi.json is excluded by !**/gen/**
  • api/apiv1/gen/http/openapi.yaml is excluded by !**/gen/**
  • api/apiv1/gen/http/openapi3.json is excluded by !**/gen/**
  • api/apiv1/gen/http/openapi3.yaml is excluded by !**/gen/**
📒 Files selected for processing (15)
  • api/apiv1/design/database.go
  • server/internal/api/apiv1/validate.go
  • server/internal/api/apiv1/validate_test.go
  • server/internal/database/postgrest_service_config.go
  • server/internal/database/postgrest_service_config_test.go
  • server/internal/orchestrator/swarm/postgrest_config_resource.go
  • server/internal/orchestrator/swarm/postgrest_preflight_resource.go
  • server/internal/orchestrator/swarm/resources.go
  • server/internal/orchestrator/swarm/service_images.go
  • server/internal/orchestrator/swarm/service_images_test.go
  • server/internal/orchestrator/swarm/service_instance_spec.go
  • server/internal/orchestrator/swarm/service_spec.go
  • server/internal/orchestrator/swarm/service_spec_test.go
  • server/internal/orchestrator/swarm/service_user_role.go
  • server/internal/orchestrator/swarm/service_user_role_test.go
✅ Files skipped from review due to trivial changes (3)
  • server/internal/api/apiv1/validate.go
  • server/internal/api/apiv1/validate_test.go
  • server/internal/orchestrator/swarm/postgrest_config_resource.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • server/internal/orchestrator/swarm/resources.go
  • server/internal/orchestrator/swarm/service_instance_spec.go
  • api/apiv1/design/database.go
  • server/internal/orchestrator/swarm/postgrest_preflight_resource.go

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
server/internal/orchestrator/swarm/service_user_role.go (1)

172-210: ⚠️ Potential issue | 🔴 Critical

Do not silently treat unknown ServiceType as MCP.

server/internal/orchestrator/swarm/orchestrator.go, Lines 455-468 and 540-555 still construct ServiceUserRole without ServiceType or DBAnonRole. With ServiceType == "", Line 172 misses the PostgREST branch and this fallback provisions an MCP-style role instead, so PostgREST services never get NOINHERIT or the anon-role grant. Please make this an explicit mcp branch and return an error for unknown/empty types so the wiring bug fails fast.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/internal/orchestrator/swarm/service_user_role.go` around lines 172 -
210, The code currently treats any non-"postgrest" ServiceType as MCP
implicitly; change the logic in ServiceUserRole.Create (the block using
r.ServiceType and r.roleAttributesAndGrants) to use an explicit switch: case
"postgrest" keep current PostgREST flow, case "mcp" keep the group-role/Roles
path, and default return an error for unknown/empty ServiceType (include
r.ServiceType in the message). Also ensure callers that construct
ServiceUserRole (notably in orchestrator.go where instances are created without
ServiceType or DBAnonRole) are updated to populate ServiceType and DBAnonRole so
PostgREST users get NOINHERIT and anon-role grants instead of silently falling
into the MCP branch.
🧹 Nitpick comments (1)
server/internal/database/postgrest_service_config.go (1)

56-57: Extract the default anon role into a shared constant.

"pgedge_application_read_only" is defaulted here and again in server/internal/orchestrator/swarm/service_user_role.go, Lines 221-223 and 253-255. Pulling it into one shared constant will keep config parsing and role reconciliation aligned if the default ever changes.

♻️ Sketch
+const DefaultPostgRESTAnonRole = "pgedge_application_read_only"
+
 // db_anon_role — optional string, default "pgedge_application_read_only"
-	dbAnonRole := "pgedge_application_read_only"
+	dbAnonRole := DefaultPostgRESTAnonRole
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/internal/database/postgrest_service_config.go` around lines 56 - 57,
Extract the hard-coded string "pgedge_application_read_only" into a single
exported constant (e.g., DefaultDBAnonRole) in a shared package (such as
server/internal/database or server/internal/config) and replace the local
literal usage in postgrest_service_config.go (dbAnonRole) and in
server/internal/orchestrator/swarm/service_user_role.go (the defaults at the two
locations around lines 221–223 and 253–255) to reference that constant; ensure
the constant name is exported if files are in different packages and update any
comments to reference the constant instead of duplicating the literal.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/internal/orchestrator/swarm/service_user_role.go`:
- Around line 258-266: The revoke+regrant path must be executed inside a single
DB transaction to avoid transient loss of anon-role membership; update the
sequence in service_user_role.go so that the call to r.revokeStaleAnonRoles(ctx,
conn, desiredAnon) and the subsequent re-application via grants.Exec(ctx, conn)
(from roleAttributesAndGrants()) are performed within one transaction (begin,
run both operations, commit/rollback on error), ensuring atomicity and that
failures during grants.Exec roll back the revoke; apply the same transactional
change to the analogous block around lines 272-288.

---

Duplicate comments:
In `@server/internal/orchestrator/swarm/service_user_role.go`:
- Around line 172-210: The code currently treats any non-"postgrest" ServiceType
as MCP implicitly; change the logic in ServiceUserRole.Create (the block using
r.ServiceType and r.roleAttributesAndGrants) to use an explicit switch: case
"postgrest" keep current PostgREST flow, case "mcp" keep the group-role/Roles
path, and default return an error for unknown/empty ServiceType (include
r.ServiceType in the message). Also ensure callers that construct
ServiceUserRole (notably in orchestrator.go where instances are created without
ServiceType or DBAnonRole) are updated to populate ServiceType and DBAnonRole so
PostgREST users get NOINHERIT and anon-role grants instead of silently falling
into the MCP branch.

---

Nitpick comments:
In `@server/internal/database/postgrest_service_config.go`:
- Around line 56-57: Extract the hard-coded string
"pgedge_application_read_only" into a single exported constant (e.g.,
DefaultDBAnonRole) in a shared package (such as server/internal/database or
server/internal/config) and replace the local literal usage in
postgrest_service_config.go (dbAnonRole) and in
server/internal/orchestrator/swarm/service_user_role.go (the defaults at the two
locations around lines 221–223 and 253–255) to reference that constant; ensure
the constant name is exported if files are in different packages and update any
comments to reference the constant instead of duplicating the literal.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f6141142-b0c0-4dd8-9ba2-12fc0a137747

📥 Commits

Reviewing files that changed from the base of the PR and between 4005f53 and 7ff4d39.

📒 Files selected for processing (3)
  • server/internal/database/postgrest_service_config.go
  • server/internal/orchestrator/swarm/postgrest_preflight_resource.go
  • server/internal/orchestrator/swarm/service_user_role.go

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/internal/orchestrator/swarm/service_spec.go`:
- Around line 155-156: The switch that returns swarm.ServiceSpec currently
rejects any ServiceType other than "mcp" and "postgrest" (default returns error
using opts.ServiceSpec.ServiceType) but upstream validation still allows "rag",
causing successful validation but provisioning failure; fix by either (A) adding
a dedicated "rag" case in this switch that constructs and returns the correct
swarm.ServiceSpec for "rag" (follow the same pattern as the "mcp"/"postgrest"
cases and reference opts.ServiceSpec.ServiceType to branch), or (B) remove "rag"
from the accepted types in the upstream validator
(server/internal/api/apiv1/validate.go) so it never reaches this function—choose
one consistent approach and update the switch or the validator accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 163b1feb-a2fa-4a33-818a-2367471b422a

📥 Commits

Reviewing files that changed from the base of the PR and between 7ff4d39 and 1571b15.

📒 Files selected for processing (3)
  • server/internal/orchestrator/swarm/postgrest_config_resource.go
  • server/internal/orchestrator/swarm/service_spec.go
  • server/internal/orchestrator/swarm/service_user_role.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/internal/orchestrator/swarm/service_user_role.go

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
server/internal/orchestrator/swarm/postgrest_authenticator_resource.go (2)

196-215: Confirm that revoking all non-desired memberships is intentional.

revokeStaleAnonRoles revokes all role memberships that differ from desiredAnon, not just previously-managed anon roles. If this authenticator user ever receives additional grants from other sources (manual intervention, other resources), they will be silently revoked on the next Update cycle.

If this aggressive reconciliation is by design for PostgREST's security model (authenticator should only have the single anon role), consider adding a brief inline comment to clarify this intent for future maintainers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/internal/orchestrator/swarm/postgrest_authenticator_resource.go`
around lines 196 - 215, revokeStaleAnonRoles currently revokes every role on the
user that does not equal desiredAnon (in function revokeStaleAnonRoles), which
will remove grants applied by other actors; either narrow the revocation to only
roles you manage (e.g., filter currentRoles by a managed-role naming pattern or
by consulting a managed-role list/source before calling REVOKE) or, if the
aggressive behavior is intentional, add a concise inline comment above
revokeStaleAnonRoles stating that the authenticator must only ever have the
single anon role and that revoking all other roles is deliberate for PostgREST
security, referencing the function name revokeStaleAnonRoles to make intent
explicit for future maintainers.

70-75: Silent default fallback may mask deserialization issues.

When the resource is deserialized from etcd/JSON and DBAnonRole is missing, this helper silently falls back to the default. Combined with the context snippet showing RegisterResourceType uses standard JSON unmarshaling, a serialization mismatch could cause per-node authenticators to silently use a different anon role than intended.

This matches the default in server/internal/database/postgrest_service_config.go:56-67, so the fallback is correct. However, consider adding a debug/trace log when the fallback is triggered to aid troubleshooting configuration drift.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/internal/orchestrator/swarm/postgrest_authenticator_resource.go`
around lines 70 - 75, The helper desiredAnonRole should emit a debug/trace log
when it uses the hardcoded fallback so configuration drift is observable; modify
PostgRESTAuthenticatorResource.desiredAnonRole to check r.DBAnonRole == "" and,
before returning "pgedge_application_read_only", log a concise debug/trace
message including the resource identity and that DBAnonRole was missing, using
the resource's existing logger field (e.g., r.logger or equivalent); if the
resource lacks a logger, add a lightweight logger field and use it for this
single debug message.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/internal/orchestrator/swarm/orchestrator.go`:
- Around line 596-608: The code re-parses the PostgREST config and discards
errors in the per-node loop (ParsePostgRESTServiceConfig called with `_`), which
can yield an empty DBAnonRole and cause mismatched anon roles between primary
and per-node authenticators; instead hoist and reuse the successfully parsed
postgrestConfig from the earlier parse (the result used to set the primary
authenticator) so PostgRESTAuthenticatorResource initialization uses that same
postgrestConfig.DBAnonRole, and remove the second ParsePostgRESTServiceConfig
call (or check and propagate its error) so desiredAnonRole() can't silently fall
back to the default; update the code paths constructing
PostgRESTAuthenticatorResource and any uses of spec.ServiceSpec.Config to
reference the hoisted postgrestConfig.

In `@server/internal/orchestrator/swarm/postgrest_authenticator_resource.go`:
- Around line 99-101: The current fmt.Errorf call uses two %w verbs which
prevents unwrapping the inner error; change it to join the sentinel and the
original error (errors.Join(resource.ErrNotFound, err)) and wrap that single
joined error with one %w so both resource.ErrNotFound and the original err are
discoverable; i.e., replace the double-%w return with a single-wrapped
errors.Join result in the code around r.authenticatorUsername() where the error
is constructed.

---

Nitpick comments:
In `@server/internal/orchestrator/swarm/postgrest_authenticator_resource.go`:
- Around line 196-215: revokeStaleAnonRoles currently revokes every role on the
user that does not equal desiredAnon (in function revokeStaleAnonRoles), which
will remove grants applied by other actors; either narrow the revocation to only
roles you manage (e.g., filter currentRoles by a managed-role naming pattern or
by consulting a managed-role list/source before calling REVOKE) or, if the
aggressive behavior is intentional, add a concise inline comment above
revokeStaleAnonRoles stating that the authenticator must only ever have the
single anon role and that revoking all other roles is deliberate for PostgREST
security, referencing the function name revokeStaleAnonRoles to make intent
explicit for future maintainers.
- Around line 70-75: The helper desiredAnonRole should emit a debug/trace log
when it uses the hardcoded fallback so configuration drift is observable; modify
PostgRESTAuthenticatorResource.desiredAnonRole to check r.DBAnonRole == "" and,
before returning "pgedge_application_read_only", log a concise debug/trace
message including the resource identity and that DBAnonRole was missing, using
the resource's existing logger field (e.g., r.logger or equivalent); if the
resource lacks a logger, add a lightweight logger field and use it for this
single debug message.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 73e61511-5e90-4161-acce-a7b0fbb793b1

📥 Commits

Reviewing files that changed from the base of the PR and between 1571b15 and 2e7c161.

📒 Files selected for processing (7)
  • server/internal/orchestrator/swarm/orchestrator.go
  • server/internal/orchestrator/swarm/postgrest_authenticator_resource.go
  • server/internal/orchestrator/swarm/postgrest_config_resource.go
  • server/internal/orchestrator/swarm/resources.go
  • server/internal/orchestrator/swarm/service_instance_spec.go
  • server/internal/orchestrator/swarm/service_user_role.go
  • server/internal/orchestrator/swarm/service_user_role_test.go
✅ Files skipped from review due to trivial changes (1)
  • server/internal/orchestrator/swarm/service_user_role_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • server/internal/orchestrator/swarm/resources.go
  • server/internal/orchestrator/swarm/service_instance_spec.go
  • server/internal/orchestrator/swarm/service_user_role.go
  • server/internal/orchestrator/swarm/postgrest_config_resource.go


// revokeStaleAnonRoles removes any role memberships on username that differ
// from desiredAnon. Must be called within a transaction for atomicity.
func (r *PostgRESTAuthenticatorResource) revokeStaleAnonRoles(ctx context.Context, conn postgres.Executor, username, desiredAnon string) error {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude flagged a logic bug here:

revokeStaleAnonRoles also revokes the group role from ServiceUserRole                                                                                                                                                               
                                                                                                             
The query at line 200 finds ALL role memberships for the username. ServiceUserRole grants
pgedge_application (RW group role) to this user. Then revokeStaleAnonRoles will revoke any
membership that isn't desiredAnon. So on Update,  it will revoke pgedge_application because
it's not the anon role — breaking the base group role membership that ServiceUserRole set up.
                                                                                                                                                                                                                                         
This is a real bug. The revoke query needs to exclude the base group roles, something like:                                                                                                                                            
   
if current != desiredAnon && current != "pgedge_application" && current != "pgedge_application_read_only" {                                                                                                                            
                                                                                                             
Or better, track which roles this resource granted and only manage those.        

@moizpgedge moizpgedge requested a review from rshoemaker April 2, 2026 13:06
return resource.PrimaryExecutor(r.NodeName)
}

func (r *PostgRESTPreflightResource) Dependencies() []resource.Identifier {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jason-lynch do you think this resource should depend on r.NodeName? With nil here, it seems like this resource could be created before the database nodes are ready.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call-out! I think this should depend on database.PostgresDatabaseResourceIdentifier(r.NodeName, r.DatabaseName) because it needs to connect to that specific database.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@moizpgedge ☝️

Thanks Jason.

Copy link
Copy Markdown
Contributor

@rshoemaker rshoemaker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good - thanks Moiz!

I dropped a question for Jason about the preflight resource dependency, so keep an eye out for his response. You can change it in the next PR if he suggests a diff there.

Move credential handling out of container env vars into postgrest.conf
via db-uri. Environment variables are visible to all users on the host
via docker inspect and /proc; the config file is restricted to the
service user (mode 0600).

Move config file generation to PostgRESTServiceConfig.GenerateConf in
the database package, where it belongs alongside the type it serializes.
PostgRESTConnParams carries the runtime connection details (host, port,
credentials) separately from the user-supplied PostgRESTServiceConfig.

Fix merge conflict resolution in service_user_role.go: remove the
duplicate MCP code block that was left in and drop DBOwner: false to
align with the upstream change in main.

Implement Update() for PostgREST ServiceUserRole to reconcile DBAnonRole
changes at runtime. Queries pg_auth_members for stale role memberships,
revokes them, and re-applies the desired grants idempotently. Without
this, a DBAnonRole change would leave the authenticator role unable to
SET ROLE to the new anon role.

Add REVOKE CONNECT ON DATABASE before DROP ROLE in Delete() for PostgREST
service users. PostgreSQL refuses to drop a role that holds database
privileges, causing the DROP to fail silently. Revoking first ensures
clean deletion.
- Extract `checkSchemas` helper from `PostgRESTPreflightResource.validate`
  to bring cyclomatic complexity within the limit of 8.
- Extract `revokeStaleAnonRoles` helper from `reconcilePostgRESTGrants`
  for the same reason.
- Change `buildPostgRESTDBURI` to return `(string, error)` and guard
  against empty `DatabaseHosts`, preventing a silently malformed db-uri
  in `postgrest.conf`.
- Wrap anon-role revoke + regrant in a transaction in
  `reconcilePostgRESTGrants` so a failed regrant rolls back the revoke,
  preventing transient loss of anon-role membership.
- Change `revokeStaleAnonRoles` to accept `postgres.Executor` instead of
  `*pgx.Conn` so it can be called with a transaction.
- Extract shared health check timing into named constants to remove
  duplication between PostgREST and MCP container specs.
- Collapse identical `PostgRESTConfigResource.Update` into `Create`.
- Add `#nosec G201` suppressions for SQL injection false positives where
  `sanitizeIdentifier` already quotes all dynamic identifiers.
Introduce `PostgRESTAuthenticatorResource` — a dedicated resource that
handles all PostgREST-specific Postgres role configuration (NOINHERIT,
GRANT CONNECT, GRANT <anon_role>) on top of the generic `ServiceUserRole`.
This keeps `ServiceUserRole` as a clean shared path for all service types.

- `ServiceUserRole`: remove `ServiceType`/`DBAnonRole` fields and all
  PostgREST-specific logic (`roleAttributesAndGrants`,
  `reconcilePostgRESTGrants`, `revokeStaleAnonRoles`). `Update` is now a
  no-op; both removed fields are added to `DiffIgnore` for state migration.
- `PostgRESTAuthenticatorResource` (new): owns ALTER ROLE NOINHERIT,
  GRANT CONNECT, and GRANT <anon_role>. Update reconciles the anon role
  in a single transaction to prevent transient loss of membership.
- `PostgRESTConfigResource`: add `ServiceUserRoleRW` dependency so
  `populateCredentials` can read the RW role from state at Create time;
  add `/username` and `/password` toDiffIgnore`.
- `orchestrator.go`: replace the `!= "mcp"` guard with a switch that
  generates the correct resource chain for both `"mcp"` and `"postgrest"`.
  PostgREST chain: preflight → authenticator → dataDir → configResource.
- `service_instance_spec.go`: `Dependencies()` is now service-type-aware
  (PostgREST → `PostgRESTConfigResource`, MCP → `MCPConfigResource`);
  `populateCredentials` sets role label to `postgrest_authenticator` for
  PostgREST and `pgedge_application_read_only` for MCP; unknown types log
  a warning in `Dependencies()`.
- `resources.go`: register `PostgRESTAuthenticatorResource`.

PLAT-499, PLAT-500, PLAT-501, PLAT-502, PLAT-503
`revokeStaleAnonRoles` queried all role memberships for the authenticator
user, which included `pgedge_application` and
`pgedge_application_read_only` granted by `ServiceUserRole`. On Update,
any role not matching the desired anon role was revoked — silently
breaking the base group role membership.

Fix by excluding the two known base group roles in the SQL query so only
anon role candidates (user-supplied custom roles) are considered for
revocation.
- Hoist parsed PostgREST config outside the service type switch so the
  per-node authenticator loop reuses the already-validated result instead
  of re-parsing and silently discarding errors, which could cause
  per-node authenticators to use a different anon role than the primary.
- Fix double `%w` in `PostgRESTAuthenticatorResource.Refresh` — replace
  with `errors.Join` so both `resource.ErrNotFound` and the original
  error are independently unwrappable via `errors.Is`/`errors.As`.
@moizpgedge moizpgedge force-pushed the feat/PLAT-499/PLAT-500/PostgREST-prerequisites/image-wiring branch from bd06bab to 2932f64 Compare April 3, 2026 13:47
@moizpgedge moizpgedge merged commit 63c54b8 into main Apr 3, 2026
3 checks passed
@jason-lynch jason-lynch deleted the feat/PLAT-499/PLAT-500/PostgREST-prerequisites/image-wiring branch April 3, 2026 20:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants