Skip to content

feat: add PostgREST container spec, config file, and service user role#303

Merged
moizpgedge merged 5 commits intofeat/PLAT-499/PLAT-500/PostgREST-prerequisites/image-wiringfrom
feat/PLAT-501/PLAT-502/PLAT-503/postgrest-container-spec
Mar 30, 2026
Merged

feat: add PostgREST container spec, config file, and service user role#303
moizpgedge merged 5 commits intofeat/PLAT-499/PLAT-500/PostgREST-prerequisites/image-wiringfrom
feat/PLAT-501/PLAT-502/PLAT-503/postgrest-container-spec

Conversation

@moizpgedge
Copy link
Copy Markdown
Contributor

@moizpgedge moizpgedge commented Mar 19, 2026

  • Add postgrest_config.go: generates postgrest.conf with db-schemas, db-anon-role,
    db-pool, db-max-rows, and optional JWT fields; credentials are excluded from the file
    and injected as libpq env vars at the container level; nil-guards on params/Config
  • Add postgrest_config_resource.go: manages postgrest.conf lifecycle on the host
    filesystem; depends on DirResource; delete is a no-op (parent dir handles cleanup)
  • Update service_spec.go: PostgREST container uses postgrest /app/data/postgrest.conf
    as command, postgrest --ready health check (no curl in static binary image),
    read-only bind mount, and PGHOST/PGPORT/PGDATABASE/PGUSER/PGPASSWORD env vars;
    PGHOST and PGPORT are built as comma-separated lists from the DatabaseHosts slice
    to support multi-host libpq connections; PGTARGETSESSIONATTRS is set when present
  • Update service_instance_spec.go: pass DatabaseHosts and TargetSessionAttrs
    through to ServiceContainerSpecOptions
  • Update service_user_role.go: add ServiceType and DBAnonRole fields; PostgREST
    role is created with LOGIN NOINHERIT + GRANT <anon_role> to enable the
    SET ROLE mechanism; bump ResourceVersion to 4
  • Register PostgRESTConfigResource in resources.go

Summary

Implements the PostgREST container runtime layer: config file generation, host filesystem resource management, container spec (command, health check, mounts, env vars), and service user role SQL with NOINHERIT semantics.

Changes

  • Add postgrest_config.go: generates postgrest.conf with db-schemas, db-anon-role, db-pool, db-max-rows, and optional JWT fields; credentials excluded from the file and injected as libpq env vars; nil-guards on params/Config
  • Add postgrest_config_resource.go: manages postgrest.conf lifecycle on the host filesystem; depends on DirResource; chowns config to UID 1000 (PostgREST container user); delete is a no-op (parent dir handles cleanup)
  • Update service_spec.go: PostgREST container uses postgrest /app/data/postgrest.conf as command, postgrest --ready health check (no curl in static binary image), read-only bind mount, PGHOST/PGPORT/PGDATABASE/PGUSER/PGPASSWORD env vars, PGRST_SERVER_HOST=0.0.0.0 (required for --ready flag), explicit container UID 1000; replace default: with explicit case "mcp": — unknown service types now return an error
  • Update service_user_role.go: add ServiceType and DBAnonRole fields; roleAttributesAndGrants() is PostgREST-specific (LOGIN NOINHERIT + GRANT anon_role); MCP uses the group-role path in createUserRole() directly — dead MCP path and associated tests removed; bump ResourceVersion to 4
  • Register PostgRESTConfigResource in resources.go

Testing

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": {}
    }]
  }
}'

Retish command:

restish control-plane-local-1 update-database storefront \
  --body '{
    "spec": {
      "database_name": "storefront",
      "database_users": [{"username": "admin", "password": "password123", "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": {
        }
      }]
    }
  }'



Curl command:

