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
22 changes: 14 additions & 8 deletions internal/controller/postgresqldatabase_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ func (r *PostgreSQLDatabaseReconciler) reconcile(ctx context.Context, reqLogger
status.host = host
reqLogger = reqLogger.WithValues("host", host)

if err := r.runPreflight(reqLogger, host, *adminCredentials); err != nil {
if err := r.prepareHost(reqLogger, host, *adminCredentials); err != nil {
return status, err
}

Expand Down Expand Up @@ -290,11 +290,10 @@ func (r *PostgreSQLDatabaseReconciler) EnsurePostgreSQLDatabase(ctx context.Cont
return nil
}

// runPreflight opens an admin connection to the host and verifies controller
// invariants (superuser role membership) before any reconcile work. The
// connection is closed before returning - the downstream postgres.Database
// call opens its own.
func (r *PostgreSQLDatabaseReconciler) runPreflight(log logr.Logger, host string, admin postgres.Credentials) error {
// prepareHost opens an admin connection to the host, runs preflight checks,
// and ensures the management role exists. The connection is closed before
// returning - the downstream postgres.Database call opens its own.
func (r *PostgreSQLDatabaseReconciler) prepareHost(log logr.Logger, host string, admin postgres.Credentials) error {
db, err := postgres.Connect(postgres.ConnectionString{
Host: host,
Database: "postgres",
Expand All @@ -303,10 +302,17 @@ func (r *PostgreSQLDatabaseReconciler) runPreflight(log logr.Logger, host string
Params: admin.Params,
})
if err != nil {
return fmt.Errorf("preflight: connect to host %s: %w", host, err)
return fmt.Errorf("prepare host %s: connect: %w", host, err)
}
defer db.Close()
return postgres.Preflight(log, db, r.SuperuserRoleName)

if err := postgres.Preflight(log, db, r.SuperuserRoleName); err != nil {
return err
}
if err := postgres.EnsureManagerRole(log, db, r.ManagerRoleName); err != nil {
return fmt.Errorf("prepare host %s: ensure management role: %w", host, err)
}
return nil
}

type adminCredentialsParams struct {
Expand Down
23 changes: 23 additions & 0 deletions pkg/postgres/manager_role.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package postgres

import (
"database/sql"
"fmt"

"github.com/go-logr/logr"
"github.com/lib/pq"
)

// EnsureManagerRole creates the management role on db if it does not
// already exist. The connecting user must have privileges to create roles.
// Idempotent: a duplicate_object error is treated as a no-op.
func EnsureManagerRole(log logr.Logger, db *sql.DB, role string) error {
if role == "" {
return fmt.Errorf("ensure manager role: role name is empty")
}
return tryExec(log, db, tryExecReq{
objectType: "management role",
errorCode: "duplicate_object",
query: fmt.Sprintf("CREATE ROLE %s", pq.QuoteIdentifier(role)),
Comment thread
tmablunar marked this conversation as resolved.
})
}
40 changes: 40 additions & 0 deletions pkg/postgres/manager_role_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package postgres_test

import (
"fmt"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.lunarway.com/postgresql-controller/pkg/postgres"
"go.lunarway.com/postgresql-controller/test"
)

func TestEnsureManagerRole_createsAndIsIdempotent(t *testing.T) {
host := test.Integration(t)
log := test.SetLogger(t)

db := preflightAdminConn(t, host)
defer db.Close()

role := fmt.Sprintf("ensure_manager_%d", time.Now().UnixNano())
defer func() {
_, _ = db.Exec(fmt.Sprintf("DROP ROLE %s", role))
}()

require.NoError(t, postgres.EnsureManagerRole(log, db, role), "first call should create the role")

var exists bool
require.NoError(t, db.QueryRow(`SELECT EXISTS(SELECT 1 FROM pg_roles WHERE rolname = $1)`, role).Scan(&exists))
assert.True(t, exists, "role should exist after EnsureManagerRole")

assert.NoError(t, postgres.EnsureManagerRole(log, db, role), "second call should be a no-op")
}

func TestEnsureManagerRole_emptyName(t *testing.T) {
log := test.SetLogger(t)
err := postgres.EnsureManagerRole(log, nil, "")
require.Error(t, err)
assert.Contains(t, err.Error(), "role name is empty")
}
Loading