Skip to content

Commit 2e69b77

Browse files
Merge pull request #12 from Facets-cloud/support-username-override
support user name override in role crd
2 parents 7707639 + cc14db8 commit 2e69b77

File tree

7 files changed

+109
-38
lines changed

7 files changed

+109
-38
lines changed

apis/postgresql/v1alpha1/role_types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ type RoleSpec struct {
7474
// +kubebuilder:validation:Required
7575
PasswordSecretRef common.SecretKeySelector `json:"passwordSecretRef,omitempty"`
7676

77+
// UserNameOverride allows specifying a custom username for the PostgreSQL role.
78+
// When set, this takes precedence over the role name from the CRD metadata.
79+
// +optional
80+
UserNameOverride *string `json:"userNameOverride,omitempty"`
81+
7782
// ConnectionLimit to be applied to the role.
7883
// +kubebuilder:validation:Min=-1
7984
// +optional

apis/postgresql/v1alpha1/zz_generated.deepcopy.go

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

chart/postgresql-operator/templates/role-crd.yaml

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
---
12
apiVersion: apiextensions.k8s.io/v1
23
kind: CustomResourceDefinition
34
metadata:
@@ -54,8 +55,8 @@ spec:
5455
description: RoleSpec defines the desired state of Role
5556
properties:
5657
connectSecretRef:
57-
description: ConnectSecretRef references the secret that contains database
58-
details () used to create this role.
58+
description: ConnectSecretRef references the secret that contains
59+
database details () used to create this role.
5960
properties:
6061
name:
6162
description: Name of the resource.
@@ -73,8 +74,8 @@ spec:
7374
format: int32
7475
type: integer
7576
passwordSecretRef:
76-
description: PasswordSecretRef references the secret that contains the
77-
password used for this role.
77+
description: PasswordSecretRef references the secret that contains
78+
the password used for this role.
7879
properties:
7980
key:
8081
description: The key to select.
@@ -100,13 +101,13 @@ spec:
100101
type: boolean
101102
createDb:
102103
default: false
103-
description: CreateDb grants CREATEDB when true, allowing the role
104-
to create databases.
104+
description: CreateDb grants CREATEDB when true, allowing the
105+
role to create databases.
105106
type: boolean
106107
createRole:
107108
default: false
108-
description: CreateRole grants CREATEROLE when true, allowing this
109-
role to create other roles.
109+
description: CreateRole grants CREATEROLE when true, allowing
110+
this role to create other roles.
110111
type: boolean
111112
inherit:
112113
default: false
@@ -128,6 +129,11 @@ spec:
128129
description: SuperUser grants SUPERUSER privilege when true.
129130
type: boolean
130131
type: object
132+
userNameOverride:
133+
description: UserNameOverride allows specifying a custom username
134+
for the PostgreSQL role. When set, this takes precedence over the
135+
role name from the CRD metadata.
136+
type: string
131137
type: object
132138
status:
133139
description: RoleStatus defines the observed state of Role
@@ -137,8 +143,8 @@ spec:
137143
description: "Condition contains details for one aspect of the current
138144
state of this API Resource. --- This struct is intended for direct
139145
use as an array at the field path .status.conditions. For example,
140-
\n type FooStatus struct{ // Represents the observations of a foo's
141-
current state. // Known .status.conditions.type are: \"Available\",
146+
\n type FooStatus struct{ // Represents the observations of a
147+
foo's current state. // Known .status.conditions.type are: \"Available\",
142148
\"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
143149
// +listType=map // +listMapKey=type Conditions []metav1.Condition
144150
`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
@@ -152,8 +158,8 @@ spec:
152158
format: date-time
153159
type: string
154160
message:
155-
description: message is a human readable message indicating details
156-
about the transition. This may be an empty string.
161+
description: message is a human readable message indicating
162+
details about the transition. This may be an empty string.
157163
maxLength: 32768
158164
type: string
159165
observedGeneration:
@@ -167,11 +173,11 @@ spec:
167173
type: integer
168174
reason:
169175
description: reason contains a programmatic identifier indicating
170-
the reason for the condition's last transition. Producers of
171-
specific condition types may define expected values and meanings
172-
for this field, and whether the values are considered a guaranteed
173-
API. The value should be a CamelCase string. This field may
174-
not be empty.
176+
the reason for the condition's last transition. Producers
177+
of specific condition types may define expected values and
178+
meanings for this field, and whether the values are considered
179+
a guaranteed API. The value should be a CamelCase string.
180+
This field may not be empty.
175181
maxLength: 1024
176182
minLength: 1
177183
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$

config/crd/bases/postgresql.facets.cloud_roles.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,11 @@ spec:
128128
description: SuperUser grants SUPERUSER privilege when true.
129129
type: boolean
130130
type: object
131+
userNameOverride:
132+
description: UserNameOverride allows specifying a custom username
133+
for the PostgreSQL role. When set, this takes precedence over the
134+
role name from the CRD metadata.
135+
type: string
131136
type: object
132137
status:
133138
description: RoleStatus defines the observed state of Role
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
apiVersion: postgresql.facets.cloud/v1alpha1
2+
kind: Role
3+
metadata:
4+
labels:
5+
app.kubernetes.io/name: role
6+
app.kubernetes.io/instance: role-example-with-override
7+
app.kubernetes.io/part-of: postgresql-operator
8+
app.kubernetes.io/managed-by: kustomize
9+
app.kubernetes.io/created-by: postgresql-operator
10+
name: role-example-with-override
11+
spec:
12+
connectSecretRef:
13+
name: db-conn
14+
namespace: default
15+
passwordSecretRef:
16+
namespace: default
17+
name: db-conn
18+
key: role_password
19+
userNameOverride: "test_user"
20+
connectionLimit: 100
21+
privileges:
22+
bypassRls: false
23+
createDb: false
24+
createRole: false
25+
inherit: false
26+
login: true
27+
replication: false
28+
superUser: false

controllers/postgresql/grantstatement_controller.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ func (r *GrantStatementReconciler) Reconcile(ctx context.Context, req ctrl.Reque
205205
}
206206

207207
// Revoke all privileges from the role before granting new privileges
208-
err, message = r.revokeAllPrivileges(grantStatement, prevDatabase, prevRoleRef, secret)
208+
err, message = r.revokeAllPrivileges(ctx, grantStatement, prevDatabase, prevRoleRef, secret)
209209
if err != nil {
210210
message := fmt.Sprintf("Failed to revoke all privileges for GrantStatement %s/%s: %s", grantStatement.Namespace, grantStatement.Name, message)
211211
r.appendGrantStatementStatusCondition(ctx, grantStatement, common.FAIL, metav1.ConditionFalse, common.FAIL, fmt.Sprintf("%s: %s", message, err.Error()))
@@ -257,7 +257,7 @@ func (r *GrantStatementReconciler) finalizeGrantStatement(ctx context.Context, g
257257
logger.Error(err, message)
258258
return fmt.Errorf("failed to retrieve secret from current state for GrantStatement %s/%s: %s", grantStatement.Namespace, grantStatement.Name, err.Error())
259259
}
260-
err, message = r.revokeAllPrivileges(grantStatement, database, roleRef, secret)
260+
err, message = r.revokeAllPrivileges(ctx, grantStatement, database, roleRef, secret)
261261
if err != nil {
262262
r.appendGrantStatementStatusCondition(ctx, grantStatement, common.FAIL, metav1.ConditionFalse, common.FAIL, err.Error())
263263
logger.Error(err, message)
@@ -268,7 +268,7 @@ func (r *GrantStatementReconciler) finalizeGrantStatement(ctx context.Context, g
268268
return nil
269269
}
270270

271-
func (r *GrantStatementReconciler) revokeAllPrivileges(grantStatement *postgresqlv1alpha1.GrantStatement, database string, roleRef common.ResourceReference, secret *corev1.Secret) (error, string) {
271+
func (r *GrantStatementReconciler) revokeAllPrivileges(ctx context.Context, grantStatement *postgresqlv1alpha1.GrantStatement, database string, roleRef common.ResourceReference, secret *corev1.Secret) (error, string) {
272272
var message string
273273
db, err := common.ConnectToPostgres(secret, database)
274274
if err != nil {
@@ -298,7 +298,20 @@ func (r *GrantStatementReconciler) revokeAllPrivileges(grantStatement *postgresq
298298
schemas = append(schemas, schema)
299299
}
300300

301-
role := fmt.Sprintf("\"%s\"", roleRef.Name)
301+
// Get the Role object to access UserNameOverride
302+
roleObj := &postgresqlv1alpha1.Role{}
303+
err = r.Get(ctx, types.NamespacedName{
304+
Namespace: roleRef.Namespace,
305+
Name: roleRef.Name,
306+
}, roleObj)
307+
if err != nil {
308+
message = fmt.Sprintf("Failed to get role resource %s/%s for GrantStatement %s", roleRef.Namespace, roleRef.Name, grantStatement.Name)
309+
logger.Error(err, message)
310+
return err, message
311+
}
312+
313+
effectiveRoleName := getEffectiveRoleName(roleObj)
314+
role := fmt.Sprintf("\"%s\"", effectiveRoleName)
302315
rootUser := string(secret.Data[common.ResourceCredentialsSecretUserKey])
303316

304317
// For each schema, execute each SQL command to revoke all privileges as a separate transaction

controllers/postgresql/role_controller.go

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,15 @@ type RoleReconciler struct {
7979
Scheme *runtime.Scheme
8080
}
8181

82+
// getEffectiveRoleName returns the PostgreSQL role name to use.
83+
// If UserNameOverride is set, it takes precedence over the CRD name.
84+
func getEffectiveRoleName(role *postgresql.Role) string {
85+
if role.Spec.UserNameOverride != nil && *role.Spec.UserNameOverride != "" {
86+
return *role.Spec.UserNameOverride
87+
}
88+
return role.Name
89+
}
90+
8291
//+kubebuilder:rbac:groups=postgresql.facets.cloud,resources=roles,verbs=get;list;watch;create;update;patch;delete
8392
//+kubebuilder:rbac:groups=postgresql.facets.cloud,resources=roles/status,verbs=get;update;patch
8493
//+kubebuilder:rbac:groups=postgresql.facets.cloud,resources=roles/finalizers,verbs=update
@@ -179,7 +188,7 @@ func (r *RoleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.
179188
defaultDatabase := string(connectionSecret.Data[common.ResourceCredentialsSecretDatabaseKey])
180189
roleDB, err = common.ConnectToPostgres(connectionSecret, defaultDatabase)
181190
if err != nil {
182-
reason := fmt.Sprintf("Failed connecting to database for role `%s`", role.Name)
191+
reason := fmt.Sprintf("Failed connecting to database for role `%s`", getEffectiveRoleName(role))
183192
roleLogger.Error(err, reason)
184193
r.appendRoleStatusCondition(ctx, role, common.FAIL, metav1.ConditionFalse, common.CONNECTIONFAILED, err.Error())
185194
}
@@ -325,31 +334,31 @@ func (r *RoleReconciler) findObjectsForSecret(secret client.Object) []reconcile.
325334

326335
func (r *RoleReconciler) CreateRole(ctx context.Context, role *postgresql.Role, rolePassword string) (string, metav1.ConditionStatus, string, string) {
327336
privileges := strings.Join(PrivilegesToClauses(role.Spec.Privileges), " ")
328-
createRoleQuery := fmt.Sprintf("CREATE ROLE \"%s\" WITH %s PASSWORD '%s' CONNECTION LIMIT %d", role.Name, privileges, rolePassword, *role.Spec.ConnectionLimit)
337+
createRoleQuery := fmt.Sprintf("CREATE ROLE \"%s\" WITH %s PASSWORD '%s' CONNECTION LIMIT %d", getEffectiveRoleName(role), privileges, rolePassword, *role.Spec.ConnectionLimit)
329338
_, err := roleDB.Exec(createRoleQuery)
330339
if err != nil {
331-
if strings.Contains(err.Error(), fmt.Sprintf("pq: role \"%s\" already exists", role.Name)) {
332-
roleLogger.Error(err, fmt.Sprintf("Role `%s` created outside of database operator.", role.Name))
340+
if strings.Contains(err.Error(), fmt.Sprintf("pq: role \"%s\" already exists", getEffectiveRoleName(role))) {
341+
roleLogger.Error(err, fmt.Sprintf("Role `%s` created outside of database operator.", getEffectiveRoleName(role)))
333342
return common.FAIL, metav1.ConditionFalse, ROLECREATEFAILED, errRoleCreatedOutside
334343
} else {
335-
roleLogger.Error(err, fmt.Sprintf("Failed to create role `%s`, Check if the secret `%s/%s` has valid database connection details", role.Name, role.Spec.ConnectSecretRef.Namespace, role.Spec.ConnectSecretRef.Name))
344+
roleLogger.Error(err, fmt.Sprintf("Failed to create role `%s`, Check if the secret `%s/%s` has valid database connection details", getEffectiveRoleName(role), role.Spec.ConnectSecretRef.Namespace, role.Spec.ConnectSecretRef.Name))
336345
return common.FAIL, metav1.ConditionFalse, ROLECREATEFAILED, fmt.Sprintf("%s, Check if the secret `%s/%s` has valid database connection details", err.Error(), role.Spec.ConnectSecretRef.Namespace, role.Spec.ConnectSecretRef.Name)
337346
}
338347
}
339348

340-
roleLogger.Info(fmt.Sprintf("Role `%s` got created successfully", role.Name))
349+
roleLogger.Info(fmt.Sprintf("Role `%s` got created successfully", getEffectiveRoleName(role)))
341350
return common.CREATE, metav1.ConditionTrue, ROLECREATED, "Role created successfully"
342351
}
343352

344353
func (r *RoleReconciler) DeletRole(ctx context.Context, role *v1alpha1.Role) (string, metav1.ConditionStatus, string, string, error) {
345-
deleteRoleQuery := fmt.Sprintf("DROP ROLE IF EXISTS \"%s\"", role.Name)
354+
deleteRoleQuery := fmt.Sprintf("DROP ROLE IF EXISTS \"%s\"", getEffectiveRoleName(role))
346355
_, err := roleDB.Exec(deleteRoleQuery)
347356
if err != nil {
348-
roleLogger.Error(err, fmt.Sprintf("Failed to delete role `%s`", role.Name))
357+
roleLogger.Error(err, fmt.Sprintf("Failed to delete role `%s`", getEffectiveRoleName(role)))
349358
return common.FAIL, metav1.ConditionFalse, ROLEDELETEFAILED, err.Error(), err
350359
}
351360

352-
roleLogger.Info(fmt.Sprintf("Role `%s` got deleted successfully", role.Name))
361+
roleLogger.Info(fmt.Sprintf("Role `%s` got deleted successfully", getEffectiveRoleName(role)))
353362
return common.DELETE, metav1.ConditionTrue, ROLEDELETED, "Role deleted successfully", err
354363
}
355364

@@ -376,23 +385,23 @@ func (r *RoleReconciler) SyncRole(ctx context.Context, role *postgresql.Role, ro
376385
}
377386
}
378387

379-
alterRoleQuery := fmt.Sprintf("ALTER ROLE \"%s\" WITH %s PASSWORD '%s' CONNECTION LIMIT %d", role.Name, strings.Join(privileges, " "), rolePassword, *role.Spec.ConnectionLimit)
388+
alterRoleQuery := fmt.Sprintf("ALTER ROLE \"%s\" WITH %s PASSWORD '%s' CONNECTION LIMIT %d", getEffectiveRoleName(role), strings.Join(privileges, " "), rolePassword, *role.Spec.ConnectionLimit)
380389
_, err := roleDB.Exec(alterRoleQuery)
381390
if err != nil {
382-
if strings.Contains(err.Error(), fmt.Sprintf("pq: role \"%s\" does not exist", role.Name)) {
383-
roleLogger.Error(err, fmt.Sprintf("Failed to sync role `%s`. Role deleted outside of database operator ", role.Name))
391+
if strings.Contains(err.Error(), fmt.Sprintf("pq: role \"%s\" does not exist", getEffectiveRoleName(role))) {
392+
roleLogger.Error(err, fmt.Sprintf("Failed to sync role `%s`. Role deleted outside of database operator ", getEffectiveRoleName(role)))
384393
return common.SYNC, metav1.ConditionFalse, ROLESYNCFAILED, errRoleDeletedOutside
385394
} else {
386-
roleLogger.Error(err, fmt.Sprintf("Failed to sync role `%s`", role.Name))
395+
roleLogger.Error(err, fmt.Sprintf("Failed to sync role `%s`", getEffectiveRoleName(role)))
387396
return common.SYNC, metav1.ConditionFalse, ROLESYNCFAILED, err.Error()
388397
}
389398
}
390399

391400
if isPasswordSync {
392-
roleLogger.Info(fmt.Sprintf("Role `%s` password got synced successfully", role.Name))
401+
roleLogger.Info(fmt.Sprintf("Role `%s` password got synced successfully", getEffectiveRoleName(role)))
393402
return common.SYNC, metav1.ConditionTrue, ROLEPASSWORDSYNCED, "Role password synced successfully"
394403
}
395-
roleLogger.Info(fmt.Sprintf("Role `%s` got synced successfully", role.Name))
404+
roleLogger.Info(fmt.Sprintf("Role `%s` got synced successfully", getEffectiveRoleName(role)))
396405
return common.SYNC, metav1.ConditionTrue, ROLESYNCED, "Role synced successfully"
397406
}
398407

@@ -411,7 +420,7 @@ func (r *RoleReconciler) ObserveRoleState(ctx context.Context, role *postgresql.
411420

412421
err := roleDB.QueryRow(
413422
observeRoleStateQuery,
414-
role.Name,
423+
getEffectiveRoleName(role),
415424
&role.Spec.Privileges.SuperUser,
416425
&role.Spec.Privileges.Inherit,
417426
&role.Spec.Privileges.CreateRole,
@@ -422,7 +431,7 @@ func (r *RoleReconciler) ObserveRoleState(ctx context.Context, role *postgresql.
422431
&role.Spec.Privileges.BypassRls,
423432
).Scan(&isRoleStateChanged)
424433
if err != nil {
425-
roleLogger.Error(err, fmt.Sprintf("Failed to get role `%s` when observing ", role.Name))
434+
roleLogger.Error(err, fmt.Sprintf("Failed to get role `%s` when observing ", getEffectiveRoleName(role)))
426435
r.appendRoleStatusCondition(ctx, role, common.FAIL, metav1.ConditionFalse, ROLEGETFAILED, err.Error())
427436
}
428437
return isRoleStateChanged

0 commit comments

Comments
 (0)