fix(postgres): tolerate databases declared without roles

`databases.<name>.roles` and its `admin`/`readonly` lists are optional in
the schema, but the init script dereferenced `$d.roles.admin` for every
database. A database declared with only `extensions`, or with an empty
body, failed the whole render with a nil pointer error instead of simply
granting nothing.

Treat a missing `roles`, `admin` or `readonly` as an empty list, so such a
database reconciles the same way as `roles: {}`: no grants, every managed
role revoked.

Assisted-by: LLM
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
This commit is contained in:
Andrei Kvapil committed 2026-09-11 20:19:53 +02:00
1 parent ed9ddf9c3a
commit eafc6423f3
2 files changed
+104 -2

No files matched your search

@@ -204,13 +204,18 @@ stringData:
echo "== assign roles to users"
psql -v ON_ERROR_STOP=1 --echo-all <<\EOT
{{- range $database, $d := .Values.databases }}
{{- /* roles, roles.admin and roles.readonly are all optional: a database
declared without them grants nothing and revokes every managed role. */}}
{{- $roles := default dict (default dict $d).roles }}
{{- $admin := default list $roles.admin }}
{{- $readonly := default list $roles.readonly }}
{{- range $user, $u := $.Values.users }}
{{- if has $user $d.roles.admin }}
{{- if has $user $admin }}
GRANT "{{ $database }}_admin" TO "{{ $user }}";
{{- else }}
REVOKE "{{ $database }}_admin" FROM "{{ $user }}";
{{- end }}
{{- if has $user $d.roles.readonly }}
{{- if has $user $readonly }}
GRANT "{{ $database }}_readonly" TO "{{ $user }}";
{{- else }}
REVOKE "{{ $database }}_readonly" FROM "{{ $user }}";
@@ -0,0 +1,97 @@
suite: databases render without roles
# `databases.<name>.roles`, `roles.admin` and `roles.readonly` are all optional
# in the schema. The init script used to dereference `$d.roles.admin` for every
# database, so a database declared with only `extensions` (or with nothing at
# all) failed the whole render with "nil pointer evaluating interface {}.admin".
# A missing list must read as an empty list: no user is granted the role, and
# every managed role is revoked from every user, exactly as `roles: {}` does.
templates:
- templates/init-script.yaml
release:
name: pg-test
namespace: tenant-test
set:
_cluster:
scheduling: {}
users:
app:
password: secret
tests:
- it: "a database with only extensions renders and revokes both managed roles"
set:
databases:
events:
extensions:
- hstore
documentSelector:
path: metadata.name
value: pg-test-init-script
asserts:
- matchRegex:
path: stringData["init.sh"]
pattern: 'REVOKE "events_admin" FROM "app";'
- matchRegex:
path: stringData["init.sh"]
pattern: 'REVOKE "events_readonly" FROM "app";'
- notMatchRegex:
path: stringData["init.sh"]
pattern: 'GRANT "events_(admin|readonly)" TO'
- matchRegex:
path: stringData["init.sh"]
pattern: 'CREATE EXTENSION IF NOT EXISTS hstore;'
- it: "a database with an empty body renders the same as roles: {}"
set:
databases:
events: {}
documentSelector:
path: metadata.name
value: pg-test-init-script
asserts:
- matchRegex:
path: stringData["init.sh"]
pattern: 'REVOKE "events_admin" FROM "app";'
- matchRegex:
path: stringData["init.sh"]
pattern: 'REVOKE "events_readonly" FROM "app";'
- it: "roles with only admin grants admin and revokes readonly"
set:
databases:
events:
roles:
admin:
- app
documentSelector:
path: metadata.name
value: pg-test-init-script
asserts:
- matchRegex:
path: stringData["init.sh"]
pattern: 'GRANT "events_admin" TO "app";'
- matchRegex:
path: stringData["init.sh"]
pattern: 'REVOKE "events_readonly" FROM "app";'
- it: "roles with only readonly grants readonly and revokes admin"
set:
databases:
events:
roles:
readonly:
- app
documentSelector:
path: metadata.name
value: pg-test-init-script
asserts:
- matchRegex:
path: stringData["init.sh"]
pattern: 'REVOKE "events_admin" FROM "app";'
- matchRegex:
path: stringData["init.sh"]
pattern: 'GRANT "events_readonly" TO "app";'