Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ jobs:
- uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130

- name: Run unit tests
run: go test ./... -skip '^(TestCreateToRunDocker|TestDirtyMigrationFailsClosed|TestLifecycleContractDocker|TestMigrationLockBudgetExpiresWhilePeerHoldsTheLock|TestMigrationStatementBudgetExpiresAndPreservesTheLedger)$' -timeout 20m
run: go test ./... -skip '^(TestCreateToRunDocker|TestDirtyMigrationFailsClosed|TestLifecycleContractDocker|TestLocalServerLifecycleDocker|TestMigrationLockBudgetExpiresWhilePeerHoldsTheLock|TestMigrationStatementBudgetExpiresAndPreservesTheLedger)$' -timeout 20m

- name: Build runtime image
run: docker build --build-arg SOURCE_DATE_EPOCH=0 --tag service-postgres:test .
Expand All @@ -90,6 +90,11 @@ jobs:
SERVICE_POSTGRES_TEST_IMAGE: service-postgres:test
run: go test . -run '^(TestMigrationLockBudgetExpiresWhilePeerHoldsTheLock|TestMigrationStatementBudgetExpiresAndPreservesTheLedger)$' -timeout 20m

- name: Shared local server regression
env:
SERVICE_POSTGRES_TEST_IMAGE: service-postgres:test
run: go test . -race -run '^TestLocalServerLifecycleDocker$' -count=1 -timeout 10m