curl -s -X PUT http://localhost:3000/v1/databases/storefront \
  -H "Content-Type: application/json" \
  -d '{
    "spec": {
      "database_name": "storefront",
      "database_users": [{"username": "admin", "password": "password123", "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

Base branch is feat/PLAT-499/PLAT-500/PostgREST-prerequisites/image-wiring (not main)
End-to-end provisioning is complete — orchestrator wiring is in PLAT-504 (separate PR on top of this one)
MCP path is unchanged; roleAttributesAndGrants() is now PostgREST-only — MCP uses built-in group roles (pgedge_application_read_only / pgedge_application) via Roles field in CreateUserRole, not this function
Copilot review items addressed: nil guards in GeneratePostgRESTConfig, panic on unexpected statement type in statementsSQL, container UID and chown for postgrest.conf

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 19, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1dbb2863-0475-44dc-be68-dda1119d8bbc

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.23% 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 accurately and specifically describes the main changes: adding PostgREST container spec, config file generation, and service user role functionality.
Description check ✅ Passed The pull request description follows the template structure with all major sections completed: Summary, Changes (detailed bulleted list), Testing (with specific commands), Checklist (marked appropriately), and comprehensive Notes for Reviewers.

✏️ 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-501/PLAT-502/PLAT-503/postgrest-container-spec

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

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

Adds the initial Swarm “runtime layer” for running PostgREST alongside existing services by introducing PostgREST config generation + host-file lifecycle management, extending the service container spec builder, and expanding service-user role SQL to support PostgREST’s SET ROLE flow.

Changes:

  • Add PostgREST config generation (postgrest.conf) plus a host filesystem resource to manage it.
  • Extend Swarm ServiceContainerSpec to support a new postgrest service type (command/args, healthcheck, env vars, RO mount).
  • Extend ServiceUserRole with ServiceType and DBAnonRole to generate PostgREST-appropriate role attributes/grants (and add unit tests).

Reviewed changes

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

Show a summary per file
File Description
server/internal/orchestrator/swarm/postgrest_config.go Generates postgrest.conf content from typed PostgREST service config.
server/internal/orchestrator/swarm/postgrest_config_resource.go Manages postgrest.conf lifecycle on the host filesystem.
server/internal/orchestrator/swarm/postgrest_config_test.go Unit coverage for config generation and absence/presence of JWT fields.
server/internal/orchestrator/swarm/service_spec.go Adds postgrest container spec branch (env, mounts, healthcheck, command).
server/internal/orchestrator/swarm/service_spec_test.go Adds PostgREST container spec unit tests.
server/internal/orchestrator/swarm/service_user_role.go Adds service-type-driven role/grant generation, bumps resource version.
server/internal/orchestrator/swarm/service_user_role_test.go Adds unit tests for PostgREST + MCP role/grant behavior.
server/internal/orchestrator/swarm/resources.go Registers PostgRESTConfigResource type.

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

default: // "mcp" and future service types
// MCP role: direct grants rather than role membership to avoid exposing
// replication internals via pgedge_application_read_only.
// https://github.com/pgEdge/pgedge-postgres-mcp/blob/main/docs/guide/security_mgmt.go
Comment on lines +13 to +19
func statementsSQL(stmts postgres.Statements) []string {
out := make([]string, 0, len(stmts))
for _, s := range stmts {
if stmt, ok := s.(postgres.Statement); ok {
out = append(out, stmt.SQL)
}
}
r := &ServiceUserRole{
ServiceType: "mcp",
DatabaseName: "mydb",
Username: `"svc_mcp"`,
Comment on lines +17 to +25
func GeneratePostgRESTConfig(params *PostgRESTConfigParams) ([]byte, error) {
cfg := params.Config

var buf bytes.Buffer

fmt.Fprintf(&buf, "db-schemas = %q\n", cfg.DBSchemas)
fmt.Fprintf(&buf, "db-anon-role = %q\n", cfg.DBAnonRole)
fmt.Fprintf(&buf, "db-pool = %d\n", cfg.DBPool)
fmt.Fprintf(&buf, "db-max-rows = %d\n", cfg.MaxRows)
Comment on lines +123 to +126
configPath := filepath.Join(dirPath, "postgrest.conf")
if err := afero.WriteFile(fs, configPath, content, 0o600); err != nil {
return fmt.Errorf("failed to write %s: %w", configPath, err)
}
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.

@jason-lynch
Can you guide for this issue?

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 is an accurate comment. What it's telling you is that in order for the postgrest container to read this file, it needs to be owned by the user that postgrest runs as. It also gives you a suggestion to explicitly set the UID for the container, which is a good suggestion.

If you look at the official postgrest Dockerfile: https://github.com/PostgREST/postgrest/blob/main/Dockerfile

It says:

USER 1000

Which means that postgrest runs with UID = 1000 by default. What I would do in this case is to do the chown, like Copilot suggests, and I would do it with UID and GID = 1000. You can find a few examples of that operation by searching the code for fs.Chown, such as this one in the DirResource that copilot mentions:

if err := fs.Chown(d.FullPath, d.OwnerUID, d.OwnerGID); err != nil {
return fmt.Errorf("failed to change ownership for directory %q: %w", d.FullPath, err)

I'd also take its other suggestion, and explicitly set the container UID to 1000. Since that's also the default, it doesn't have any effect now, but it protects us in case that container image changes its UID.

@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
- Add PostgRESTConfigResource: writes postgrest.conf to the service data
  directory from a Go template; re-runs on every apply via ErrNotFound
- Add PostgRESTConfigResource registration in RegisterResourceTypes
- Extend ServiceUserRole with ServiceType and DBAnonRole fields;
  add roleAttributesAndGrants() — PostgREST gets LOGIN+NOINHERIT and
  GRANT <anon_role>, MCP/default uses group role membership
- Extend ServiceContainerSpecOptions with DatabaseHosts and
  TargetSessionAttrs; build PGHOST/PGPORT from the hosts slice in
  buildPostgRESTEnvVars; pass both fields from ServiceInstanceSpecResource
- Add postgrest case to ServiceContainerSpec: postgrest command,
  read-only config mount, and postgrest --ready health check
- Add tests for postgrest container spec, config generation, and
  service user role attributes/grants

PLAT-501, PLAT-502, PLAT-503
@moizpgedge moizpgedge force-pushed the feat/PLAT-501/PLAT-502/PLAT-503/postgrest-container-spec branch from 99689f0 to 4d38da0 Compare March 24, 2026 14:12
- Fix dead link in MCP grants comment: .go → .md
- Panic on unexpected statement type in statementsSQL test helper
  instead of silently dropping statements
- Use unquoted username in TestRoleAttributesAndGrants_MCP to better
  reflect production input (quoting is the caller's responsibility)
- Guard against nil params/Config in GeneratePostgRESTConfig

PLAT-501, PLAT-502, PLAT-503
// replication internals via pgedge_application_read_only.
// https://github.com/pgEdge/pgedge-postgres-mcp/blob/main/docs/guide/security_mgmt.md
attributes := []string{"LOGIN"}
grants := postgres.Statements{
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.

I think we've removed these in the latest version of this file. Is that right, @rshoemaker?

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.

Correct, we use the built-in RO/RW roles instead:

// 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)
}

Copy link
Copy Markdown
Contributor

@rshoemaker rshoemaker Mar 26, 2026

Choose a reason for hiding this comment

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

@moizpgedge After looking at this more carefully, I can see that the RO/RW roles are still being used correctly in createUserRole(). The "default:" block here is dead code (I think). It should probably be removed and this func should return an error for non-PostgREST cases. Or refactor the code.

mounts = []mount.Mount{
docker.BuildMount(opts.DataPath, "/app/data", true),
}
default: // "mcp"
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.

Same here - MCP specific code shouldn't be in a "default" branch, it should have an explicit branch check for "mcp".

- Remove dead MCP path from roleAttributesAndGrants; function is now
  PostgREST-only since MCP uses the group-role path in createUserRole
- Remove associated MCP tests (TestRoleAttributesAndGrants_MCP,
  TestRoleAttributesAndGrants_EmptyServiceTypeIsMCP)
- Replace default branch with explicit case "mcp" in ServiceContainerSpec
  switch; unknown service types now return an error

PLAT-501, PLAT-502, PLAT-503
- Add postgrestContainerUID (1000) from official PostgREST Dockerfile
  and set explicit container User in the PostgREST service spec
- Chown postgrest.conf to UID 1000 so the non-root container user
  can read it via the bind mount
- Add PGRST_SERVER_HOST=0.0.0.0 to env vars; without this PostgREST
  defaults to server-host="!4" which causes postgrest --ready to fail
  with "cannot be used when server-host is configured as !4"

PLAT-501, PLAT-502, PLAT-503
@moizpgedge moizpgedge merged commit 1de877b into feat/PLAT-499/PLAT-500/PostgREST-prerequisites/image-wiring Mar 30, 2026
3 checks passed
@jason-lynch jason-lynch deleted the feat/PLAT-501/PLAT-502/PLAT-503/postgrest-container-spec 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