Skip to content

Commit 3c367df

Browse files
committed
feat(postgresrole): support REASSIGN OWNED BY on deletion
1 parent 95812b0 commit 3c367df

File tree

11 files changed

+273
-7
lines changed

11 files changed

+273
-7
lines changed

api/v1alpha1/postgresrole_types.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ type PostgresRolePasswordFromSecret struct {
2727
Key string `json:"key"`
2828
}
2929

30+
// PostgresRoleOnDeleteSpec holds the options to change the operator's behavior when deleting a resource.
31+
type PostgresRoleOnDeleteSpec struct {
32+
ReassignOwnedTo string `json:"reassignOwnedTo,omitempty"`
33+
}
34+
3035
// PostgresRoleSpec defines the desired state of PostgresRole.
3136
type PostgresRoleSpec struct {
3237
// PostgreSQL role name
@@ -50,6 +55,8 @@ type PostgresRoleSpec struct {
5055
SecretTemplate map[string]string `json:"secretTemplate,omitempty"`
5156

5257
MemberOfRoles []string `json:"memberOfRoles,omitempty"`
58+
59+
OnDelete *PostgresRoleOnDeleteSpec `json:"onDelete,omitempty"`
5360
}
5461

5562
// PostgresRoleStatus defines the observed state of PostgresRole.

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

deploy/crds/managed-postgres-operator.hoppscale.com_postgresroles.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,13 @@ spec:
6363
x-kubernetes-validations:
6464
- message: name is immutable
6565
rule: self == oldSelf
66+
onDelete:
67+
description: PostgresRoleOnDeleteSpec holds the options to change
68+
the operator's behavior when deleting a resource.
69+
properties:
70+
reassignOwnedTo:
71+
type: string
72+
type: object
6673
passwordFromSecret:
6774
properties:
6875
key:

docs/docs/how_to_guides/usage/configure_role_postgresrole.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,3 +219,20 @@ spec:
219219
```
220220

221221
In this example, we assign our role `myrole` to the role `admin-role`.
222+
223+
## Change the objects' ownership before deleting the role
224+
225+
You can configure the resource to change the ownership on the objects that the role owns by setting the option `onDelete.reassignOwnedTo`.
226+
227+
The value is the name of a role that must already exist in the PostgreSQL instance.
228+
229+
```yaml
230+
apiVersion: managed-postgres-operator.hoppscale.com/v1alpha1
231+
kind: PostgresRole
232+
metadata:
233+
name: myrole
234+
spec:
235+
name: myrole
236+
onDelete:
237+
reassignOwnedTo: myotherrole
238+
```

docs/docs/reference/api/v1alpha1/index.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ This resource aims to implement most of the PostgreSQL role's parameters: [https
6161
| **`apiVersion`**<br />*string* | :material-check: | `managed-postgres-operator.hoppscale.com/v1alpha1` |
6262
| **`kind`**<br />*string* | :material-check: | `PostgresRole` |
6363
| **`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. |
64-
| role **`spec`**<br />*[PostgresRoleSpec](#postgresrolespec)* | :material-check: | |
64+
| **`spec`**<br />*[PostgresRoleSpec](#postgresrolespec)* | :material-check: | |
6565
| **`status`**<br />*[PostgresRoleStatus](#postgresrolestatus)* | :material-minus: | |
6666

6767
### PostgresRoleSpec
@@ -83,6 +83,15 @@ PostgresRoleSpec holds the specification of a PostgreSQL role.
8383
| **`secretName`**<br />*string* | :material-close: | Name of the Secret the operator should create, containing the role's log in information.<br />*Default: `""`* |
8484
| **`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: `{}`* |
8585
| **`memberOfRoles`**<br />*[]string* | :material-close: | List of role's names of which the role should be member of.<br />*Default: `[]`* |
86+
| **`onDelete`**<br />*[PostgresRoleOnDeleteSpec](#postgresroleondeletespec)* | :material-close: | Options to change the operator's default behavior on resource deletion.<br />*Default: `nil`* |
87+
88+
### PostgresRoleOnDeleteSpec
89+
90+
PostgresRoleOnDeleteSpec holds the options to change the operator's behavior when deleting a resource.
91+
92+
| Field | Required | Description |
93+
|-------|----------|-------------|
94+
| **`reassignOwnedTo`**<br />*string* | :material-close: | Reassign objects owned by the current role to another.<br />*Default: `""`* |
8695

8796
### PostgresRoleStatus
8897

internal/controller/postgresrole_controller.go

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ func (r *PostgresRoleReconciler) Reconcile(ctx context.Context, req ctrl.Request
144144
return r.Result(nil)
145145
}
146146

147-
err = r.reconcileOnDeletion(existingRole, resource.Spec.KeepOnDelete)
147+
err = r.reconcileOnDeletion(existingRole, resource.Spec.KeepOnDelete, resource.Spec.OnDelete)
148148
if err != nil {
149149
return r.Result(err)
150150
}
@@ -217,16 +217,38 @@ func (r *PostgresRoleReconciler) Result(err error) (ctrl.Result, error) {
217217
}
218218

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

226226
if keepOnDelete {
227227
// If the resource is configured to keep the remote role on delete
228228
r.logging.Info("keepOnDelete is true, skipping DROP ROLE")
229-
return
229+
return nil
230+
}
231+
232+
if onDeleteOptions != nil {
233+
if onDeleteOptions.ReassignOwnedTo != "" {
234+
databases, err := postgresql.ListDatabases(r.PGPools.Default)
235+
if err != nil {
236+
return fmt.Errorf("failed to list databases: %s", err)
237+
}
238+
239+
for _, database := range databases {
240+
err := postgresql.EnsurePGPoolExists(r.PGPools, database)
241+
if err != nil {
242+
return fmt.Errorf("failed to open pg pool: %s", err)
243+
}
244+
245+
err = postgresql.ReassignOwnedToRole(r.PGPools.Databases[database], existingRole.Name, onDeleteOptions.ReassignOwnedTo)
246+
if err != nil {
247+
return fmt.Errorf("failed to reassign owned objects in database before deletion: %s", err)
248+
}
249+
}
250+
r.logging.Info(fmt.Sprintf("Objects owned by '%s' have been reassigned to '%s'", existingRole.Name, onDeleteOptions.ReassignOwnedTo))
251+
}
230252
}
231253

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

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

239-
return
261+
return nil
240262
}
241263

242264
// reconcileOnCreation performs all actions related to creating the resource

internal/controller/postgresrole_controller_test.go

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -867,6 +867,7 @@ var _ = Describe("PostgresRole Controller", func() {
867867
When("no role exists", func() {
868868
It("should return immediately", func() {
869869
var existingRole *postgresql.Role
870+
var onDeleteOptions *managedpostgresoperatorhoppscalecomv1alpha1.PostgresRoleOnDeleteSpec
870871

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

878-
err := controllerReconciler.reconcileOnDeletion(existingRole, false)
879+
err := controllerReconciler.reconcileOnDeletion(existingRole, false, onDeleteOptions)
879880
Expect(err).NotTo(HaveOccurred())
880881
for _, poolMock := range pgpoolsMock {
881882
if err := poolMock.ExpectationsWereMet(); err != nil {
@@ -884,6 +885,95 @@ var _ = Describe("PostgresRole Controller", func() {
884885
}
885886
})
886887
})
888+
889+
When("reassignOwnedTo is configured", func() {
890+
It("reassign owned objects before deletion", func() {
891+
existingRole := &postgresql.Role{
892+
Name: "myrole",
893+
}
894+
onDeleteOptions := &managedpostgresoperatorhoppscalecomv1alpha1.PostgresRoleOnDeleteSpec{
895+
ReassignOwnedTo: "myrolebis",
896+
}
897+
898+
databases := []string{
899+
"postgres",
900+
"foo",
901+
}
902+
903+
for _, database := range databases {
904+
mock, err := pgxmock.NewPool()
905+
if err != nil {
906+
Fail(err.Error())
907+
}
908+
pgpoolsMock[database] = mock
909+
pgpools.Databases[database] = pgpoolsMock[database]
910+
}
911+
912+
/*
913+
pgpoolsMock["default"].ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(postgresql.GetRoleSQLStatement))).
914+
WithArgs("myrole").
915+
WillReturnRows(
916+
pgxmock.NewRows([]string{
917+
"rolname",
918+
"rolsuper",
919+
"rolinherit",
920+
"rolcreaterole",
921+
"rolcreatedb",
922+
"rolcanlogin",
923+
"rolreplication",
924+
"rolbypassrls",
925+
}).
926+
AddRow(
927+
"myrole",
928+
false,
929+
false,
930+
true,
931+
true,
932+
false,
933+
false,
934+
false,
935+
),
936+
)
937+
*/
938+
939+
pgpoolsMock["default"].ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta("SELECT datname FROM pg_database WHERE datistemplate = false"))).
940+
WillReturnRows(
941+
pgxmock.NewRows([]string{
942+
"datname",
943+
}).
944+
AddRow(
945+
"postgres",
946+
).
947+
AddRow(
948+
"foo",
949+
),
950+
)
951+
952+
for _, database := range databases {
953+
pgpoolsMock[database].ExpectExec(fmt.Sprintf("^%s$", regexp.QuoteMeta(`REASSIGN OWNED BY "myrole" TO "myrolebis"`))).
954+
WillReturnResult(pgxmock.NewResult("REASSIGN OWNED", 1))
955+
}
956+
957+
pgpoolsMock["default"].ExpectExec(fmt.Sprintf("^%s$", regexp.QuoteMeta(`DROP ROLE "myrole"`))).
958+
WillReturnResult(pgxmock.NewResult("DROP ROLE", 1))
959+
960+
controllerReconciler := &PostgresRoleReconciler{
961+
Client: k8sClient,
962+
Scheme: k8sClient.Scheme(),
963+
PGPools: pgpools,
964+
CacheRolePasswords: map[string]string{},
965+
}
966+
967+
err := controllerReconciler.reconcileOnDeletion(existingRole, false, onDeleteOptions)
968+
Expect(err).NotTo(HaveOccurred())
969+
for _, poolMock := range pgpoolsMock {
970+
if err := poolMock.ExpectationsWereMet(); err != nil {
971+
Fail(err.Error())
972+
}
973+
}
974+
})
975+
})
976+
887977
})
888978
})
889979
})

internal/postgresql/database.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,20 @@ func RevokeDatabaseRolePrivilege(pgpool PGPoolInterface, database, role, privile
171171

172172
return
173173
}
174+
175+
func ListDatabases(pgpool PGPoolInterface) (databases []string, err error) {
176+
rows, err := pgpool.Query(context.Background(), "SELECT datname FROM pg_database WHERE datistemplate = false")
177+
if err != nil {
178+
err = fmt.Errorf("pg query failed: %s", err)
179+
return
180+
}
181+
defer rows.Close()
182+
183+
databases, err = pgx.CollectRows(rows, pgx.RowTo[string])
184+
if err != nil {
185+
err = fmt.Errorf("failed to collect rows: %s", err)
186+
return
187+
}
188+
189+
return
190+
}

internal/postgresql/database_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,4 +481,43 @@ var _ = Describe("PostgreSQL Database", func() {
481481

482482
})
483483

484+
Context("Calling ListDatabases", func() {
485+
It("should return a list of databases without templates", func() {
486+
pgpoolMock.ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(`SELECT datname FROM pg_database WHERE datistemplate = false`))).
487+
WillReturnRows(
488+
pgxmock.NewRows([]string{
489+
"datname",
490+
}).
491+
AddRow(
492+
"foo",
493+
).
494+
AddRow(
495+
"postgres",
496+
),
497+
)
498+
499+
databases, err := ListDatabases(pgpool)
500+
501+
Expect(err).NotTo(HaveOccurred())
502+
Expect(databases).To(Equal([]string{"foo", "postgres"}))
503+
if err := pgpoolMock.ExpectationsWereMet(); err != nil {
504+
Fail(err.Error())
505+
}
506+
})
507+
508+
It("should return an error if the PostgreSQL request failed", func() {
509+
pgpoolMock.ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(`SELECT datname FROM pg_database WHERE datistemplate = false`))).
510+
WillReturnError(fmt.Errorf("fake error from PostgreSQL"))
511+
512+
databases, err := ListDatabases(pgpool)
513+
514+
Expect(err).To(HaveOccurred())
515+
Expect(databases).To(BeEmpty())
516+
if err := pgpoolMock.ExpectationsWereMet(); err != nil {
517+
Fail(err.Error())
518+
}
519+
})
520+
521+
})
522+
484523
})

internal/postgresql/role.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,3 +195,15 @@ func AlterRole(pgpool PGPoolInterface, operatorRole, existingRole, desiredRole *
195195
}
196196
return
197197
}
198+
199+
func ReassignOwnedToRole(pgpool PGPoolInterface, oldRole, newRole string) (err error) {
200+
sanitizedOldRoleName := pgx.Identifier{oldRole}.Sanitize()
201+
sanitizedNewRoleName := pgx.Identifier{newRole}.Sanitize()
202+
203+
_, err = pgpool.Exec(context.Background(), fmt.Sprintf("REASSIGN OWNED BY %s TO %s", sanitizedOldRoleName, sanitizedNewRoleName))
204+
if err != nil {
205+
err = fmt.Errorf("pg exec failed: %s", err)
206+
return
207+
}
208+
return
209+
}

0 commit comments

Comments
 (0)