- name: Smoke test runtime image
shell: bash
run: |
Expand Down
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,18 +72,18 @@ Derived from `.github/workflows/ci.yml` and the reusable
go build ./...
go vet ./...
go mod tidy -diff # CI fails on any diff
go test ./... -skip '^(TestCreateToRunDocker|TestDirtyMigrationFailsClosed|TestLifecycleContractDocker|TestMigrationLockBudgetExpiresWhilePeerHoldsTheLock|TestMigrationStatementBudgetExpiresAndPreservesTheLedger)$' -timeout 20m
go test ./... -skip '^(TestCreateToRunDocker|TestDirtyMigrationFailsClosed|TestLifecycleContractDocker|TestLocalServerLifecycleDocker|TestMigrationLockBudgetExpiresWhilePeerHoldsTheLock|TestMigrationStatementBudgetExpiresAndPreservesTheLedger)$' -timeout 20m
```

That skip list is CI's unit step, and it is the inventory of what needs a
container. Everything else is template rendering, lock parsing, workflow
assertions and unit tests. The three migration regressions run after it,
assertions and unit tests. The shared-server and three migration regressions run after it,
against a locally built image:

```bash
docker build --build-arg SOURCE_DATE_EPOCH=0 --tag service-postgres:test .
SERVICE_POSTGRES_TEST_IMAGE=service-postgres:test go test . \
-run '^(TestDirtyMigrationFailsClosed|TestMigrationLockBudgetExpiresWhilePeerHoldsTheLock|TestMigrationStatementBudgetExpiresAndPreservesTheLedger)$' -timeout 20m
-run '^(TestLocalServerLifecycleDocker|TestDirtyMigrationFailsClosed|TestMigrationLockBudgetExpiresWhilePeerHoldsTheLock|TestMigrationStatementBudgetExpiresAndPreservesTheLedger)$' -timeout 20m
```

`TestLifecycleContractDocker` runs later still, against the *published* image.
Expand Down
127 changes: 127 additions & 0 deletions docs/local-shared-server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Share a local Postgres server

A local composition can run one Postgres server with a separate logical database
for each database service. This is an explicit dependency, using Codefly's existing
network/configuration handoff and lifecycle ordering. It works with an owned Nix
or Docker server. The default remains one owned server per service.

## Compose it

On the service that owns the server, set:

```yaml
spec:
database-name: local_control
shared-local-server: true
```

On each database service using that server, declare a normal managed service
dependency (not `kind: external`) and set `local-server` to its module/service:

```yaml
service-dependencies:
- name: postgres
module: storage
spec:
database-name: accounts
local-server: storage/postgres
```

These are excerpts of services scaffolded with `codefly add service --agent=postgres`.
Use the containing module's actual dependency identity. The server and borrowers
must use an agent containing this feature. A missing opt-in, dependency mapping or
exported configuration fails before provisioning; there is no default address.

Give each database service a **distinct `POSTGRES_USER` migration-owner name**
through its Codefly configuration. Keep its own generated owner/reader/writer
passwords. Names must fit PostgreSQL's 63-byte identifier limit. Application
principals continue to be derived from the database name, and declared delegated
roles must be unique across databases. A collision is refused before changing
another database's credentials. This matters because PostgreSQL roles are global
to a server, even though tables and migrations belong to individual databases.

Run the consuming services together with `codefly run service`; their shared
server dependency starts once. Endpoints and credentials come from the dependency's
accepted Init response. Each logical service republishes its own endpoint identity
and reader/writer configuration at that server's addresses, so application clients
continue using their existing service dependencies. No application DSN changes.
Independent naming scopes resolve separate owning servers and retained state.
There is no process-global or machine-global pool.

The server owner exports the secret `postgres.local-server-connection` only after
`shared-local-server: true` is selected. A borrower does not export it: sharing
chains are refused. This is a local administrative capability, available to the
database runtime, not a credential applications should consume. As with existing
local owned clusters, migration owners are trusted superusers. Reader/writer
logins are not; CONNECT is explicitly granted per database and revoked from PUBLIC
on both the sharing server's database and each borrowed database. Do not use an
owner connection in an application. This feature is not an isolation boundary
between mutually untrusted migration authors.

The Postgres major must match the agent's managed runtime image. Borrowers cannot
set `docker-image`, `keep-running`, or external-identity authentication: those are
server-owner decisions. Required extensions still go through normal schema
preparation and fail if unavailable. Hosted deployment still uses the existing
`external-instances` binding; these local settings do not replace that binding or
change deployed manifests.

## Lifecycle and recovery

The existing Codefly graph owns server ordering and supervision. A logical service
runs its own migrations and runtime-grant reconciliation before reporting ready.
Concurrent initializations serialize cluster-wide role allocation through an
advisory lock in the server owner's database. Durable role reservations in
`codefly_local_server.roles` survive interrupted/dirty bootstrap and prevent
another database from taking the same role name. Do not delete this schema to
resolve a collision: choose distinct declared identities instead.

Stopping or destroying a borrower stops its watchers and releases its local
lifecycle state; it never stops the dependency's server or drops data. The owning
service stops the physical server when its composition stops. Restarting uses the
same retained server directory and per-database migration ledgers. Directly
stopping the server owner while applications still use it stops their shared
server, just as stopping any other required dependency does.

## Move existing local data deliberately

Changing `local-server` does **not** import an existing service-owned cluster. It
would create an empty logical database on the shared server. Keep the old cluster
and backup until the composed application has been verified against the restored
state; none of this agent's Stop/Destroy operations delete data.

1. Quiesce application writes and make a consistent `pg_dump` custom-format
backup of each source database, using Codefly-resolved owner connections.
Record row counts, migration versions and representative reference identities.
2. Prove each backup restores into an isolated test composition. Resolve its
connections through Codefly as well. Bootstrap the target service with its
distinct owner and normal migrations, then restore the application's data
using a reviewed restore list. Do not restore global roles from the source
server, replace the target migration ledger blindly, or use `--clean` on an
existing target. A schema/version mismatch must be resolved before cutover.
3. Repeat the restore into the intended fresh shared composition while writes
remain stopped. Verify counts, relationships, application queries, permissions
and migration ledgers; retain the original cluster for rollback.
4. Switch the application composition only after verification. Rollback means
stopping writes and returning to the original dependency configuration; writes
accepted after cutover require reconciliation before such a rollback.

The PR's lifecycle test proves persistence on a new shared composition. It does
not claim to have migrated any user's existing database or validated an arbitrary
application backup.

## Qualification

`go test . -run '^TestLocalServer' -count=1` includes actual Nix/Docker Postgres startup,
two simultaneous independently migrated database services, exported mappings,
reader access to own data, denied cross-database connections, role collision
refusal, borrower Stop/Destroy, and data surviving a whole-server restart.
Unavailable local runtimes are explicit skips. CI runs the Docker lifecycle
with a required locally built image; a missing daemon then fails. The pure unit
cases cover missing/ambiguous dependency handoff, mismatched addresses, missing
opt-in and incompatible settings. Run with
`-race` for concurrent-bootstrap qualification.

A standalone `codefly endpoint` invocation that cannot attach to the running flow
may fall back to the borrower's proposed port. For shared databases, consume the
accepted mappings/runtime configurations from the live composition; a deterministic
pre-run endpoint is not evidence of the actual shared listener.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/lib/pq v1.12.3
github.com/stretchr/testify v1.12.1
google.golang.org/grpc v1.84.0
google.golang.org/protobuf v1.36.12
gopkg.in/yaml.v3 v3.0.1
)

Expand Down Expand Up @@ -120,7 +121,6 @@ require (
golang.org/x/text v0.42.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc // indirect
google.golang.org/protobuf v1.36.12 // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad // indirect
sigs.k8s.io/kustomize/api v0.21.1 // indirect
Expand Down
Loading
Loading