Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
helm install postgres bitnami/postgresql \
--namespace postgres \
--version 18.5.14 \
--set auth.postgresPassword=devpassword \
-f postgres-dev-values.yaml \
--wait

- name: Deploy operator
Expand Down
15 changes: 14 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,23 @@ on:
- src/**
- Dockerfile

env:
CHART_FILE: charts/postgres-db-admin-operator/Chart.yaml

jobs:
release:
runs-on: ubuntu-latest
permissions:
packages: write
contents: write
steps:
- uses: actions/checkout@v4

- name: Get chart version
id: chart
run: |
echo "version=$(grep '^version:' charts/postgres-db-admin-operator/Chart.yaml | awk '{print $2}')" >> $GITHUB_OUTPUT
echo "version=$(grep '^version:' ${{ env.CHART_FILE }} | awk '{print $2}')" >> $GITHUB_OUTPUT
echo "chart_changed=$( git diff --name-only HEAD~1 HEAD | grep -q '${{ env.CHART_FILE }}' && echo true || echo false)" >> $GITHUB_OUTPUT

- name: Log in to GHCR
uses: docker/login-action@v3
Expand All @@ -47,3 +52,11 @@ jobs:
registry: ghcr.io
registry_username: ${{ github.actor }}
registry_password: ${{ secrets.GITHUB_TOKEN }}

- name: Create GitHub release
if: steps.chart.outputs.chart_changed == 'true'
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ steps.chart.outputs.version }}
name: v${{ steps.chart.outputs.version }}
generate_release_notes: true
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ init:
helm install $(POSTGRES_RELEASE) bitnami/postgresql \
--namespace $(POSTGRES_NAMESPACE) \
--version $(POSTGRES_CHART_VERSION) \
--set auth.postgresPassword=$(POSTGRES_PASSWORD) \
-f postgres-dev-values.yaml \
--wait

deploy:
Expand All @@ -27,6 +27,9 @@ deploy:
--from-literal=password=$(POSTGRES_PASSWORD) \
--dry-run=client -o yaml | kubectl apply -f -
helm upgrade --install postgres-db-admin-operator ./charts/postgres-db-admin-operator \
--set image.repository=postgres-db-admin-operator \
--set image.tag=dev \
--set image.pullPolicy=Never \
--set postgres.host=$(POSTGRES_RELEASE)-postgresql.$(POSTGRES_NAMESPACE).svc.cluster.local \
--set postgres.user=postgres \
--set postgres.password.existingSecret=$(SECRET_NAME)
Expand Down
4 changes: 2 additions & 2 deletions charts/postgres-db-admin-operator/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ apiVersion: v2
name: postgres-db-admin-operator
description: Kubernetes operator for managing PostgreSQL databases
type: application
version: 0.0.6
appVersion: 0.0.6
version: 0.0.7
appVersion: 0.0.7
15 changes: 15 additions & 0 deletions postgres-dev-values.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
auth:
postgresPassword: devpassword

architecture: standalone

primary:
persistence:
enabled: false
livenessProbe:
initialDelaySeconds: 5
pdb:
create: false

global:
defaultFips: "off"
5 changes: 4 additions & 1 deletion scripts/integration_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@
- Postgres reachable via port-forward on localhost:5432
"""

import random
import string
import time
import psycopg
from kubernetes import client, config
from kubernetes.client.rest import ApiException

DB_NAME = "integration-test"
_SUFFIX = "".join(random.choices(string.ascii_lowercase, k=5))
DB_NAME = f"integration-test-{_SUFFIX}"
SECRET_NAME = f"{DB_NAME}-credentials"
NAMESPACE = "default"
PG_HOST = "localhost"
Expand Down
75 changes: 75 additions & 0 deletions tests/test_roles.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import pytest
import psycopg
import psycopg.errors
from psycopg import sql

from postgres_db_admin_operator.db import (
create_database,
drop_database,
create_admin_role,
setup_admin_role_privileges,
create_readonly_role,
Expand Down Expand Up @@ -118,3 +121,75 @@ def test_drop_role(conn, role_db):

def test_drop_role_idempotent(conn):
drop_role(conn, "definitely-does-not-exist-role")


@pytest.fixture
def two_isolated_dbs(conn):
"""Creates two fully-provisioned databases (admin + readonly roles each), cleans up on teardown."""
db_a = "test-isolation-db-a"
db_b = "test-isolation-db-b"

for db in [db_a, db_b]:
drop_database(conn, db)
create_database(conn, db)
create_admin_role(conn, db, ROLE_PASSWORD)
create_readonly_role(conn, db, ROLE_PASSWORD)
with psycopg.connect(
host="localhost",
port=POSTGRES_PORT,
user="postgres",
password=POSTGRES_PASSWORD,
dbname=db,
autocommit=True,
) as db_conn:
setup_admin_role_privileges(db_conn, db)
setup_readonly_role_privileges(db_conn, db)
# Seed table created by postgres so default privilege grants apply to both roles
db_conn.execute("CREATE TABLE seed (id int)")
db_conn.execute("INSERT INTO seed VALUES (1)")

yield db_a, db_b

for db in [db_a, db_b]:
for role in [f"{db}_admin", f"{db}_readonly"]:
if conn.execute("SELECT 1 FROM pg_roles WHERE rolname = %s", (role,)).fetchone():
with psycopg.connect(
host="localhost",
port=POSTGRES_PORT,
user="postgres",
password=POSTGRES_PASSWORD,
dbname=db,
autocommit=True,
) as db_conn:
db_conn.execute(sql.SQL("DROP OWNED BY {}").format(sql.Identifier(role)))
conn.execute(sql.SQL("DROP ROLE {}").format(sql.Identifier(role)))
drop_database(conn, db)


def test_roles_cannot_access_other_database(two_isolated_dbs):
"""Admin and readonly roles of one database must not have privileges in another database."""
db_a, db_b = two_isolated_dbs

for own_db, other_db in [(db_a, db_b), (db_b, db_a)]:
# Admin can insert into its own database
with connect_as(f"{own_db}_admin", own_db) as conn:
conn.execute("INSERT INTO seed VALUES (2)")
result = conn.execute("SELECT COUNT(*) FROM seed").fetchone()
assert result == (2,)

# Readonly can select from its own database
with connect_as(f"{own_db}_readonly", own_db) as conn:
result = conn.execute("SELECT * FROM seed").fetchall()
assert result == [(1,), (2,)]

# Admin cannot create a table in the other database
with connect_as(f"{own_db}_admin", other_db) as conn:
with pytest.raises(psycopg.errors.InsufficientPrivilege):
conn.execute("CREATE TABLE should_not_exist (id int)")

# Readonly has no table grants in the other database
with connect_as(f"{own_db}_readonly", other_db) as conn:
result = conn.execute(
"SELECT COUNT(*) FROM information_schema.role_table_grants WHERE grantee = current_user"
).fetchone()
assert result == (0,)
Loading