Deployment
Bundle composition
A policy bundle is one root file plus multiple scoped policy fragments that pgroles composes into a single effective PolicyManifest, with ownership boundaries validated up front. Use bundles when different teams or environments own different parts of the same database policy and you want composition-time conflict detection rather than late-binding merges.
When to use which workflow
pgroles offers three workflows for managing a PostgresPolicy. Pick one per database and stick with it — mixing them is supported but creates ambiguity about who owns what.
| Workflow | Best for | Composition validated at | Operator-compatible |
|---|---|---|---|
| Single manifest | Small/medium setups, one owner per database | N/A (no composition) | Yes — write the manifest under spec: directly |
| CLI bundle, direct apply | Multi-team DBs reconciled via the CLI (e.g. local compose, ad-hoc admin) | pgroles diff / apply time | No — the CLI talks directly to the database |
| CLI bundle, rendered for operator | Multi-team or multi-environment DBs reconciled by the operator | CI time, via pgroles render-bundle --check | Yes — render in CI, commit the flat manifest, wrap under PostgresPolicy.spec |
The CLI validates bundle composition before it touches the database. The operator accepts only a flat manifest in PostgresPolicy.spec, so render-bundle moves composition validation into CI and lets the operator reconcile an already-composed manifest. That keeps the operator's existing one-policy-per-database guarantee intact while still giving fragment authors independent files to own.
Why pre-render for the operator?
The PostgresPolicy CRD inlines the manifest fields under spec — roles, grants, profiles, and so on — alongside connection. The operator does not load policy fragments at reconcile time. To benefit from bundle composition under the operator, you compose ahead of time and commit the result:
bundle.yaml ─┐
platform.yaml ─┼─► pgroles render-bundle ─► pgroles.yaml ─► PostgresPolicy.spec
app.yaml ─┘
This keeps three properties:
- Fragment authors own their own files. App teams edit
app.yaml, the platform team editsplatform.yaml. Neither needs write access to the other's file. - Conflicts are caught in CI.
pgroles render-bundle --check pgroles.yamlfails the build if the bundle composes to something different from the committed flat manifest, or if any fragment claims something outside its declared scope. - The operator stays simple. Per-database serialization, advisory locking, and conflict detection between
PostgresPolicyresources all keep working as documented in the operator guide.
A minimal end-to-end example
1. Author the bundle and fragments
# bundle.yaml
shared:
default_owner: app_owner
profiles:
editor:
grants:
- privileges: [USAGE]
object: { type: schema }
- privileges: [SELECT, INSERT, UPDATE, DELETE]
object: { type: table, name: "*" }
sources:
- file: platform.yaml
- file: app.yaml
# platform.yaml
policy:
name: platform
scope:
roles: [app_owner]
schemas:
- name: inventory
facets: [owner]
roles:
- name: app_owner
schemas:
- name: inventory
owner: app_owner
# app.yaml
policy:
name: app
scope:
schemas:
- name: inventory
facets: [bindings]
schemas:
- name: inventory
profiles: [editor]
2. Render the composed manifest
pgroles render-bundle --bundle bundle.yaml --output pgroles.yaml
The rendered file is byte-deterministic across machines: the header records only the bundle's basename (never a pwd-relative path) and the body strips serde-emitted defaults so it doesn't churn under unrelated upgrades. See the render-bundle CLI reference for details on what is and isn't stripped.
3. Wrap the rendered manifest into a PostgresPolicy
The rendered file is a PolicyManifest. To deploy it under the operator, embed its contents under spec: alongside connection::
# postgres-policy.yaml
apiVersion: pgroles.io/v1alpha1
kind: PostgresPolicy
metadata:
name: app
spec:
connection:
url:
secretName: app-db
secretKey: url
mode: apply
# ... paste the rendered manifest body here:
default_owner: app_owner
profiles:
editor:
# ...
schemas:
- name: inventory
# ...
roles:
- name: app_owner
# ...
Common ways to automate the wrap step:
- Helm / cdk8s / Jsonnet: template the rendered manifest into the resource as part of chart rendering.
yqor a small script: merge the rendered manifest fields underspecafter setting the operator-only fields such asconnectionandmode.- Kustomize: generate the wrapped resource in CI before Kustomize runs, then let Kustomize handle environment overlays for metadata and connection references.
The rendered file is the source of truth for policy content; the wrapping step is about deployment metadata (connection, mode, schedule) and is orthogonal to composition correctness.
4. Gate drift in CI
Commit bundle.yaml, the fragments, and the rendered pgroles.yaml together. Add a CI step that fails the build if the rendered file is stale:
# .github/workflows/policy.yml
jobs:
render-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Verify rendered manifest is up-to-date
run: |
docker run --rm -v "${{ github.workspace }}:/work" \
ghcr.io/hardbyte/pgroles:latest \
render-bundle --bundle /work/bundle.yaml --check /work/pgroles.yaml
The --check flag exits with code 2 on drift and 0 on match, so any CI system that respects exit codes works as a gate.
What composition rejects
render-bundle and the underlying CLI bundle commands reject the same set of errors before any database inspection:
- Out-of-scope declarations: a fragment defining a role or schema facet outside its declared
scope. - Duplicate ownership: two fragments claiming the same role name, grant key, default-privilege key, or membership selector.
- Overlapping schema facets: two fragments both managing the
ownerfacet of the same schema, or both managing thebindingsfacet.
The error message names both fragments and the conflicting key, so reviewers can resolve it without running the database.
Limitations
- The operator does not surface a
--check-style validator on thePostgresPolicyresource itself. Drift between the source bundle and a hand-editedpgroles.yamlis caught only at--checktime, not atkubectl applytime. Treat the rendered manifest as machine-generated and rely on the CI gate. - There is no operator-native way to fan out a bundle into multiple
PostgresPolicyresources today. One bundle composes to one rendered manifest, which becomes onePostgresPolicy. Multi-database setups need one bundle per database (cross-environment fragments are shared by reference). - The schema-version marker in the rendered header (
pgroles.manifest.v1) only changes on incompatiblePolicyManifestshape changes. A pgroles upgrade that bumps the schema requires a re-render and a fresh commit.
See also
- CLI commands → render-bundle
- Manifest reference → Bundle mode for fragment and bundle field reference
- Kubernetes operator
- CI/CD integration