Skip to content
Open
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
5 changes: 5 additions & 0 deletions api/v1alpha1/postgresuser_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ type PostgresUserSpec struct {
Annotations map[string]string `json:"annotations,omitempty"`
// +optional
Labels map[string]string `json:"labels,omitempty"`
// +optional
Replication bool `json:"replication,omitempty"`
}

// PostgresUserAWSSpec encapsulates AWS specific configuration toggles.
Expand All @@ -46,6 +48,9 @@ type PostgresUserStatus struct {
// Reflects whether IAM authentication is enabled for this user.
// +optional
EnableIamAuth bool `json:"enableIamAuth"`
// Grants the REPLICATION attribute, or rds_replication on AWS RDS.
// +optional
Replication bool `json:"replication,omitempty"`
}

// +kubebuilder:object:root=true
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
name: postgresusers.db.movetokube.com
spec:
group: db.movetokube.com
Expand All @@ -17,14 +20,19 @@ spec:
description: PostgresUser is the Schema for the postgresusers API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
Expand All @@ -36,15 +44,18 @@ spec:
type: string
type: object
aws:
description: AWS specific settings for the user
description: PostgresUserAWSSpec encapsulates AWS specific configuration
toggles.
properties:
enableIamAuth:
description: Enable IAM authentication for this user (PostgreSQL on AWS RDS only)
default: false
description: Enable IAM authentication for this user (PostgreSQL
on AWS RDS only)
type: boolean
type: object
database:
description: Name of the PostgresDatabase this user will be related to
description: Name of the PostgresDatabase this user will be related
to
type: string
labels:
additionalProperties:
Expand All @@ -53,8 +64,11 @@ spec:
privileges:
description: List of privileges to grant to this user
type: string
replication:
type: boolean
role:
description: Name of the PostgresRole this user will be associated with
description: Name of the PostgresRole this user will be associated
with
type: string
secretName:
description: Name of the secret to create with user credentials
Expand All @@ -71,17 +85,20 @@ spec:
status:
description: PostgresUserStatus defines the observed state of PostgresUser
properties:
enableIamAuth:
description: Reflects whether IAM authentication is enabled for this user.
type: boolean
databaseName:
type: string
enableIamAuth:
description: Reflects whether IAM authentication is enabled for this
user.
type: boolean
postgresGroup:
type: string
postgresLogin:
type: string
postgresRole:
type: string
replication:
type: boolean
succeeded:
type: boolean
required:
Expand Down
4 changes: 4 additions & 0 deletions config/crd/bases/db.movetokube.com_postgresusers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ spec:
privileges:
description: List of privileges to grant to this user
type: string
replication:
type: boolean
role:
description: Name of the PostgresRole this user will be associated
with
Expand Down Expand Up @@ -95,6 +97,8 @@ spec:
type: string
postgresRole:
type: string
replication:
type: boolean
succeeded:
type: boolean
required:
Expand Down
11 changes: 11 additions & 0 deletions internal/controller/postgresuser_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,17 @@ func (r *PostgresUserReconciler) Reconcile(ctx context.Context, req ctrl.Request
reqLogger.WithValues("role", role).Info("IAM Auth requested while we are not running with AWS cloud provider config")
}

if instance.Spec.Replication != instance.Status.Replication {
if err := r.pg.SetReplication(role, instance.Spec.Replication); err != nil {
reqLogger.WithValues("role", role).Error(err, "failed to set replication")
return r.requeue(ctx, instance, err)
}
instance.Status.Replication = instance.Spec.Replication
if sErr := r.Status().Update(ctx, instance); sErr != nil {
return r.requeue(ctx, instance, sErr)
}
}

