mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-10 06:24:09 +09:00
fix(postgres): refuse system databases and never drop them (#5155)
<!-- Thank you for making a contribution! Here are some tips for you:
- Use Conventional Commits for the PR title: `type(scope): description`
- Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
- Scopes are not an exhaustive list — pick the most specific scope for
the change and extend the list when a genuinely new area appears.
Examples:
- System components: dashboard, platform, operator, cilium, kube-ovn,
linstor, fluxcd, cluster-api
- Managed apps: postgres, mariadb, redis, kafka, clickhouse,
virtual-machine, kubernetes
- Development and maintenance: api, hack, tests, ci, docs, maintenance
- Breaking changes: append `!` after type/scope (`feat(api)!: ...`) or
add a `BREAKING CHANGE:` footer
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `kind/backport` if it's a bugfix that needs to be
backported to a previous version.
-->
## What this PR does
`databases` accepts `postgres`, `template0` and `template1`, and the
init job treats them like application databases: it comments them
`database managed by helm`, creates `<db>_admin` and `<db>_readonly`,
makes `<db>_admin` the owner of the database and its schemas, and
installs the `trigger_auto_grant` event trigger. When the entry later
leaves values, the delete pass runs `DROP DATABASE` on it, which can
only fail:
- `template1`, `template0`: `cannot drop a template database`;
- `postgres`: `cannot drop the currently open database`, since the job
is connected to it.
The init job then fails on every upgrade, so the HelmRelease sits in
`UpgradeFailed` with `post-upgrade hooks failed`, and putting the entry
back does not help. `template1` also hands the takeover (owner of
`public`, default privileges, trigger) to every database created
afterwards. Once its roles are dropped by hand, the trigger left behind
names a role that no longer exists, so `CREATE SCHEMA` fails in every
new database, even as superuser.
This PR:
- **Refuses** the three names at render with a message that says what to
do. `cozyvalues-gen` has no way to constrain the keys of a
`map[string]Database`, so the refusal sits next to the existing
database-name guard, like the `users.postgres` refusal. The field
description says it too.
- **Never selects them for `DROP DATABASE`.** The delete pass excludes
them whatever values say.
- **Hands a taken-over system database back.** In a new `release system
databases` pass before the delete pass, for each of the three still
commented `database managed by helm`: drop the trigger and
`auto_grant_schema_privileges()`, `REASSIGN OWNED` / `DROP OWNED` for
its two roles in that database, give `public` back to
`pg_database_owner` (PostgreSQL 15+), and clear the comment. The
existing orphaned-roles pass then drops the roles; it never visits
`template1`, hence the per-database step. `template0` accepts no
connection and the job that commented it never got further, so only its
comment is cleared.
**Upgrade note.** A release that declares one of the three names fails
to render until the entry is removed. Removing it is now safe: the next
run releases the database instead of dropping it.
**Testing**
- `tests/system_databases_test.yaml`: 5 of its 6 cases fail on `main`
without the change. `helm unittest packages/apps/postgres`: 215 passed.
- I replayed the scenario against a `postgres:17` container, running the
rendered `init.sh` with `PGUSER=postgres`:
1. The `main` script with `databases: {appdb, template1, postgres}`
reproduces the takeover.
2. The `main` script with only `appdb` then fails with `cannot drop the
currently open database`.
3. This branch's script on that same state exits 0. It releases both
databases (owner `postgres`, no comment, no event trigger, `public`
owned by `pg_database_owner`) and drops `postgres_admin`,
`postgres_readonly`, `template1_admin` and `template1_readonly`. A
database created afterwards gets no trigger, and `CREATE SCHEMA`
succeeds in it.
4. A second run is a no-op.
5. A `template0` commented and given roles by hand is released the same
way.
Open PR #5091 touches the same script (stable database order); the hunks
do not overlap.
Backport: the automatic cherry-pick onto `release-1.6` conflicts in
`init-script.yaml` and the generated cozyrd, so the backport to
`release-1.6` is done by hand in #5157 (same SQL, replayed against that
branch's script). `release-1.7` keeps `kind/backport`.
### Screenshots
<!-- REQUIRED for UI changes: attach screenshots or screen recordings
demonstrating
the visual impact of your changes. PRs with UI changes without
screenshots will not be merged. -->
### Downstream repositories
<!--
Cozystack is upstream for repositories that are NOT kept in sync with it
automatically, and nothing in CI compares the two sides, so a change
here can break them silently.
Which change forces what, and which file to touch, is the trigger map in
docs/agents/contributing.md under "Downstream Repositories". Walk it
before you tick anything.
IF YOU ARE AN AI AGENT OR AN LLM, THIS IS PART OF THE TASK, NOT A FORM
TO FILL IN:
1. Walk the trigger map against the actual diff of this PR, file by
file. Do not decide from the PR title or from your memory of what you
changed.
2. For every repository you tick, open the follow-up PR there and link
it on the same line. A ticked box with no link claims work that does not
exist, which is worse than an empty box. Search that repository for an
open PR or issue covering it first, and link that instead of filing a
duplicate.
3. When the follow-up is out of scope, or needs a decision that is not
yours to make, open an issue in that repository instead, link that, and
say so in the PR body. Do not push a speculative PR into another
repository just to fill a line here.
4. Do not tick "No downstream repository is affected" to make the
checklist go away. If you are unsure, leave every box empty and say so
in the PR body, so a human decides.
-->
- [x] No downstream repository is affected by this change (walked the
map: a template guard, the init script, a test suite and one field
description regenerated into README, schema, Go types and cozyrd; no
field, default, enum or name change)
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up:
- [ ]
[cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack)
- follow-up:
- [ ]
[cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack)
- follow-up:
- [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up:
- [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up:
- [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) -
follow-up:
- [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) -
follow-up:
- [ ]
[cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server)
- follow-up:
- [ ]
[cozystack/external-apps-example](https://github.com/cozystack/external-apps-example)
- follow-up:
- [ ] [cozystack/examples](https://github.com/cozystack/examples) -
follow-up:
- [ ] [cozystack/community](https://github.com/cozystack/community) -
follow-up:
### Release note
<!-- Write a release note:
- Explain what has changed internally and for users.
- Start with the same `type(scope):` prefix as in the PR title
- Follow the guidelines at
https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md.
-->
```release-note
fix(postgres): databases now refuses the PostgreSQL system databases postgres, template0 and template1, and the init job never runs DROP DATABASE on them. A system database a previous version took over is handed back to the superuser on the next upgrade (event trigger removed, ownership and public schema restored, its <db>_admin and <db>_readonly roles dropped). A release that declares one of them must remove the entry before upgrading.
```
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
<!-- coderabbit-ai-generated:v1 -->
## Summary by CodeRabbit
* **Behavior Changes**
* PostgreSQL system databases (`postgres`, `template0`, and `template1`)
are refused as managed application databases.
* Previously managed system databases are released during cleanup and
excluded from database deletion. Cleanup restores eligible ownership and
security settings and clears system database comments.
* **Documentation**
* Updated PostgreSQL configuration guidance to clarify that these system
database names are refused. Names that merely contain these strings
remain accepted.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
10 files changed
+635
-12
No files matched your search
Generated
+1
-1
@@ -58,7 +58,7 @@ type ConfigSpec struct {
|
||||
// Users configuration map. Passwords are always auto-generated and stored in the `<release>-credentials` Secret; they cannot be set from values — read a user's password from that Secret. A `password` left over in values from before the field was removed is ignored by the render and draws an admission warning, but it is not inert on an upgraded release: the chart preserves whatever password is already in the Secret, which on the first upgrade is the value that was set before removal, so that value stays the live credential until it is rotated (dedicated rotation is tracked in cozystack/community#72). The live password lives only in the Secret.
|
||||
// +kubebuilder:default:={}
|
||||
Users map[string]User `json:"users,omitempty"`
|
||||
// Databases configuration map.
|
||||
// Databases configuration map. The PostgreSQL system databases `postgres`, `template0` and `template1` are refused.
|
||||
// +kubebuilder:default:={}
|
||||
Databases map[string]Database `json:"databases,omitempty"`
|
||||
// Backup configuration.
|
||||
|
||||
@@ -347,3 +347,148 @@ spec:
|
||||
set -eu
|
||||
cd ../../..
|
||||
NAMESPACE="$NAMESPACE" examples/backups/postgres/cleanup.sh
|
||||
---
|
||||
# A system database that a version before the refusal took over is handed back
|
||||
# by the next init-job run, and only what this chart marked changes hands.
|
||||
#
|
||||
# The helm-unittest and bats suites check the rendered SQL against a fake psql;
|
||||
# only a real PostgreSQL can say that the predicates select what they claim. The
|
||||
# state an older version left is rebuilt by hand as the superuser, since the
|
||||
# chart now refuses to produce it:
|
||||
# * postgres is commented as managed and owned by a postgres_admin role that
|
||||
# carries the cozystack:managed mark, as the chart left it: it must come back
|
||||
# to the superuser, and the role must be dropped;
|
||||
# * template1 is commented as managed, but its template1_admin carries only the
|
||||
# role comment, which a CREATEROLE holder can forge, and not the mark: the
|
||||
# comment must be cleared and everything that role owns or was granted must
|
||||
# stay as it is, in template1 and in a database cloned from it.
|
||||
apiVersion: chainsaw.kyverno.io/v1alpha1
|
||||
kind: Test
|
||||
metadata:
|
||||
name: postgres-3-system-databases
|
||||
spec:
|
||||
catch:
|
||||
- events: {}
|
||||
- describe:
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
name: postgres-sysdb
|
||||
- podLogs:
|
||||
selector: job-name=postgres-sysdb-init-job
|
||||
tail: 200
|
||||
steps:
|
||||
- name: create-postgres
|
||||
try:
|
||||
- apply:
|
||||
file: postgres-sysdb.yaml
|
||||
# 11m: above the 600s install timeout cozystack-api gives this release;
|
||||
# see "Sizing an HR-Ready budget" in docs/agents/e2e-testing.md.
|
||||
- assert:
|
||||
timeout: 11m
|
||||
resource:
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
name: postgres-sysdb
|
||||
status:
|
||||
(conditions[?type == 'Ready']):
|
||||
- status: "True"
|
||||
- assert:
|
||||
timeout: 2m
|
||||
resource:
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: postgres-sysdb-init-job
|
||||
status:
|
||||
(conditions[?type == 'Complete']):
|
||||
- status: "True"
|
||||
|
||||
- name: rebuild-taken-over-system-databases
|
||||
try:
|
||||
- script:
|
||||
timeout: 3m
|
||||
content: |
|
||||
set -eu
|
||||
pod=$(kubectl -n "$NAMESPACE" get pod \
|
||||
-l cnpg.io/cluster=postgres-sysdb,cnpg.io/instanceRole=primary -o name | head -n1)
|
||||
[ -n "$pod" ] || { echo "no primary pod for postgres-sysdb" >&2; exit 1; }
|
||||
sql() { kubectl -n "$NAMESPACE" exec -i "$pod" -c postgres -- psql -v ON_ERROR_STOP=1 -U postgres -d "$1"; }
|
||||
sql postgres <<'SQL'
|
||||
CREATE ROLE postgres_admin NOLOGIN;
|
||||
COMMENT ON ROLE postgres_admin IS 'role managed by helm';
|
||||
GRANT "cozystack:managed" TO postgres_admin WITH INHERIT FALSE, SET FALSE;
|
||||
COMMENT ON DATABASE postgres IS 'database managed by helm';
|
||||
ALTER DATABASE postgres OWNER TO postgres_admin;
|
||||
CREATE ROLE template1_admin NOLOGIN;
|
||||
COMMENT ON ROLE template1_admin IS 'role managed by helm';
|
||||
CREATE ROLE template1_readonly NOLOGIN;
|
||||
COMMENT ON DATABASE template1 IS 'database managed by helm';
|
||||
ALTER DATABASE template1 OWNER TO template1_admin;
|
||||
SQL
|
||||
sql template1 <<'SQL'
|
||||
ALTER SCHEMA public OWNER TO template1_admin;
|
||||
CREATE TABLE public.kept (id int);
|
||||
ALTER TABLE public.kept OWNER TO template1_admin;
|
||||
GRANT SELECT ON public.kept TO template1_readonly;
|
||||
SQL
|
||||
sql postgres <<'SQL'
|
||||
CREATE DATABASE sysdb_clone TEMPLATE template1;
|
||||
SQL
|
||||
# The next run must be a new Job: remove the completed one so the
|
||||
# assert below cannot match it.
|
||||
kubectl -n "$NAMESPACE" delete job postgres-sysdb-init-job --wait=true
|
||||
# Declaring a user is enough for an upgrade, and runs the init-job.
|
||||
kubectl -n "$NAMESPACE" patch postgres.apps.cozystack.io sysdb --type merge \
|
||||
-p '{"spec":{"users":{"sysdbuser":{},"sysdbprobe":{}}}}'
|
||||
check:
|
||||
($error == null): true
|
||||
- assert:
|
||||
timeout: 11m
|
||||
resource:
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: postgres-sysdb-init-job
|
||||
status:
|
||||
(conditions[?type == 'Complete']):
|
||||
- status: "True"
|
||||
|
||||
- name: verify-release
|
||||
try:
|
||||
- script:
|
||||
timeout: 2m
|
||||
content: |
|
||||
set -eu
|
||||
pod=$(kubectl -n "$NAMESPACE" get pod \
|
||||
-l cnpg.io/cluster=postgres-sysdb,cnpg.io/instanceRole=primary -o name | head -n1)
|
||||
q() { kubectl -n "$NAMESPACE" exec "$pod" -c postgres -- psql -v ON_ERROR_STOP=1 -U postgres -tA -d "$1" -c "$2"; }
|
||||
fail=0
|
||||
expect() {
|
||||
got=$(q "$1" "$3")
|
||||
if [ "$got" != "$4" ]; then
|
||||
echo "FAIL: $2: got '$got', want '$4'" >&2
|
||||
fail=1
|
||||
fi
|
||||
}
|
||||
comment="SELECT coalesce(shobj_description(oid, 'pg_database'), '<none>') FROM pg_database WHERE datname = "
|
||||
owner="SELECT pg_get_userbyid(datdba) FROM pg_database WHERE datname = "
|
||||
expect postgres "postgres comment cleared" "$comment'postgres'" '<none>'
|
||||
expect postgres "postgres back to the superuser" "$owner'postgres'" postgres
|
||||
expect postgres "marked postgres_admin dropped" "SELECT count(*) FROM pg_roles WHERE rolname = 'postgres_admin'" 0
|
||||
expect postgres "template1 comment cleared" "$comment'template1'" '<none>'
|
||||
expect postgres "unmarked template1_admin still exists" "SELECT count(*) FROM pg_roles WHERE rolname = 'template1_admin'" 1
|
||||
expect postgres "template1 still owned by the unmarked role" "$owner'template1'" template1_admin
|
||||
for db in template1 sysdb_clone; do
|
||||
expect "$db" "$db public still owned by the unmarked role" \
|
||||
"SELECT pg_get_userbyid(nspowner) FROM pg_namespace WHERE nspname = 'public'" template1_admin
|
||||
expect "$db" "$db kept table still owned by the unmarked role" \
|
||||
"SELECT tableowner FROM pg_tables WHERE schemaname = 'public' AND tablename = 'kept'" template1_admin
|
||||
expect "$db" "$db grant to the unmarked readonly role kept" \
|
||||
"SELECT has_table_privilege('template1_readonly', 'public.kept', 'SELECT')" t
|
||||
done
|
||||
exit "$fail"
|
||||
check:
|
||||
($error == null): true
|
||||
# The Postgres CR was applied by chainsaw; deleting it removes the whole
|
||||
# cluster, the databases and roles built above included.
|
||||
@@ -0,0 +1,18 @@
|
||||
apiVersion: apps.cozystack.io/v1alpha1
|
||||
kind: Postgres
|
||||
metadata:
|
||||
name: sysdb
|
||||
spec:
|
||||
external: false
|
||||
size: 1Gi
|
||||
replicas: 1
|
||||
storageClass: ""
|
||||
quorum:
|
||||
minSyncReplicas: 0
|
||||
maxSyncReplicas: 0
|
||||
users:
|
||||
sysdbuser: {}
|
||||
databases:
|
||||
appdb: {}
|
||||
resources: {}
|
||||
resourcesPreset: "nano"
|
||||
@@ -0,0 +1,261 @@
|
||||
#!/usr/bin/env bats
|
||||
# Behavioural tests for the "release system databases" pass of the Postgres
|
||||
# init script:
|
||||
#
|
||||
# packages/apps/postgres/templates/init-script.yaml
|
||||
#
|
||||
# The pass is cut out of the rendered script and RUN against a fake psql that
|
||||
# records which database each session connects to. The helm-unittest suite in
|
||||
# packages/apps/postgres/tests/system_databases_test.yaml matches the SQL text,
|
||||
# and stayed green when the template0 guard was inverted: the rendered pass then
|
||||
# connected to template0, which accepts no connection, and skipped postgres and
|
||||
# template1, which are the two it exists to hand back.
|
||||
#
|
||||
# What is pinned here:
|
||||
# * no session ever connects to template0;
|
||||
# * postgres and template1 are each connected to and released;
|
||||
# * every taken-over system database, template0 included, has its comment
|
||||
# cleared;
|
||||
# * releasing template1 visits every other connectable database, for the
|
||||
# trigger it passed on to them, and a failed lookup of those databases
|
||||
# stops the pass before the comment that selects template1 is cleared;
|
||||
# * the session that releases a system database hands its public schema
|
||||
# and the database itself back only from a marked <db>_admin or
|
||||
# <db>_readonly owner (or cozystack:orphaned), before the reassignment,
|
||||
# an inherited public only from a marked template1_admin, and touches
|
||||
# only the roles that carry the
|
||||
# "cozystack:managed" mark, not a role that merely has one of the names.
|
||||
#
|
||||
# Run from the repo root with make bats-unit-tests. Direct status checks
|
||||
# retain compatibility with cozytest, which has no Bats run/setup helpers.
|
||||
# Inline cleanup preserves Bats' TAP handler on failed assertions.
|
||||
|
||||
load test_helper
|
||||
|
||||
PG_CHART=packages/apps/postgres
|
||||
|
||||
# render_pass <out>
|
||||
# Extract the release pass, from its banner to the next one, out of the rendered
|
||||
# init script. The non-empty check must be an early `return 1`: hack/cozytest.sh
|
||||
# rewrites every line matching ^}$ into `return 0` plus `}`, helpers included,
|
||||
# so a check left in last-command position has its status discarded.
|
||||
render_pass() {
|
||||
helm template pg-test "$PG_CHART" --namespace tenant-test --set-json '_cluster={"scheduling":{}}' 2>/dev/null \
|
||||
| yq eval 'select(.metadata.name == "pg-test-init-script") | .stringData["init.sh"]' - \
|
||||
| awk '/^echo "== release system databases"/ { f = 1 } /^echo "== delete databases"/ { f = 0 } f' > "$1"
|
||||
grep -q 'SYSTEM_DBS=' "$1" || return 1
|
||||
}
|
||||
|
||||
# write_fake_psql <dir>
|
||||
# Emit a fake psql. A `-c` query answers from $SYSTEM_DBS for the system
|
||||
# database lookup and from $OTHER_DBS for the connectable-database lookup,
|
||||
# which exits with $CLONE_LOOKUP_FAILS instead when that is set. A
|
||||
# session fed on stdin logs "connect <dbname>" ("connect -" without --dbname)
|
||||
# followed by its first line and every -v db= value, to $PLOG, and its whole
|
||||
# input under a "-- session <dbname> db=<value>" header to $PLOG.sql.
|
||||
# No line below is a bare column-0 `}`, so cozytest.sh's parser leaves it alone.
|
||||
write_fake_psql() {
|
||||
cat > "$1/psql" <<'PEOF'
|
||||
#!/bin/sh
|
||||
dbname=- query= vdb=
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--dbname) dbname=$2; shift ;;
|
||||
-c) query=$2; shift ;;
|
||||
-v) case "$2" in db=*) vdb=${2#db=} ;; esac; shift ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
if [ -n "$query" ]; then
|
||||
case "$query" in
|
||||
*"d.datname IN ('postgres', 'template0', 'template1')"*) printf '%s\n' $SYSTEM_DBS ;;
|
||||
*datallowconn*)
|
||||
if [ -n "${CLONE_LOOKUP_FAILS:-}" ]; then
|
||||
echo "fake psql: connectable-database lookup failed" >&2
|
||||
exit "$CLONE_LOOKUP_FAILS"
|
||||
fi
|
||||
printf '%s\n' $OTHER_DBS ;;
|
||||
*) echo "fake psql: unexpected query: $query" >&2; exit 64 ;;
|
||||
esac
|
||||
exit 0
|
||||
fi
|
||||
sql=$(cat)
|
||||
echo "connect $dbname db=$vdb $(printf '%s\n' "$sql" | head -n 1)" >> "$PLOG"
|
||||
printf -- '-- session %s db=%s\n%s\n' "$dbname" "$vdb" "$sql" >> "$PLOG.sql"
|
||||
PEOF
|
||||
chmod +x "$1/psql"
|
||||
}
|
||||
|
||||
# run_pass <dir>
|
||||
# Run the extracted pass with the fake psql first on PATH; status in $rc.
|
||||
run_pass() {
|
||||
export PLOG="$1/calls"
|
||||
: > "$PLOG"
|
||||
rc=0
|
||||
PATH="$1:$PATH" sh -e "$1/pass.sh" > "$1/out" 2> "$1/err" || rc=$?
|
||||
}
|
||||
|
||||
@test "a render that produces no release pass fails instead of passing as a no-op" {
|
||||
tmp=$(mktemp -d)
|
||||
if PG_CHART=packages/apps/no-such-chart render_pass "$tmp/pass.sh"; then
|
||||
echo "FAIL: render_pass reported success for a chart that does not exist"
|
||||
false
|
||||
fi
|
||||
rm -rf "$tmp"
|
||||
}
|
||||
|
||||
@test "release pass connects to postgres and template1, never to template0" {
|
||||
tmp=$(mktemp -d)
|
||||
write_fake_psql "$tmp"
|
||||
render_pass "$tmp/pass.sh"
|
||||
export SYSTEM_DBS="postgres template0 template1" OTHER_DBS="postgres app"
|
||||
run_pass "$tmp"
|
||||
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
echo "FAIL: the pass exited $rc"
|
||||
cat "$tmp/out" "$tmp/err"
|
||||
false
|
||||
fi
|
||||
if grep -q '^connect template0 ' "$tmp/calls"; then
|
||||
echo "FAIL: the pass connected to template0, which accepts no connection"
|
||||
cat "$tmp/calls"
|
||||
false
|
||||
fi
|
||||
for db in postgres template1; do
|
||||
if ! grep -q "^connect $db db=$db DROP EVENT TRIGGER IF EXISTS trigger_auto_grant;" "$tmp/calls"; then
|
||||
echo "FAIL: $db was not connected to and released"
|
||||
cat "$tmp/calls"
|
||||
false
|
||||
fi
|
||||
done
|
||||
for db in postgres template0 template1; do
|
||||
if ! grep -q "^connect - db=$db COMMENT ON DATABASE :\"db\" IS NULL;" "$tmp/calls"; then
|
||||
echo "FAIL: the comment on $db was not cleared"
|
||||
cat "$tmp/calls"
|
||||
false
|
||||
fi
|
||||
done
|
||||
rm -rf "$tmp"
|
||||
}
|
||||
|
||||
@test "release session hands public back and touches only marked roles" {
|
||||
tmp=$(mktemp -d)
|
||||
write_fake_psql "$tmp"
|
||||
render_pass "$tmp/pass.sh"
|
||||
export SYSTEM_DBS="postgres" OTHER_DBS="postgres"
|
||||
run_pass "$tmp"
|
||||
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
echo "FAIL: the pass exited $rc"
|
||||
cat "$tmp/out" "$tmp/err"
|
||||
false
|
||||
fi
|
||||
awk '/^-- session / { f = ($0 == "-- session postgres db=postgres") } f' "$tmp/calls.sql" > "$tmp/release.sql"
|
||||
# Each statement must end on its whole predicate: a clause appended after the
|
||||
# mark (an OR, say) would select an unmarked role again.
|
||||
reassign_where="WHERE EXISTS (SELECT FROM pg_auth_members m JOIN pg_roles g ON g.oid = m.roleid JOIN pg_roles u ON u.oid = m.member WHERE g.rolname = 'cozystack:managed' AND u.rolname = r)\\gexec"
|
||||
owner_marked="(o.rolname = 'cozystack:orphaned' OR o.rolname IN (:'db' || '_admin', :'db' || '_readonly') AND EXISTS (SELECT FROM pg_auth_members m JOIN pg_roles g ON g.oid = m.roleid WHERE g.rolname = 'cozystack:managed' AND m.member = o.oid))\\gexec"
|
||||
public_where="WHERE n.nspname = 'public' AND $owner_marked"
|
||||
database_where="WHERE d.datname = :'db' AND $owner_marked"
|
||||
public_line=$(grep -n 'ALTER SCHEMA public OWNER TO' "$tmp/release.sql" | head -1)
|
||||
database_line=$(grep -n 'ALTER DATABASE' "$tmp/release.sql" | head -1)
|
||||
reassign_line=$(grep -n 'REASSIGN OWNED BY' "$tmp/release.sql" | head -1)
|
||||
if [ "${public_line##*"$public_where"}" != "" ] || [ -z "$public_line" ]; then
|
||||
echo "FAIL: the release session does not hand public back only from a marked <db>_admin or <db>_readonly owner"
|
||||
cat "$tmp/release.sql"
|
||||
false
|
||||
fi
|
||||
if [ "${database_line##*"$database_where"}" != "" ] || [ -z "$database_line" ]; then
|
||||
echo "FAIL: the release session does not take the database back only from a marked owner"
|
||||
cat "$tmp/release.sql"
|
||||
false
|
||||
fi
|
||||
if [ "${reassign_line##*"$reassign_where"}" != "" ] || [ -z "$reassign_line" ]; then
|
||||
echo "FAIL: the release session does not reassign only roles carrying the cozystack:managed mark"
|
||||
cat "$tmp/release.sql"
|
||||
false
|
||||
fi
|
||||
# public and the database must be handed back before REASSIGN OWNED would
|
||||
# move them to cozystack:orphaned.
|
||||
if [ "${public_line%%:*}" -ge "${reassign_line%%:*}" ] || [ "${database_line%%:*}" -ge "${reassign_line%%:*}" ]; then
|
||||
echo "FAIL: public or the database is handed back after the reassignment has already moved it"
|
||||
cat "$tmp/release.sql"
|
||||
false
|
||||
fi
|
||||
rm -rf "$tmp"
|
||||
}
|
||||
|
||||
@test "release pass of template1 visits every other connectable database" {
|
||||
tmp=$(mktemp -d)
|
||||
write_fake_psql "$tmp"
|
||||
render_pass "$tmp/pass.sh"
|
||||
export SYSTEM_DBS="template1" OTHER_DBS="postgres app"
|
||||
run_pass "$tmp"
|
||||
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
echo "FAIL: the pass exited $rc"
|
||||
cat "$tmp/out" "$tmp/err"
|
||||
false
|
||||
fi
|
||||
for db in postgres app; do
|
||||
if ! grep -q "^connect $db db= SELECT 'DROP EVENT TRIGGER IF EXISTS trigger_auto_grant'" "$tmp/calls"; then
|
||||
echo "FAIL: $db was not checked for the trigger inherited from template1"
|
||||
cat "$tmp/calls"
|
||||
false
|
||||
fi
|
||||
done
|
||||
# An inherited public changes hands only from a template1_admin the chart
|
||||
# marked; an owner that merely has the name keeps it.
|
||||
inherited_where="WHERE n.nspname = 'public' AND o.rolname = 'template1_admin' AND EXISTS (SELECT FROM pg_auth_members m JOIN pg_roles g ON g.oid = m.roleid WHERE g.rolname = 'cozystack:managed' AND m.member = o.oid)\\gexec"
|
||||
inherited_line=$(awk '/^-- session / { f = ($0 == "-- session app db=") } f' "$tmp/calls.sql" | grep 'ALTER SCHEMA public OWNER TO' | head -1)
|
||||
if [ -z "$inherited_line" ] || [ "${inherited_line##*"$inherited_where"}" != "" ]; then
|
||||
echo "FAIL: an inherited public is handed back without checking that a marked template1_admin owns it"
|
||||
cat "$tmp/calls.sql"
|
||||
false
|
||||
fi
|
||||
rm -rf "$tmp"
|
||||
}
|
||||
|
||||
@test "release pass of template1 stops before clearing its comment when the clone lookup fails" {
|
||||
# Clearing the comment is what stops later runs from selecting template1, so
|
||||
# a lookup failure that let the pass go on would skip the clone repair for
|
||||
# good.
|
||||
tmp=$(mktemp -d)
|
||||
write_fake_psql "$tmp"
|
||||
render_pass "$tmp/pass.sh"
|
||||
export SYSTEM_DBS="template1" OTHER_DBS="postgres app" CLONE_LOOKUP_FAILS=73
|
||||
run_pass "$tmp"
|
||||
unset CLONE_LOOKUP_FAILS
|
||||
|
||||
if [ "$rc" -ne 73 ]; then
|
||||
echo "FAIL: the pass exited $rc instead of stopping on the failed lookup (73)"
|
||||
cat "$tmp/out" "$tmp/err" "$tmp/calls"
|
||||
false
|
||||
fi
|
||||
if grep -q 'COMMENT ON DATABASE' "$tmp/calls"; then
|
||||
echo "FAIL: the comment on template1 was cleared although its clones were never visited"
|
||||
cat "$tmp/calls"
|
||||
false
|
||||
fi
|
||||
rm -rf "$tmp"
|
||||
}
|
||||
|
||||
@test "release pass of postgres alone leaves the other databases alone" {
|
||||
tmp=$(mktemp -d)
|
||||
write_fake_psql "$tmp"
|
||||
render_pass "$tmp/pass.sh"
|
||||
export SYSTEM_DBS="postgres" OTHER_DBS="postgres app"
|
||||
run_pass "$tmp"
|
||||
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
echo "FAIL: the pass exited $rc"
|
||||
cat "$tmp/out" "$tmp/err"
|
||||
false
|
||||
fi
|
||||
if grep -q '^connect app ' "$tmp/calls"; then
|
||||
echo "FAIL: the pass visited app although template1 was not taken over"
|
||||
cat "$tmp/calls"
|
||||
false
|
||||
fi
|
||||
rm -rf "$tmp"
|
||||
}
|
||||
@@ -279,13 +279,13 @@ For `sslmode=verify-full` to work, the CA bundle retrieved above must be saved t
|
||||
|
||||
### Databases configuration
|
||||
|
||||
| Name | Description | Type | Value |
|
||||
| -------------------------------- | ---------------------------------------- | ------------------- | ----- |
|
||||
| `databases` | Databases configuration map. | `map[string]object` | `{}` |
|
||||
| `databases[name].roles` | Roles assigned to users. | `object` | `{}` |
|
||||
| `databases[name].roles.admin` | List of users with admin privileges. | `[]string` | `[]` |
|
||||
| `databases[name].roles.readonly` | List of users with read-only privileges. | `[]string` | `[]` |
|
||||
| `databases[name].extensions` | List of enabled PostgreSQL extensions. | `[]string` | `[]` |
|
||||
| Name | Description | Type | Value |
|
||||
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------- | ----- |
|
||||
| `databases` | Databases configuration map. The PostgreSQL system databases `postgres`, `template0` and `template1` are refused. | `map[string]object` | `{}` |
|
||||
| `databases[name].roles` | Roles assigned to users. | `object` | `{}` |
|
||||
| `databases[name].roles.admin` | List of users with admin privileges. | `[]string` | `[]` |
|
||||
| `databases[name].roles.readonly` | List of users with read-only privileges. | `[]string` | `[]` |
|
||||
| `databases[name].extensions` | List of enabled PostgreSQL extensions. | `[]string` | `[]` |
|
||||
|
||||
|
||||
### Backup parameters
|
||||
|
||||
@@ -53,6 +53,9 @@
|
||||
PostgreSQL identifiers too, capped at 63.
|
||||
*/}}
|
||||
{{- range $database, $d := .Values.databases }}
|
||||
{{- if has $database (list "postgres" "template0" "template1") }}
|
||||
{{- fail (printf "databases.%s is refused: %q is a PostgreSQL system database. Managing it would hand it, its public schema and the auto-grant event trigger to the \"%s_admin\" role (template1 then passes all of that on to every database created afterwards), and removing the entry later could not drop it. Remove the entry and declare an application database instead; the next upgrade releases a system database a previous version took over, without dropping it" $database $database $database) }}
|
||||
{{- end }}
|
||||
{{- if or (not (regexMatch "^[-._a-zA-Z0-9]+$" $database)) (gt (len $database) 54) }}
|
||||
{{- fail (printf "invalid database name %q: a database name may contain only letters, digits and the characters -._ and must be at most 54 characters (it is interpolated into superuser SQL and the init-script shell, and it seeds the \"<db>_admin\" and \"<db>_readonly\" PostgreSQL roles, which CREATE ROLE silently truncates past 63 bytes; the next init-job run then cannot drop the truncated role and fails). Changing only its key in values makes the init-job drop the database; to keep the data, a platform administrator first renames it as the CNPG superuser (ALTER DATABASE, and ALTER ROLE for its <db>_admin and <db>_readonly roles), then the key is changed" $database) }}
|
||||
{{- end }}
|
||||
@@ -320,6 +323,67 @@ stringData:
|
||||
EOT
|
||||
done
|
||||
|
||||
echo "== release system databases"
|
||||
{{- /*
|
||||
Versions that did not refuse postgres, template0 and template1 in
|
||||
databases took them over like any other entry. DROP DATABASE on them can
|
||||
only fail (template databases cannot be dropped, and this session is
|
||||
connected to postgres), which wedged every later upgrade, so they are
|
||||
handed back instead: the auto-grant trigger goes, since it names a role
|
||||
about to be dropped and would make every CREATE SCHEMA fail; what the
|
||||
<db>_admin and <db>_readonly roles own there goes to cozystack:orphaned,
|
||||
as in the orphaned-roles pass and for the same reason (a tenant's
|
||||
SECURITY DEFINER function must not end up owned by the superuser), and is
|
||||
made SECURITY INVOKER (only a role marked "cozystack:managed" is touched:
|
||||
a role that merely carries one of those names was not created by this
|
||||
chart and keeps what it owns); the database itself returns to the superuser,
|
||||
public to pg_database_owner (to the superuser before PostgreSQL 15), each
|
||||
only when one of those marked roles owns it and before the reassignment
|
||||
would move it (or when cozystack:orphaned does, after a run that stopped
|
||||
in between); an owner the chart did not mark keeps what it owns, and
|
||||
the comment is cleared. The orphaned-roles pass below then drops the
|
||||
roles; it does not visit template1, hence the per-database pass here.
|
||||
template0 accepts no connection, and the Job that commented it never got
|
||||
further, so only the comment is cleared. A database created from
|
||||
template1 while it was taken over inherited its trigger, naming
|
||||
template1_admin, and its public schema owned by that role; the trigger
|
||||
goes, public returns to its default owner if a marked template1_admin
|
||||
still owns it, and a declared database gets
|
||||
its trigger back from the grant pass further down.
|
||||
*/}}
|
||||
SYSTEM_DBS=$(psql -v ON_ERROR_STOP=1 -t -A -c "SELECT datname FROM pg_database d JOIN pg_shdescription s ON d.oid = s.objoid WHERE s.description = 'database managed by helm' AND d.datname IN ('postgres', 'template0', 'template1')")
|
||||
echo "system databases to release: $SYSTEM_DBS"
|
||||
for db in $SYSTEM_DBS; do
|
||||
if [ "$db" != template0 ]; then
|
||||
psql -v ON_ERROR_STOP=1 --echo-all --dbname "$db" -v db="$db" <<\EOT
|
||||
DROP EVENT TRIGGER IF EXISTS trigger_auto_grant;
|
||||
DROP FUNCTION IF EXISTS auto_grant_schema_privileges();
|
||||
SELECT format('ALTER SCHEMA public OWNER TO %s', CASE WHEN current_setting('server_version_num')::int >= 150000 THEN 'pg_database_owner' ELSE 'CURRENT_USER' END) FROM pg_namespace n JOIN pg_roles o ON o.oid = n.nspowner WHERE n.nspname = 'public' AND (o.rolname = 'cozystack:orphaned' OR o.rolname IN (:'db' || '_admin', :'db' || '_readonly') AND EXISTS (SELECT FROM pg_auth_members m JOIN pg_roles g ON g.oid = m.roleid WHERE g.rolname = 'cozystack:managed' AND m.member = o.oid))\gexec
|
||||
SELECT format('ALTER DATABASE %I OWNER TO CURRENT_USER', d.datname) FROM pg_database d JOIN pg_roles o ON o.oid = d.datdba WHERE d.datname = :'db' AND (o.rolname = 'cozystack:orphaned' OR o.rolname IN (:'db' || '_admin', :'db' || '_readonly') AND EXISTS (SELECT FROM pg_auth_members m JOIN pg_roles g ON g.oid = m.roleid WHERE g.rolname = 'cozystack:managed' AND m.member = o.oid))\gexec
|
||||
SELECT format('REASSIGN OWNED BY %I TO "cozystack:orphaned"', r), format('DROP OWNED BY %I', r) FROM unnest(ARRAY[:'db' || '_admin', :'db' || '_readonly']) AS r WHERE EXISTS (SELECT FROM pg_auth_members m JOIN pg_roles g ON g.oid = m.roleid JOIN pg_roles u ON u.oid = m.member WHERE g.rolname = 'cozystack:managed' AND u.rolname = r)\gexec
|
||||
SELECT format('ALTER %s %s SECURITY INVOKER', CASE p.prokind WHEN 'p' THEN 'PROCEDURE' ELSE 'FUNCTION' END, p.oid::regprocedure) FROM pg_proc p WHERE p.proowner = '"cozystack:orphaned"'::regrole AND p.prosecdef\gexec
|
||||
SELECT format('ALTER VIEW %s SET (security_invoker = true)', c.oid::regclass) FROM pg_class c WHERE c.relowner = '"cozystack:orphaned"'::regrole AND c.relkind = 'v' AND current_setting('server_version_num')::int >= 150000 AND NOT coalesce('security_invoker=true' = ANY (c.reloptions), false)\gexec
|
||||
EOT
|
||||
fi
|
||||
if [ "$db" = template1 ]; then
|
||||
{{- /*
|
||||
An assignment of its own, so that set -e stops the pass when the lookup
|
||||
fails: inside the for list its status is discarded, the comment below is
|
||||
cleared without a clone visited, and no later run selects template1 again.
|
||||
*/}}
|
||||
CLONE_DBS=$(psql -v ON_ERROR_STOP=1 -t -A -c "SELECT datname FROM pg_database WHERE datallowconn AND NOT datistemplate")
|
||||
for other in $CLONE_DBS; do
|
||||
psql -v ON_ERROR_STOP=1 --echo-queries --dbname "$other" <<\EOT
|
||||
SELECT 'DROP EVENT TRIGGER IF EXISTS trigger_auto_grant', 'DROP FUNCTION auto_grant_schema_privileges()' WHERE position('''template1_admin''' IN coalesce((SELECT prosrc FROM pg_proc WHERE oid = to_regprocedure('auto_grant_schema_privileges()')), '')) > 0\gexec
|
||||
SELECT format('ALTER SCHEMA public OWNER TO %s', CASE WHEN current_setting('server_version_num')::int >= 150000 THEN 'pg_database_owner' ELSE 'CURRENT_USER' END) FROM pg_namespace n JOIN pg_roles o ON o.oid = n.nspowner WHERE n.nspname = 'public' AND o.rolname = 'template1_admin' AND EXISTS (SELECT FROM pg_auth_members m JOIN pg_roles g ON g.oid = m.roleid WHERE g.rolname = 'cozystack:managed' AND m.member = o.oid)\gexec
|
||||
EOT
|
||||
done
|
||||
fi
|
||||
psql -v ON_ERROR_STOP=1 --echo-all -v db="$db" <<\EOT
|
||||
COMMENT ON DATABASE :"db" IS NULL;
|
||||
EOT
|
||||
done
|
||||
|
||||
echo "== delete databases"
|
||||
{{- /*
|
||||
A restore replaces spec.databases with the Backup's snapshot, but recovery
|
||||
@@ -338,7 +402,7 @@ stringData:
|
||||
ADOPTED_MARK=$(psql -v ON_ERROR_STOP=1 -t -A --dbname postgres -c "SELECT current_setting('cozystack.adopted_restore', true)")
|
||||
MANAGED_DBS=$(psql -v ON_ERROR_STOP=1 -t -A -c "SELECT datname FROM pg_database d JOIN pg_shdescription s ON d.oid = s.objoid WHERE s.description = 'database managed by helm'")
|
||||
DEFINED_DBS="{{ join " " (keys .Values.databases | sortAlpha) }}"
|
||||
DELETE_DBS=$(for db in $MANAGED_DBS; do case " $DEFINED_DBS " in *" $db "*) :;; *) echo $db;; esac; done)
|
||||
DELETE_DBS=$(for db in $MANAGED_DBS; do case " postgres template0 template1 $DEFINED_DBS " in *" $db "*) :;; *) echo $db;; esac; done)
|
||||
|
||||
if [ -n "$RESTORE_MARK" ] && [ "$ADOPTED_MARK" != "$RESTORE_MARK" ]; then
|
||||
echo "databases the restore brought back that values do not declare, released from management: $DELETE_DBS"
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
suite: init-script never takes over or drops a system database
|
||||
|
||||
# Each of postgres, template0 and template1 used to be accepted in databases and
|
||||
# handled like an application database: owned by <db>_admin, given the
|
||||
# auto-grant event trigger, and on removal sent to DROP DATABASE, which fails on
|
||||
# all three (template databases cannot be dropped, and the Job is connected to
|
||||
# postgres) and wedged every later upgrade. template1 also passed the takeover
|
||||
# on to every database created afterwards.
|
||||
|
||||
templates:
|
||||
- templates/init-script.yaml
|
||||
|
||||
release:
|
||||
name: pg-test
|
||||
namespace: tenant-test
|
||||
|
||||
set:
|
||||
_cluster:
|
||||
scheduling: {}
|
||||
|
||||
tests:
|
||||
- it: refuses postgres in databases
|
||||
set:
|
||||
databases:
|
||||
postgres: {}
|
||||
asserts:
|
||||
- failedTemplate:
|
||||
errorPattern: "databases.postgres is refused: \"postgres\" is a PostgreSQL system database"
|
||||
|
||||
- it: refuses template0 in databases
|
||||
set:
|
||||
databases:
|
||||
template0: {}
|
||||
asserts:
|
||||
- failedTemplate:
|
||||
errorPattern: "databases.template0 is refused: \"template0\" is a PostgreSQL system database"
|
||||
|
||||
- it: refuses template1 in databases
|
||||
set:
|
||||
databases:
|
||||
template1: {}
|
||||
asserts:
|
||||
- failedTemplate:
|
||||
errorPattern: "databases.template1 is refused: \"template1\" is a PostgreSQL system database"
|
||||
|
||||
- it: still accepts an application database whose name contains a system name
|
||||
set:
|
||||
databases:
|
||||
postgres_app: {}
|
||||
mytemplate1: {}
|
||||
documentSelector:
|
||||
path: metadata.name
|
||||
value: pg-test-init-script
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: stringData["init.sh"]
|
||||
pattern: 'CREATE DATABASE "postgres_app"'
|
||||
|
||||
# A release that a previous version let manage a system database still carries
|
||||
# its comment. The delete pass must never select it, whatever values say.
|
||||
- it: never lists a system database for DROP DATABASE
|
||||
set:
|
||||
databases:
|
||||
appdb: {}
|
||||
documentSelector:
|
||||
path: metadata.name
|
||||
value: pg-test-init-script
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: stringData["init.sh"]
|
||||
pattern: 'DELETE_DBS=\$\(for db in \$MANAGED_DBS; do case " postgres template0 template1 \$DEFINED_DBS " in \*" \$db "\*\) :;; \*\) echo \$db;; esac; done\)'
|
||||
|
||||
- it: hands a taken-over system database back before the delete pass
|
||||
documentSelector:
|
||||
path: metadata.name
|
||||
value: pg-test-init-script
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: stringData["init.sh"]
|
||||
pattern: "SYSTEM_DBS=\\$\\(psql -v ON_ERROR_STOP=1 -t -A -c \"SELECT datname FROM pg_database d JOIN pg_shdescription s ON d.oid = s.objoid WHERE s.description = 'database managed by helm' AND d.datname IN \\('postgres', 'template0', 'template1'\\)\"\\)"
|
||||
- matchRegex:
|
||||
path: stringData["init.sh"]
|
||||
pattern: '(?s)== release system databases.*DROP EVENT TRIGGER IF EXISTS trigger_auto_grant;\s+DROP FUNCTION IF EXISTS auto_grant_schema_privileges\(\);.*ALTER SCHEMA public OWNER TO %s.*ALTER DATABASE %I OWNER TO CURRENT_USER.*REASSIGN OWNED BY %I TO "cozystack:orphaned"''.*DROP OWNED BY %I.*COMMENT ON DATABASE :"db" IS NULL;.*== delete databases'
|
||||
|
||||
# A tenant holding <db>_admin may have created a SECURITY DEFINER function
|
||||
# there; handed to the superuser it would run with superuser rights for
|
||||
# anyone PUBLIC can execute it. It goes to cozystack:orphaned and is made
|
||||
# SECURITY INVOKER, as in the orphaned-roles pass.
|
||||
- it: never hands a released system database's objects to the superuser
|
||||
documentSelector:
|
||||
path: metadata.name
|
||||
value: pg-test-init-script
|
||||
asserts:
|
||||
- notMatchRegex:
|
||||
path: stringData["init.sh"]
|
||||
pattern: 'REASSIGN OWNED BY %I TO CURRENT_USER'
|
||||
- matchRegex:
|
||||
path: stringData["init.sh"]
|
||||
pattern: '(?s)== release system databases.*ALTER %s %s SECURITY INVOKER.*ALTER VIEW %s SET \(security_invoker = true\).*== delete databases'
|
||||
|
||||
# A database created from template1 while it was taken over inherited the
|
||||
# trigger naming template1_admin, which the orphaned-roles pass then drops;
|
||||
# CREATE SCHEMA would fail there from that point on.
|
||||
- it: drops the template1 trigger inherited by other databases
|
||||
documentSelector:
|
||||
path: metadata.name
|
||||
value: pg-test-init-script
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: stringData["init.sh"]
|
||||
pattern: "(?s)== release system databases.*if \\[ \"\\$db\" = template1 \\]; then.*SELECT 'DROP EVENT TRIGGER IF EXISTS trigger_auto_grant', 'DROP FUNCTION auto_grant_schema_privileges\\(\\)' WHERE position\\('''template1_admin''' IN .*== delete databases"
|
||||
|
||||
# That clone's public schema also came from template1, owned by
|
||||
# template1_admin; left to the orphaned-roles pass it would end up owned by
|
||||
# cozystack:orphaned and the database owner would lose CREATE on it.
|
||||
- it: gives an inherited public schema back to its default owner
|
||||
documentSelector:
|
||||
path: metadata.name
|
||||
value: pg-test-init-script
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: stringData["init.sh"]
|
||||
pattern: "(?s)if \\[ \"\\$db\" = template1 \\]; then.*SELECT format\\('ALTER SCHEMA public OWNER TO %s', .*\\) FROM pg_namespace n JOIN pg_roles o ON o.oid = n.nspowner WHERE n.nspname = 'public' AND o.rolname = 'template1_admin' AND EXISTS \\(SELECT FROM pg_auth_members m JOIN pg_roles g ON g.oid = m.roleid WHERE g.rolname = 'cozystack:managed' AND m.member = o.oid\\).*== delete databases"
|
||||
|
||||
# template0 accepts no connection: only postgres and template1 are connected
|
||||
# to and released, template0 just has its comment cleared. The behaviour is
|
||||
# run against a fake psql in hack/postgres-release-system-databases.bats.
|
||||
- it: connects to every released system database but template0
|
||||
documentSelector:
|
||||
path: metadata.name
|
||||
value: pg-test-init-script
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: stringData["init.sh"]
|
||||
pattern: "(?s)for db in \\$SYSTEM_DBS; do\\s+if \\[ \"\\$db\" != template0 \\]; then\\s+psql -v ON_ERROR_STOP=1 --echo-all --dbname \"\\$db\" -v db=\"\\$db\""
|
||||
@@ -313,7 +313,7 @@
|
||||
}
|
||||
},
|
||||
"databases": {
|
||||
"description": "Databases configuration map.",
|
||||
"description": "Databases configuration map. The PostgreSQL system databases `postgres`, `template0` and `template1` are refused.",
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"additionalProperties": {
|
||||
|
||||
@@ -287,7 +287,7 @@ users: {}
|
||||
## @field {DatabaseRoles} [roles] - Roles assigned to users.
|
||||
## @field {[]string} [extensions] - List of enabled PostgreSQL extensions.
|
||||
|
||||
## @param {map[string]Database} databases - Databases configuration map.
|
||||
## @param {map[string]Database} databases - Databases configuration map. The PostgreSQL system databases `postgres`, `template0` and `template1` are refused.
|
||||
databases: {}
|
||||
## Example:
|
||||
## databases:
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ spec:
|
||||
singular: postgres
|
||||
plural: postgreses
|
||||
openAPISchema: |-
|
||||
{"title":"Chart Values","type":"object","properties":{"replicas":{"description":"Number of Postgres replicas. Under active autoscaling this is the resting/seed count, not the live count — KEDA drives the live count via `/scale`. Disabling autoscaling does NOT automatically bring the live count back to `replicas`: the client-side merge writes `instances` only when the rendered value changes, so a cluster KEDA grew past `replicas` stays there with no autoscaler left to reduce it — lower `replicas` to the count you want and it rebases down (shedding the surplus replicas + PVCs). Lowering it below the live count, or disabling/dry-running while it sits below the live count, sheds in one step past the scaleDown pacing; stage it to the live count first (suspend the HelmRelease and `kubectl scale` if needed) to avoid a transient. Must stay greater than `quorum.maxSyncReplicas`.","type":"integer","default":2},"resources":{"description":"Explicit CPU and memory configuration for each PostgreSQL replica. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"CPU available to each replica.","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Memory (RAM) available to each replica.","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}},"resourcesPreset":{"description":"Default sizing preset used when `resources` is omitted.","type":"string","default":"t1.micro","enum":["t1.nano","t1.micro","t1.small","t1.medium","t1.large","t1.xlarge","t1.2xlarge","t1.4xlarge","c1.nano","c1.micro","c1.small","c1.medium","c1.large","c1.xlarge","c1.2xlarge","c1.4xlarge","s1.nano","s1.micro","s1.small","s1.medium","s1.large","s1.xlarge","s1.2xlarge","s1.4xlarge","u1.nano","u1.micro","u1.small","u1.medium","u1.large","u1.xlarge","u1.2xlarge","u1.4xlarge","m1.nano","m1.micro","m1.small","m1.medium","m1.large","m1.xlarge","m1.2xlarge","m1.4xlarge","nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume Claim size available for application data.","default":"10Gi","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"storageClass":{"description":"StorageClass used to store the data.","type":"string","default":"","x-kubernetes-validations":[{"rule":"self == oldSelf","message":"storageClass is immutable"}],"x-cozystack-options":{"source":"storageclass"}},"external":{"description":"Enable external access from outside the cluster.","type":"boolean","default":false},"version":{"description":"PostgreSQL major version to deploy","type":"string","default":"v18","enum":["v18","v17","v16","v15","v14","v13"]},"flavor":{"description":"PostgreSQL image flavor, fixed when the cluster is created. `postgresql` uses the CloudNativePG PostgreSQL image. `postgis` uses the CloudNativePG PostGIS image, which adds postgis, postgis_raster, postgis_topology, postgis_sfcgal, postgis_tiger_geocoder and address_standardizer; enable them per database in `databases.<name>.extensions`. Changing the flavor of an existing cluster is refused, and a backup restores only into a cluster of the flavor it was taken from: to change flavor, create a new cluster and move the data with a logical dump (pg_dump / pg_restore).","type":"string","default":"postgresql","enum":["postgresql","postgis"]},"tls":{"description":"TLS configuration for server connections.","type":"object","default":{},"properties":{"enabled":{"description":"Tri-state switch controlling whether the chart injects the external hostnames into the operator-managed CNPG cert via spec.certificates.serverAltDNSNames. When omitted, the chart injects them if `external: true` and skips them otherwise. Set explicitly to `true` to inject regardless of `external` (no-op when `external: false` since there is no external hostname to add). Set to `false` to skip injection. Note that CNPG keeps its built-in TLS on the wire regardless of this flag — this toggle only controls the chart-side SAN injection; to disable PostgreSQL TLS entirely set `postgresql.parameters.ssl = \"off\"` at the CNPG layer.","type":"boolean"},"required":{"description":"Tri-state switch controlling whether the server refuses sessions that are not encrypted. When omitted, it is on if `external: true` and off otherwise. When on, the chart renders `spec.postgresql.pg_hba` with `hostssl all all all scram-sha-256` (`md5` on PostgreSQL 13, matching CloudNativePG's default rule) followed by `host all all all reject`, so a client that Line truncated
|
||||
{"title":"Chart Values","type":"object","properties":{"replicas":{"description":"Number of Postgres replicas. Under active autoscaling this is the resting/seed count, not the live count — KEDA drives the live count via `/scale`. Disabling autoscaling does NOT automatically bring the live count back to `replicas`: the client-side merge writes `instances` only when the rendered value changes, so a cluster KEDA grew past `replicas` stays there with no autoscaler left to reduce it — lower `replicas` to the count you want and it rebases down (shedding the surplus replicas + PVCs). Lowering it below the live count, or disabling/dry-running while it sits below the live count, sheds in one step past the scaleDown pacing; stage it to the live count first (suspend the HelmRelease and `kubectl scale` if needed) to avoid a transient. Must stay greater than `quorum.maxSyncReplicas`.","type":"integer","default":2},"resources":{"description":"Explicit CPU and memory configuration for each PostgreSQL replica. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"CPU available to each replica.","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Memory (RAM) available to each replica.","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}},"resourcesPreset":{"description":"Default sizing preset used when `resources` is omitted.","type":"string","default":"t1.micro","enum":["t1.nano","t1.micro","t1.small","t1.medium","t1.large","t1.xlarge","t1.2xlarge","t1.4xlarge","c1.nano","c1.micro","c1.small","c1.medium","c1.large","c1.xlarge","c1.2xlarge","c1.4xlarge","s1.nano","s1.micro","s1.small","s1.medium","s1.large","s1.xlarge","s1.2xlarge","s1.4xlarge","u1.nano","u1.micro","u1.small","u1.medium","u1.large","u1.xlarge","u1.2xlarge","u1.4xlarge","m1.nano","m1.micro","m1.small","m1.medium","m1.large","m1.xlarge","m1.2xlarge","m1.4xlarge","nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume Claim size available for application data.","default":"10Gi","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"storageClass":{"description":"StorageClass used to store the data.","type":"string","default":"","x-kubernetes-validations":[{"rule":"self == oldSelf","message":"storageClass is immutable"}],"x-cozystack-options":{"source":"storageclass"}},"external":{"description":"Enable external access from outside the cluster.","type":"boolean","default":false},"version":{"description":"PostgreSQL major version to deploy","type":"string","default":"v18","enum":["v18","v17","v16","v15","v14","v13"]},"flavor":{"description":"PostgreSQL image flavor, fixed when the cluster is created. `postgresql` uses the CloudNativePG PostgreSQL image. `postgis` uses the CloudNativePG PostGIS image, which adds postgis, postgis_raster, postgis_topology, postgis_sfcgal, postgis_tiger_geocoder and address_standardizer; enable them per database in `databases.<name>.extensions`. Changing the flavor of an existing cluster is refused, and a backup restores only into a cluster of the flavor it was taken from: to change flavor, create a new cluster and move the data with a logical dump (pg_dump / pg_restore).","type":"string","default":"postgresql","enum":["postgresql","postgis"]},"tls":{"description":"TLS configuration for server connections.","type":"object","default":{},"properties":{"enabled":{"description":"Tri-state switch controlling whether the chart injects the external hostnames into the operator-managed CNPG cert via spec.certificates.serverAltDNSNames. When omitted, the chart injects them if `external: true` and skips them otherwise. Set explicitly to `true` to inject regardless of `external` (no-op when `external: false` since there is no external hostname to add). Set to `false` to skip injection. Note that CNPG keeps its built-in TLS on the wire regardless of this flag — this toggle only controls the chart-side SAN injection; to disable PostgreSQL TLS entirely set `postgresql.parameters.ssl = \"off\"` at the CNPG layer.","type":"boolean"},"required":{"description":"Tri-state switch controlling whether the server refuses sessions that are not encrypted. When omitted, it is on if `external: true` and off otherwise. When on, the chart renders `spec.postgresql.pg_hba` with `hostssl all all all scram-sha-256` (`md5` on PostgreSQL 13, matching CloudNativePG's default rule) followed by `host all all all reject`, so a client that Line truncated
|
||||
release:
|
||||
prefix: postgres-
|
||||
labels:
|
||||
|
||||
Reference in new issue
Block a user