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
7 changes: 7 additions & 0 deletions api/v1alpha1/postgresrole_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ type PostgresRolePasswordFromSecret struct {
Key string `json:"key"`
}

// PostgresRoleOnDeleteSpec holds the options to change the operator's behavior when deleting a resource.
type PostgresRoleOnDeleteSpec struct {
ReassignOwnedTo string `json:"reassignOwnedTo,omitempty"`
}

// PostgresRoleSpec defines the desired state of PostgresRole.
type PostgresRoleSpec struct {
// PostgreSQL role name
Expand All @@ -50,6 +55,8 @@ type PostgresRoleSpec struct {
SecretTemplate map[string]string `json:"secretTemplate,omitempty"`

MemberOfRoles []string `json:"memberOfRoles,omitempty"`

OnDelete *PostgresRoleOnDeleteSpec `json:"onDelete,omitempty"`
}

// PostgresRoleStatus defines the observed state of PostgresRole.
Expand Down
20 changes: 20 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ spec:
x-kubernetes-validations:
- message: name is immutable
rule: self == oldSelf
onDelete:
description: PostgresRoleOnDeleteSpec holds the options to change
the operator's behavior when deleting a resource.
properties:
reassignOwnedTo:
type: string
type: object
passwordFromSecret:
properties:
key:
Expand Down
17 changes: 17 additions & 0 deletions docs/docs/how_to_guides/usage/configure_role_postgresrole.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,20 @@ spec:
```

In this example, we assign our role `myrole` to the role `admin-role`.

## Change the objects' ownership before deleting the role

You can configure the resource to change the ownership on the objects that the role owns by setting the option `onDelete.reassignOwnedTo`.

The value is the name of a role that must already exist in the PostgreSQL instance.

```yaml
apiVersion: managed-postgres-operator.hoppscale.com/v1alpha1
kind: PostgresRole
metadata:
name: myrole
spec:
name: myrole
onDelete:
reassignOwnedTo: myotherrole
```
11 changes: 10 additions & 1 deletion docs/docs/reference/api/v1alpha1/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ This resource aims to implement most of the PostgreSQL role's parameters: [https
| **`apiVersion`**<br />*string* | :material-check: | `managed-postgres-operator.hoppscale.com/v1alpha1` |
| **`kind`**<br />*string* | :material-check: | `PostgresRole` |
| **`metadata`**<br />*[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#objectmeta-v1-meta)* | :material-check: | Refer to Kubernetes API documentation for fields of metadata. |
| role **`spec`**<br />*[PostgresRoleSpec](#postgresrolespec)* | :material-check: | |
| **`spec`**<br />*[PostgresRoleSpec](#postgresrolespec)* | :material-check: | |
| **`status`**<br />*[PostgresRoleStatus](#postgresrolestatus)* | :material-minus: | |

### PostgresRoleSpec
Expand All @@ -83,6 +83,15 @@ PostgresRoleSpec holds the specification of a PostgreSQL role.
| **`secretName`**<br />*string* | :material-close: | Name of the Secret the operator should create, containing the role's log in information.<br />*Default: `""`* |
| **`secretTemplate`**<br />*map[string]string* | :material-close: | Dictionnary containing the key/value to configure in the Secret created by the operator (cf. `secretName`).<br />*Default: `{}`* |
| **`memberOfRoles`**<br />*[]string* | :material-close: | List of role's names of which the role should be member of.<br />*Default: `[]`* |
| **`onDelete`**<br />*[PostgresRoleOnDeleteSpec](#postgresroleondeletespec)* | :material-close: | Options to change the operator's default behavior on resource deletion.<br />*Default: `nil`* |

### PostgresRoleOnDeleteSpec

PostgresRoleOnDeleteSpec holds the options to change the operator's behavior when deleting a resource.

| Field | Required | Description |
|-------|----------|-------------|
| **`reassignOwnedTo`**<br />*string* | :material-close: | Reassign objects owned by the current role to another.<br />*Default: `""`* |

### PostgresRoleStatus

Expand Down
32 changes: 27 additions & 5 deletions internal/controller/postgresrole_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ func (r *PostgresRoleReconciler) Reconcile(ctx context.Context, req ctrl.Request
return r.Result(nil)
}

err = r.reconcileOnDeletion(existingRole, resource.Spec.KeepOnDelete)
err = r.reconcileOnDeletion(existingRole, resource.Spec.KeepOnDelete, resource.Spec.OnDelete)
if err != nil {
return r.Result(err)
}
Expand Down Expand Up @@ -217,16 +217,38 @@ func (r *PostgresRoleReconciler) Result(err error) (ctrl.Result, error) {
}

// reconcileOnDeletion performs all actions related to deleting the resource
func (r *PostgresRoleReconciler) reconcileOnDeletion(existingRole *postgresql.Role, keepOnDelete bool) (err error) {
func (r *PostgresRoleReconciler) reconcileOnDeletion(existingRole *postgresql.Role, keepOnDelete bool, onDeleteOptions *managedpostgresoperatorhoppscalecomv1alpha1.PostgresRoleOnDeleteSpec) (err error) {
if existingRole == nil {
r.logging.Info("Role doesn't exist, skipping DROP ROLE")
return
return nil
}

if keepOnDelete {
// If the resource is configured to keep the remote role on delete
r.logging.Info("keepOnDelete is true, skipping DROP ROLE")
return
return nil
}

if onDeleteOptions != nil {
if onDeleteOptions.ReassignOwnedTo != "" {
databases, err := postgresql.ListDatabases(r.PGPools.Default)
if err != nil {
return fmt.Errorf("failed to list databases: %s", err)
}

for _, database := range databases {
err := postgresql.EnsurePGPoolExists(r.PGPools, database)
if err != nil {
return fmt.Errorf("failed to open pg pool: %s", err)
}

err = postgresql.ReassignOwnedToRole(r.PGPools.Databases[database], existingRole.Name, onDeleteOptions.ReassignOwnedTo)
if err != nil {
return fmt.Errorf("failed to reassign owned objects in database before deletion: %s", err)
}
}
r.logging.Info(fmt.Sprintf("Objects owned by '%s' have been reassigned to '%s'", existingRole.Name, onDeleteOptions.ReassignOwnedTo))
}
}

err = postgresql.DropRole(r.PGPools.Default, existingRole.Name)
Expand All @@ -236,7 +258,7 @@ func (r *PostgresRoleReconciler) reconcileOnDeletion(existingRole *postgresql.Ro

r.logging.Info("Role has been deleted")

return
return nil
}

// reconcileOnCreation performs all actions related to creating the resource
Expand Down
65 changes: 64 additions & 1 deletion internal/controller/postgresrole_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,7 @@ var _ = Describe("PostgresRole Controller", func() {
When("no role exists", func() {
It("should return immediately", func() {
var existingRole *postgresql.Role
var onDeleteOptions *managedpostgresoperatorhoppscalecomv1alpha1.PostgresRoleOnDeleteSpec

controllerReconciler := &PostgresRoleReconciler{
Client: k8sClient,
Expand All @@ -875,7 +876,7 @@ var _ = Describe("PostgresRole Controller", func() {
CacheRolePasswords: map[string]string{},
}

err := controllerReconciler.reconcileOnDeletion(existingRole, false)
err := controllerReconciler.reconcileOnDeletion(existingRole, false, onDeleteOptions)
Expect(err).NotTo(HaveOccurred())
for _, poolMock := range pgpoolsMock {
if err := poolMock.ExpectationsWereMet(); err != nil {
Expand All @@ -884,6 +885,68 @@ var _ = Describe("PostgresRole Controller", func() {
}
})
})

When("reassignOwnedTo is configured", func() {
It("reassign owned objects before deletion", func() {
existingRole := &postgresql.Role{
Name: "myrole",
}
onDeleteOptions := &managedpostgresoperatorhoppscalecomv1alpha1.PostgresRoleOnDeleteSpec{
ReassignOwnedTo: "myrolebis",
}

databases := []string{
"postgres",
"foo",
}

for _, database := range databases {
mock, err := pgxmock.NewPool()
if err != nil {
Fail(err.Error())
}
pgpoolsMock[database] = mock
pgpools.Databases[database] = pgpoolsMock[database]
}

pgpoolsMock["default"].ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta("SELECT datname FROM pg_database WHERE datistemplate = false"))).
WillReturnRows(
pgxmock.NewRows([]string{
"datname",
}).
AddRow(
"postgres",
).
AddRow(
"foo",
),
)

for _, database := range databases {
pgpoolsMock[database].ExpectExec(fmt.Sprintf("^%s$", regexp.QuoteMeta(`REASSIGN OWNED BY "myrole" TO "myrolebis"`))).
WillReturnResult(pgxmock.NewResult("REASSIGN OWNED", 1))
}

pgpoolsMock["default"].ExpectExec(fmt.Sprintf("^%s$", regexp.QuoteMeta(`DROP ROLE "myrole"`))).
WillReturnResult(pgxmock.NewResult("DROP ROLE", 1))

controllerReconciler := &PostgresRoleReconciler{
Client: k8sClient,
Scheme: k8sClient.Scheme(),
PGPools: pgpools,
CacheRolePasswords: map[string]string{},
}

err := controllerReconciler.reconcileOnDeletion(existingRole, false, onDeleteOptions)
Expect(err).NotTo(HaveOccurred())
for _, poolMock := range pgpoolsMock {
if err := poolMock.ExpectationsWereMet(); err != nil {
Fail(err.Error())
}
}
})
})

})
})
})
17 changes: 17 additions & 0 deletions internal/postgresql/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,20 @@ func RevokeDatabaseRolePrivilege(pgpool PGPoolInterface, database, role, privile

return
}

func ListDatabases(pgpool PGPoolInterface) (databases []string, err error) {
rows, err := pgpool.Query(context.Background(), "SELECT datname FROM pg_database WHERE datistemplate = false")
if err != nil {
err = fmt.Errorf("pg query failed: %s", err)
return
}
defer rows.Close()

databases, err = pgx.CollectRows(rows, pgx.RowTo[string])
if err != nil {
err = fmt.Errorf("failed to collect rows: %s", err)
return
}

return
}
39 changes: 39 additions & 0 deletions internal/postgresql/database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -481,4 +481,43 @@ var _ = Describe("PostgreSQL Database", func() {

})

Context("Calling ListDatabases", func() {
It("should return a list of databases without templates", func() {
pgpoolMock.ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(`SELECT datname FROM pg_database WHERE datistemplate = false`))).
WillReturnRows(
pgxmock.NewRows([]string{
"datname",
}).
AddRow(
"foo",
).
AddRow(
"postgres",
),
)

databases, err := ListDatabases(pgpool)

Expect(err).NotTo(HaveOccurred())
Expect(databases).To(Equal([]string{"foo", "postgres"}))
if err := pgpoolMock.ExpectationsWereMet(); err != nil {
Fail(err.Error())
}
})

It("should return an error if the PostgreSQL request failed", func() {
pgpoolMock.ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(`SELECT datname FROM pg_database WHERE datistemplate = false`))).
WillReturnError(fmt.Errorf("fake error from PostgreSQL"))

databases, err := ListDatabases(pgpool)

Expect(err).To(HaveOccurred())
Expect(databases).To(BeEmpty())
if err := pgpoolMock.ExpectationsWereMet(); err != nil {
Fail(err.Error())
}
})

})

})
12 changes: 12 additions & 0 deletions internal/postgresql/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,15 @@ func AlterRole(pgpool PGPoolInterface, operatorRole, existingRole, desiredRole *
}
return
}

func ReassignOwnedToRole(pgpool PGPoolInterface, oldRole, newRole string) (err error) {
sanitizedOldRoleName := pgx.Identifier{oldRole}.Sanitize()
sanitizedNewRoleName := pgx.Identifier{newRole}.Sanitize()

_, err = pgpool.Exec(context.Background(), fmt.Sprintf("REASSIGN OWNED BY %s TO %s", sanitizedOldRoleName, sanitizedNewRoleName))
if err != nil {
err = fmt.Errorf("pg exec failed: %s", err)
return
}
return
}
26 changes: 26 additions & 0 deletions internal/postgresql/role_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -422,4 +422,30 @@ var _ = Describe("PostgreSQL Role", func() {
})
})
})

Context("Calling ReassignOwnedToRole", func() {
It("should reassign owned objects to a new role", func() {
pgpoolMock.ExpectExec(fmt.Sprintf("^%s$", regexp.QuoteMeta(`REASSIGN OWNED BY "foo" TO "bar"`))).
WillReturnResult(pgxmock.NewResult("bar", 1))

err := ReassignOwnedToRole(pgpool, "foo", "bar")

Expect(err).NotTo(HaveOccurred())
if err := pgpoolMock.ExpectationsWereMet(); err != nil {
Fail(err.Error())
}
})
It("should return an error if the PostgreSQL request failed", func() {
pgpoolMock.ExpectExec(fmt.Sprintf("^%s$", regexp.QuoteMeta(`REASSIGN OWNED BY "foo" TO "bar"`))).
WillReturnError(fmt.Errorf("fake error from PostgreSQL"))

err := ReassignOwnedToRole(pgpool, "foo", "bar")

Expect(err).To(HaveOccurred())
if err := pgpoolMock.ExpectationsWereMet(); err != nil {
Fail(err.Error())
}
})
})

})
Loading