// Reconcile logic for changes in group membership
// This is only applicable if user role is already created
// and privileges are changed in spec
Expand Down
118 changes: 118 additions & 0 deletions internal/controller/postgresuser_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,124 @@ var _ = Describe("PostgresUser Controller", func() {
Expect(foundUser.Status.EnableIamAuth).To(BeFalse())
})
})
Context("Replication", func() {
var (
postgresDB *dbv1alpha1.Postgres
postgresUser *dbv1alpha1.PostgresUser
)

BeforeEach(func() {
postgresDB = &dbv1alpha1.Postgres{
ObjectMeta: metav1.ObjectMeta{
Name: databaseName,
Namespace: namespace,
},
Spec: dbv1alpha1.PostgresSpec{Database: databaseName},
Status: dbv1alpha1.PostgresStatus{
Succeeded: true,
Roles: dbv1alpha1.PostgresRoles{
Owner: databaseName + "-group",
Reader: databaseName + "-reader",
Writer: databaseName + "-writer",
},
},
}

postgresUser = &dbv1alpha1.PostgresUser{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: dbv1alpha1.PostgresUserSpec{
Database: databaseName,
SecretName: secretName,
Role: roleName,
Privileges: "WRITE",
},
}
})

AfterEach(func() {
secretList := &corev1.SecretList{}
Expect(cl.List(ctx, secretList, client.InNamespace(namespace))).To(Succeed())
for _, secret := range secretList.Items {
Expect(cl.Delete(ctx, &secret)).To(Succeed())
}
})

It("enables replication when spec is true and status is false", func() {
user := postgresUser.DeepCopy()
user.Spec.Replication = true
user.Status = dbv1alpha1.PostgresUserStatus{
Succeeded: true,
PostgresGroup: databaseName + "-writer",
PostgresRole: roleName + "-exists",
DatabaseName: databaseName,
PostgresLogin: "login",
}
initClient(postgresDB, user, false)

pg.EXPECT().SetReplication(roleName+"-exists", true).Return(nil)
pg.EXPECT().UpdatePassword(gomock.Any(), gomock.Any()).Return(nil)

err := runReconcile(rp, ctx, req)
Expect(err).NotTo(HaveOccurred())

foundUser := &dbv1alpha1.PostgresUser{}
err = cl.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, foundUser)
Expect(err).NotTo(HaveOccurred())
Expect(foundUser.Status.Replication).To(BeTrue())
})

It("disables replication when spec is false and status is true", func() {
user := postgresUser.DeepCopy()
user.Spec.Replication = false
user.Status = dbv1alpha1.PostgresUserStatus{
Succeeded: true,
PostgresGroup: databaseName + "-writer",
PostgresRole: roleName + "-exists",
DatabaseName: databaseName,
PostgresLogin: "login",
Replication: true,
}
initClient(postgresDB, user, false)

pg.EXPECT().SetReplication(roleName+"-exists", false).Return(nil)
pg.EXPECT().UpdatePassword(gomock.Any(), gomock.Any()).Return(nil)

err := runReconcile(rp, ctx, req)
Expect(err).NotTo(HaveOccurred())

foundUser := &dbv1alpha1.PostgresUser{}
err = cl.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, foundUser)
Expect(err).NotTo(HaveOccurred())
Expect(foundUser.Status.Replication).To(BeFalse())
})

It("requeues on SetReplication error", func() {
user := postgresUser.DeepCopy()
user.Spec.Replication = true
user.Status = dbv1alpha1.PostgresUserStatus{
Succeeded: true,
PostgresGroup: databaseName + "-writer",
PostgresRole: roleName + "-exists",
DatabaseName: databaseName,
PostgresLogin: "login",
}
initClient(postgresDB, user, false)

pg.EXPECT().SetReplication(roleName+"-exists", true).Return(fmt.Errorf("replication failed"))

err := runReconcile(rp, ctx, req)
Expect(err).To(HaveOccurred())

foundUser := &dbv1alpha1.PostgresUser{}
err = cl.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, foundUser)
Expect(err).NotTo(HaveOccurred())
Expect(foundUser.Status.Succeeded).To(BeFalse())
Expect(foundUser.Status.Replication).To(BeFalse())
})
})
Context("Secret creation with user-defined labels and annotations", func() {
It("should create a secret with user-defined labels and annotations", func() {
// Set up the reconciler with host and keepSecretName setting
Expand Down
7 changes: 7 additions & 0 deletions pkg/postgres/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ func (c *awspg) CreateUserRole(role, password string) (string, error) {
return returnedRole, nil
}

func (c *awspg) SetReplication(role string, enable bool) error {
if enable {
return c.GrantRole("rds_replication", role)
}
return c.RevokeRole("rds_replication", role)
}

func (c *awspg) DropRole(role, newOwner, database string) error {
// On AWS RDS the postgres user isn't really superuser so he doesn't have permissions
// to REASSIGN OWNED BY unless he belongs to both roles
Expand Down
14 changes: 14 additions & 0 deletions pkg/postgres/mock/postgres.go

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

1 change: 1 addition & 0 deletions pkg/postgres/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type PG interface {
AlterDefaultLoginRole(role, setRole string) error
DropDatabase(db string) error
DropRole(role, newOwner, database string) error
SetReplication(role string, enable bool) error
GetUser() string
GetDefaultDatabase() string
}
Expand Down
9 changes: 9 additions & 0 deletions pkg/postgres/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,15 @@ func (c *pg) DropRole(role, newOwner, database string) error {
return nil
}

func (c *pg) SetReplication(role string, enable bool) error {
attribute := "NOREPLICATION"
if enable {
attribute = "REPLICATION"
}
_, err := c.db.Exec(fmt.Sprintf(`ALTER ROLE %s WITH %s`, pq.QuoteIdentifier(role), attribute))
return err
}

func (c *pg) UpdatePassword(role, password string) error {
_, err := c.db.Exec(fmt.Sprintf(UPDATE_PASSWORD, role, password))
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions tests/e2e/basic-operations/02-assert.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ spec:
status:
databaseName: test-db
postgresGroup: test-db-group
replication: true
succeeded: true
---
apiVersion: v1
Expand Down
1 change: 1 addition & 0 deletions tests/e2e/basic-operations/02-postgresuser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ spec:
database: my-db
secretName: my-secret
privileges: OWNER
replication: true
labels:
custom-label: custom-value