diff --git a/README.md b/README.md index 8f5d8bd3..f93d86aa 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,27 @@ _None yet. [Become a sponsor!](https://github.com/sponsors/hitman99)_ - Handles CRs in dynamically created namespaces - Customizable secret values using templates +## AWS Specific Features when cloud_provider: "AWS" + +- Enable IAM authentication for this user (PostgreSQL on AWS RDS only) + + ```yaml + kind: PostgresUser + .... + spec: + aws: + enableIamAuth: false # (by Default false) + ``` +- AWS PG_repack extension installation / properly alter privileges for the owner user if cloud_provider: "AWS" + + ```yaml + kind: Postgres + ... + spec: + extensions: + - pg_repack + ``` + --- ## Supported Cloud Providers diff --git a/charts/ext-postgres-operator/Chart.yaml b/charts/ext-postgres-operator/Chart.yaml index 1407a4fd..499f38d3 100644 --- a/charts/ext-postgres-operator/Chart.yaml +++ b/charts/ext-postgres-operator/Chart.yaml @@ -9,4 +9,4 @@ description: | type: application version: 3.0.0 -appVersion: "2.4.0" +appVersion: "2.5.0" diff --git a/go.mod b/go.mod index dce6f3f1..fddeabdb 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( require ( cel.dev/expr v0.24.0 // indirect + github.com/DATA-DOG/go-sqlmock v1.5.2 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/beorn7/perks v1.0.1 // indirect diff --git a/go.sum b/go.sum index ec4cf339..d8ee4dda 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= @@ -82,6 +84,7 @@ github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= diff --git a/internal/controller/postgres_controller.go b/internal/controller/postgres_controller.go index da19b93e..645328aa 100644 --- a/internal/controller/postgres_controller.go +++ b/internal/controller/postgres_controller.go @@ -181,8 +181,8 @@ func (r *PostgresReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c if err != nil { return requeue(errors.NewInternalError(err)) } - // Alter database owner if the owner role was changed - err = r.pg.AlterDatabaseOwner(instance.Spec.Database, instance.Status.Roles.Owner) + // Alter database owner to desiredOwner if the owner role was changed + err = r.pg.AlterDatabaseOwner(instance.Spec.Database, desiredOwner) if err != nil { return requeue(errors.NewInternalError(err)) } diff --git a/internal/controller/postgres_controller_test.go b/internal/controller/postgres_controller_test.go index 2f9d62b0..949f72fb 100644 --- a/internal/controller/postgres_controller_test.go +++ b/internal/controller/postgres_controller_test.go @@ -71,8 +71,6 @@ var _ = Describe("PostgresReconciler", func() { // Gomock mockCtrl = gomock.NewController(GinkgoT()) pg = mockpg.NewMockPG(mockCtrl) - pg.EXPECT().AlterDatabaseOwner(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() - pg.EXPECT().ReassignDatabaseOwner(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() cl = k8sClient // Create runtime scheme sc = scheme.Scheme @@ -365,6 +363,32 @@ var _ = Describe("PostgresReconciler", func() { }) }) + Context("MasterRole is changed for existing database", func() { + BeforeEach(func() { + modPostgres := postgresCR.DeepCopy() + modPostgres.Spec.MasterRole = "new-master-role" + modPostgres.Status = v1alpha1.PostgresStatus{ + Succeeded: true, + Roles: v1alpha1.PostgresRoles{ + Owner: "old-master-role", + }, + } + initClient(modPostgres, false) + }) + + It("should alter database owner to the desired role", func() { + pg.EXPECT().RenameGroupRole("old-master-role", "new-master-role").Return(nil).Times(1) + pg.EXPECT().AlterDatabaseOwner(name, "new-master-role").Return(nil).Times(1) + + err := runReconcile(rp, ctx, req) + Expect(err).NotTo(HaveOccurred()) + + foundPostgres := &v1alpha1.Postgres{} + Expect(cl.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, foundPostgres)).To(BeNil()) + Expect(foundPostgres.Status.Roles.Owner).To(Equal("new-master-role")) + }) + }) + Context("Correct annotation filter is set", func() { BeforeEach(func() { // Create client diff --git a/pkg/postgres/aws.go b/pkg/postgres/aws.go index a27167a4..a606ebbf 100644 --- a/pkg/postgres/aws.go +++ b/pkg/postgres/aws.go @@ -2,6 +2,7 @@ package postgres import ( "fmt" + "strings" "github.com/lib/pq" ) @@ -10,6 +11,14 @@ type awspg struct { pg } +const ( + AWS_ALTER_REPACK_DEFAULT_PRIVS_TABLES = `ALTER DEFAULT PRIVILEGES FOR ROLE "%s" IN SCHEMA "repack" GRANT INSERT ON TABLES TO PUBLIC` + AWS_ALTER_REPACK_DEFAULT_PRIVS_SEQUENCES = `ALTER DEFAULT PRIVILEGES FOR ROLE "%s" IN SCHEMA "repack" GRANT USAGE, SELECT ON SEQUENCES TO PUBLIC` +) + +// defaults to GetConnection in production, but can be overridden in unit tests. +var awsGetConnection = GetConnection + func newAWSPG(postgres *pg) PG { return &awspg{ *postgres, @@ -38,6 +47,49 @@ func (c *awspg) CreateDB(dbname, role string) error { return c.pg.CreateDB(dbname, role) } +func (c *awspg) CreateExtension(dbname, extension string) error { + // Keep standard extension creation behavior for AWS as well. + err := c.pg.CreateExtension(dbname, extension) + if err != nil { + return err + } + + // AWS-specific workaround is only required for pg_repack. + if !strings.EqualFold(extension, "pg_repack") { + return nil + } + + return c.applyPgRepackPrivileges(dbname) +} + +func (c *awspg) applyPgRepackPrivileges(dbname string) error { + var owner string + // Resolve current database owner role to target ALTER DEFAULT PRIVILEGES FOR ROLE. + err := c.db.QueryRow(fmt.Sprintf(GET_DB_OWNER, dbname)).Scan(&owner) + if err != nil { + return err + } + + // Execute pg_repack privilege statements in the target database. + tmpDb, err := awsGetConnection(c.user, c.pass, c.host, dbname, c.args) + if err != nil { + return err + } + defer tmpDb.Close() + + _, err = tmpDb.Exec(fmt.Sprintf(AWS_ALTER_REPACK_DEFAULT_PRIVS_TABLES, owner)) + if err != nil { + return err + } + + _, err = tmpDb.Exec(fmt.Sprintf(AWS_ALTER_REPACK_DEFAULT_PRIVS_SEQUENCES, owner)) + if err != nil { + return err + } + + return nil +} + func (c *awspg) CreateUserRole(role, password string) (string, error) { returnedRole, err := c.pg.CreateUserRole(role, password) if err != nil { diff --git a/pkg/postgres/aws_test.go b/pkg/postgres/aws_test.go new file mode 100644 index 00000000..736284f3 --- /dev/null +++ b/pkg/postgres/aws_test.go @@ -0,0 +1,68 @@ +package postgres + +import ( + "database/sql" + "fmt" + "regexp" + "testing" + + sqlmock "github.com/DATA-DOG/go-sqlmock" +) + +func TestApplyPgRepackPrivileges(t *testing.T) { + originalGetConnection := awsGetConnection + defer func() { + awsGetConnection = originalGetConnection + }() + + mainDB, mainMock, err := sqlmock.New() + if err != nil { + t.Fatalf("failed to create main sqlmock: %v", err) + } + defer mainDB.Close() + + tmpDB, tmpMock, err := sqlmock.New() + if err != nil { + t.Fatalf("failed to create tmp sqlmock: %v", err) + } + defer tmpDB.Close() + + dbname := "test-db-dev" + owner := "test-db-dev-owner" + + mainMock.ExpectQuery(regexp.QuoteMeta(fmt.Sprintf(GET_DB_OWNER, dbname))). + WillReturnRows(sqlmock.NewRows([]string{"pg_get_userbyid"}).AddRow(owner)) + + awsGetConnection = func(user, password, host, database, uriArgs string) (*sql.DB, error) { + if database != dbname { + t.Fatalf("expected database %s, got %s", dbname, database) + } + return tmpDB, nil + } + + tmpMock.ExpectExec(regexp.QuoteMeta(fmt.Sprintf(AWS_ALTER_REPACK_DEFAULT_PRIVS_TABLES, owner))). + WillReturnResult(sqlmock.NewResult(0, 0)) + tmpMock.ExpectExec(regexp.QuoteMeta(fmt.Sprintf(AWS_ALTER_REPACK_DEFAULT_PRIVS_SEQUENCES, owner))). + WillReturnResult(sqlmock.NewResult(0, 0)) + + c := &awspg{ + pg: pg{ + db: mainDB, + host: "localhost:5432", + user: "postgres", + pass: "postgres", + args: "sslmode=disable", + }, + } + + if err := c.applyPgRepackPrivileges(dbname); err != nil { + t.Fatalf("expected nil error, got %v", err) + } + + if err := mainMock.ExpectationsWereMet(); err != nil { + t.Fatalf("main DB expectations were not met: %v", err) + } + if err := tmpMock.ExpectationsWereMet(); err != nil { + t.Fatalf("tmp DB expectations were not met: %v", err) + } +}