diff --git a/.gitignore b/.gitignore index 4360d8bfb..87f361d95 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,7 @@ kubeconfig .gomodcache .gocache + +# helm template output temporary files +/**/out +out diff --git a/charts/ext-postgres-operator/Chart.yaml b/charts/ext-postgres-operator/Chart.yaml index ce2d480ff..a6c89f94f 100644 --- a/charts/ext-postgres-operator/Chart.yaml +++ b/charts/ext-postgres-operator/Chart.yaml @@ -8,5 +8,5 @@ description: | type: application -version: 2.2.0 -appVersion: "2.0.0" +version: 2.3.0 +appVersion: "2.4.0" diff --git a/charts/ext-postgres-operator/templates/operator.yaml b/charts/ext-postgres-operator/templates/operator.yaml index 3ef609084..38d075622 100644 --- a/charts/ext-postgres-operator/templates/operator.yaml +++ b/charts/ext-postgres-operator/templates/operator.yaml @@ -44,6 +44,10 @@ spec: {{- toYaml .Values.securityContext | nindent 12 }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: metrics + containerPort: 8080 + protocol: "TCP" envFrom: - secretRef: {{- if .Values.existingSecret }} diff --git a/charts/ext-postgres-operator/templates/pod-monitor.yaml b/charts/ext-postgres-operator/templates/pod-monitor.yaml new file mode 100644 index 000000000..e4813bb1d --- /dev/null +++ b/charts/ext-postgres-operator/templates/pod-monitor.yaml @@ -0,0 +1,32 @@ +{{- if and (.Capabilities.APIVersions.Has "monitoring.coreos.com/v1") ( .Values.podMonitor.enabled ) }} +apiVersion: monitoring.coreos.com/v1 +kind: PodMonitor +metadata: + annotations: + {{- with .Values.podMonitor.additionalAnnotations }} + {{- toYaml . | nindent 4 }} + {{- end }} + labels: + {{- include "chart.labels" . | nindent 4 }} + {{- with .Values.podMonitor.additonalLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} + name: {{ include "chart.fullname" . }} + namespace: {{ .Release.Namespace }} +spec: + namespaceSelector: + matchNames: + - {{ .Release.Namespace }} + podMetricsEndpoints: + - interval: {{ .Values.podMonitor.interval | default "30s" }} + scrapeTimeout: {{ .Values.podMonitor.scrapeTimeout | default "10s" }} + path: /metrics + port: metrics + {{- if .Values.podMonitor.relabelings }} + relabelings: + {{- tpl (toYaml .Values.podMonitor.relabelings) . | nindent 6 }} + {{- end }} + selector: + matchLabels: + {{- include "chart.selectorLabels" . | nindent 6 }} +{{- end -}} diff --git a/charts/ext-postgres-operator/templates/role.yaml b/charts/ext-postgres-operator/templates/role.yaml index ad37ae5cf..97ca0500b 100644 --- a/charts/ext-postgres-operator/templates/role.yaml +++ b/charts/ext-postgres-operator/templates/role.yaml @@ -18,11 +18,31 @@ rules: resources: - pods verbs: - - "get" + - get - apiGroups: - - "apps" + - "" + resources: + - events + verbs: + - create + - patch + - update + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - apps resources: - replicasets - deployments verbs: - - "get" + - get diff --git a/charts/ext-postgres-operator/values.yaml b/charts/ext-postgres-operator/values.yaml index ba6dc18ac..2d79cc112 100644 --- a/charts/ext-postgres-operator/values.yaml +++ b/charts/ext-postgres-operator/values.yaml @@ -79,7 +79,7 @@ watchNamespace: "" # Define connection to postgres database server postgres: # postgres hostname - host: "localhost" + host: "localhost:5432" # postgres admin user and password ( ignored if existingSecret or ExternalSecret is set ) user: "admin" password: "password" @@ -114,6 +114,20 @@ env: {} # POSTGRES_INSTANCE: "XXXXXXXXXX" # POSTGRES_CLOUD_PROVIDER: "AWS" +# podMonitor is a custom resource used by the Prometheus-Operator and others +podMonitor: + enabled: false + interval: 30s + scrapeTimeout: 10s + relabeling: [] + # - targetLabel: app + # replacement: '{{ include "chart.name" . }}' + additonalLabels: {} + # e.g. release label of the prometheus operator + # release: prometheus-operator + additionalAnnotations: {} + # e.g. {} + nodeSelector: {} tolerations: [] diff --git a/cmd/main.go b/cmd/main.go index c445a64a2..14fbecc64 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -47,15 +47,15 @@ func main() { var secureMetrics bool var enableHTTP2 bool var tlsOpts []func(*tls.Config) - flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ + flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metrics endpoint binds to. "+ "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") - flag.BoolVar(&enableLeaderElection, "leader-elect", false, + flag.BoolVar(&enableLeaderElection, "leader-elect", true, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") - flag.BoolVar(&secureMetrics, "metrics-secure", true, + flag.BoolVar(&secureMetrics, "metrics-secure", false, "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.") - flag.BoolVar(&enableHTTP2, "enable-http2", false, + flag.BoolVar(&enableHTTP2, "enable-http2", true, "If set, HTTP/2 will be enabled for the metrics and webhook servers") opts := zap.Options{ Development: true, diff --git a/config/manager/operator.yaml b/config/manager/operator.yaml index 416597594..d5a6092e2 100644 --- a/config/manager/operator.yaml +++ b/config/manager/operator.yaml @@ -19,6 +19,10 @@ spec: - name: ext-postgres-operator image: movetokube/postgres-operator:2.0.0 imagePullPolicy: Always + ports: + - name: metrics + containerPort: 8080 + protocol: "TCP" envFrom: - secretRef: name: ext-postgres-operator diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index f803d4b21..77c714612 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -9,6 +9,7 @@ rules: - configmaps - secrets - services + - events verbs: - "*" - apiGroups: @@ -17,6 +18,18 @@ rules: - pods verbs: - "get" + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete - apiGroups: - "apps" resources: diff --git a/internal/controller/postgres_controller.go b/internal/controller/postgres_controller.go index 151aaea15..96b0b62eb 100644 --- a/internal/controller/postgres_controller.go +++ b/internal/controller/postgres_controller.go @@ -168,6 +168,27 @@ func (r *PostgresReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c instance.Status.Roles.Writer = writer instance.Status.Succeeded = true } + + desiredOwner := instance.Spec.MasterRole + // If no owner was specified, use default owner name + if desiredOwner == "" { + desiredOwner = fmt.Sprintf("%s-group", instance.Spec.Database) + } + // rename owner role if instance.Spec.MasterRole was changed + ownerChanged := instance.Status.Roles.Owner != "" && instance.Status.Roles.Owner != desiredOwner + if ownerChanged { + err = r.pg.RenameGroupRole(instance.Status.Roles.Owner, desiredOwner) + 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) + if err != nil { + return requeue(errors.NewInternalError(err)) + } + instance.Status.Roles.Owner = desiredOwner + } + // create extensions for _, extension := range instance.Spec.Extensions { // Check if extension is already added. Skip if already is added. @@ -184,12 +205,17 @@ func (r *PostgresReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c } // create schemas var ( - database = instance.Spec.Database - owner = instance.Status.Roles.Owner - reader = instance.Status.Roles.Reader - writer = instance.Status.Roles.Writer - readerPrivs = "SELECT" - writerPrivs = "SELECT,INSERT,DELETE,UPDATE" + database = instance.Spec.Database + owner = instance.Status.Roles.Owner + reader = instance.Status.Roles.Reader + writer = instance.Status.Roles.Writer + readerPrivs = "SELECT" + writerPrivs = "SELECT,INSERT,DELETE,UPDATE" + writerSequencePrivs = "USAGE,SELECT" + writerFunctionPrivs = "EXECUTE" + ownerPrivs = "ALL" + ownerFunctionPrivs = "ALL" + ownerSequencePrivs = "ALL" ) for _, schema := range instance.Spec.Schemas { // Schema was previously created @@ -203,6 +229,11 @@ func (r *PostgresReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c reqLogger.Error(err, fmt.Sprintf("Could not create schema %s", schema)) continue } + instance.Status.Schemas = append(instance.Status.Schemas, schema) + } + + // Set privileges on schemas during every reconcile to ensure privileges are correct + for _, schema := range instance.Spec.Schemas { // Set privileges on schema schemaPrivilegesReader := postgres.PostgresSchemaPrivileges{ @@ -218,32 +249,35 @@ func (r *PostgresReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c continue } schemaPrivilegesWriter := postgres.PostgresSchemaPrivileges{ - DB: database, - Role: writer, - Schema: schema, - Privs: writerPrivs, - CreateSchema: true, + DB: database, + Role: writer, + Schema: schema, + Privs: writerPrivs, + SequencePrivs: writerSequencePrivs, + FunctionPrivs: writerFunctionPrivs, + CreateSchema: true, } err = r.pg.SetSchemaPrivileges(schemaPrivilegesWriter, reqLogger) if err != nil { - reqLogger.Error(err, fmt.Sprintf("Could not give %s permissions \"%s\"", writer, writerPrivs)) + reqLogger.Error(err, fmt.Sprintf("Could not give %s permissions \"%s\", sequence privileges \"%s\", and function privileges \"%s\"", writer, writerPrivs, writerSequencePrivs, writerFunctionPrivs)) continue } schemaPrivilegesOwner := postgres.PostgresSchemaPrivileges{ - DB: database, - Role: owner, - Schema: schema, - Privs: writerPrivs, - CreateSchema: true, + DB: database, + Role: owner, + Schema: schema, + Privs: ownerPrivs, + SequencePrivs: ownerSequencePrivs, + FunctionPrivs: ownerFunctionPrivs, + CreateSchema: true, } err = r.pg.SetSchemaPrivileges(schemaPrivilegesOwner, reqLogger) if err != nil { - reqLogger.Error(err, fmt.Sprintf("Could not give %s permissions \"%s\"", writer, writerPrivs)) + reqLogger.Error(err, fmt.Sprintf("Could not give %s permissions \"%s\", sequence privileges \"%s\", and function privileges \"%s\"", owner, ownerPrivs, ownerSequencePrivs, ownerFunctionPrivs)) continue } - - instance.Status.Schemas = append(instance.Status.Schemas, schema) } + err = r.Status().Patch(ctx, instance, client.MergeFrom(before)) if err != nil { return requeue(err) diff --git a/internal/controller/postgres_controller_test.go b/internal/controller/postgres_controller_test.go index 86e2bfa3e..b98167be9 100644 --- a/internal/controller/postgres_controller_test.go +++ b/internal/controller/postgres_controller_test.go @@ -71,6 +71,8 @@ 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(), gomock.Any()).Return(nil).AnyTimes() cl = k8sClient // Create runtime scheme sc = scheme.Scheme @@ -684,10 +686,10 @@ var _ = Describe("PostgresReconciler", func() { // Expected method calls // customers schema pg.EXPECT().CreateSchema(name, name+"-group", "customers", gomock.Any()).Return(nil).Times(1) - pg.EXPECT().SetSchemaPrivileges(gomock.Any(), gomock.Any()).Return(nil).Times(3) + pg.EXPECT().SetSchemaPrivileges(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() // stores schema pg.EXPECT().CreateSchema(name, name+"-group", "stores", gomock.Any()).Return(nil).Times(1) - pg.EXPECT().SetSchemaPrivileges(gomock.Any(), gomock.Any()).Return(nil).Times(3) + pg.EXPECT().SetSchemaPrivileges(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() }) It("should update status", func() { @@ -708,10 +710,10 @@ var _ = Describe("PostgresReconciler", func() { // Expected method calls // customers schema errors pg.EXPECT().CreateSchema(name, name+"-group", "customers", gomock.Any()).Return(fmt.Errorf("Could not create schema")).Times(1) - pg.EXPECT().SetSchemaPrivileges(gomock.Any(), gomock.Any()).Return(nil).Times(0) + pg.EXPECT().SetSchemaPrivileges(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() // stores schema pg.EXPECT().CreateSchema(name, name+"-group", "stores", gomock.Any()).Return(nil).Times(1) - pg.EXPECT().SetSchemaPrivileges(gomock.Any(), gomock.Any()).Return(nil).Times(3) + pg.EXPECT().SetSchemaPrivileges(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() }) It("should update status", func() { @@ -743,10 +745,9 @@ var _ = Describe("PostgresReconciler", func() { It("should not recreate existing schema", func() { // customers schema pg.EXPECT().CreateSchema(name, name+"-group", "customers", gomock.Any()).Return(nil).Times(1) - pg.EXPECT().SetSchemaPrivileges(gomock.Any(), gomock.Any()).Return(nil).Times(3) + pg.EXPECT().SetSchemaPrivileges(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() // stores schema already exists pg.EXPECT().CreateSchema(name, name+"-group", "stores", gomock.Any()).Times(0) - pg.EXPECT().SetSchemaPrivileges(gomock.Any(), gomock.Any()).Return(nil).Times(0) // Call reconcile err := runReconcile(rp, ctx, req) Expect(err).NotTo(HaveOccurred()) diff --git a/internal/controller/postgresuser_controller.go b/internal/controller/postgresuser_controller.go index 6870a683d..ad36bf5a7 100644 --- a/internal/controller/postgresuser_controller.go +++ b/internal/controller/postgresuser_controller.go @@ -116,7 +116,9 @@ func (r *PostgresUserReconciler) Reconcile(ctx context.Context, req ctrl.Request } // Creation logic - var role, login string + var ( + role, login string + ) password, err := utils.GetSecureRandomString(15) if err != nil { @@ -203,6 +205,56 @@ 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") } + // Reconcile logic for changes in group membership + // This is only applicable if user role is already created + // and privileges are changed in spec + if instance.Status.PostgresRole != "" { + + // We need to get the Postgres CR to get the group role name + database, err := r.getPostgresCR(ctx, instance) + if err != nil { + return r.requeue(ctx, instance, errors.NewInternalError(err)) + } + + // Determine desired group role + var desiredGroup string + switch instance.Spec.Privileges { + case "READ": + desiredGroup = database.Status.Roles.Reader + case "WRITE": + desiredGroup = database.Status.Roles.Writer + default: + desiredGroup = database.Status.Roles.Owner + } + + // Ability user to be reassigned to another group role + currentGroup := instance.Status.PostgresGroup + if desiredGroup != "" && currentGroup != desiredGroup { + + // Remove the old group membership if present + if currentGroup != "" { + if err := r.pg.RevokeRole(currentGroup, role); err != nil { + return r.requeue(ctx, instance, errors.NewInternalError(err)) + } + } + + // Grant the new group role + if err := r.pg.GrantRole(desiredGroup, role); err != nil { + return r.requeue(ctx, instance, errors.NewInternalError(err)) + } + + // Ensure objects created by the user are owned by the new group + if err := r.pg.AlterDefaultLoginRole(role, desiredGroup); err != nil { + return r.requeue(ctx, instance, errors.NewInternalError(err)) + } + + instance.Status.PostgresGroup = desiredGroup + if err := r.Status().Update(ctx, instance); err != nil { + return r.requeue(ctx, instance, err) + } + } + } + err = r.addFinalizer(ctx, reqLogger, instance) if err != nil { return r.requeue(ctx, instance, err) diff --git a/pkg/postgres/database.go b/pkg/postgres/database.go index a6b260f57..7608f8da3 100644 --- a/pkg/postgres/database.go +++ b/pkg/postgres/database.go @@ -8,19 +8,24 @@ import ( ) const ( - CREATE_DB = `CREATE DATABASE "%s"` - CREATE_SCHEMA = `CREATE SCHEMA IF NOT EXISTS "%s" AUTHORIZATION "%s"` - CREATE_EXTENSION = `CREATE EXTENSION IF NOT EXISTS "%s"` - ALTER_DB_OWNER = `ALTER DATABASE "%s" OWNER TO "%s"` - DROP_DATABASE = `DROP DATABASE "%s"` - GRANT_USAGE_SCHEMA = `GRANT USAGE ON SCHEMA "%s" TO "%s"` - GRANT_CREATE_TABLE = `GRANT CREATE ON SCHEMA "%s" TO "%s"` - GRANT_ALL_TABLES = `GRANT %s ON ALL TABLES IN SCHEMA "%s" TO "%s"` - DEFAULT_PRIVS_SCHEMA = `ALTER DEFAULT PRIVILEGES IN SCHEMA "%s" GRANT %s ON TABLES TO "%s"` - REVOKE_CONNECT = `REVOKE CONNECT ON DATABASE "%s" FROM public` - TERMINATE_BACKEND = `SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = '%s' AND pid <> pg_backend_pid()` - GET_DB_OWNER = `SELECT pg_catalog.pg_get_userbyid(d.datdba) FROM pg_catalog.pg_database d WHERE d.datname = '%s'` - GRANT_CREATE_SCHEMA = `GRANT CREATE ON DATABASE "%s" TO "%s"` + CREATE_DB = `CREATE DATABASE "%s"` + CREATE_SCHEMA = `CREATE SCHEMA IF NOT EXISTS "%s" AUTHORIZATION "%s"` + CREATE_EXTENSION = `CREATE EXTENSION IF NOT EXISTS "%s"` + ALTER_DB_OWNER = `ALTER DATABASE "%s" OWNER TO "%s"` + REASSIGN_DB_OWNER = `REASSIGN OWNED BY "%s" TO "%s"` + DROP_DATABASE = `DROP DATABASE "%s"` + GRANT_USAGE_SCHEMA = `GRANT USAGE ON SCHEMA "%s" TO "%s"` + GRANT_CREATE_TABLE = `GRANT CREATE ON SCHEMA "%s" TO "%s"` + GRANT_ALL_TABLES = `GRANT %s ON ALL TABLES IN SCHEMA "%s" TO "%s"` + DEFAULT_PRIVS_SCHEMA = `ALTER DEFAULT PRIVILEGES IN SCHEMA "%s" GRANT %s ON TABLES TO "%s"` + GRANT_ALL_FUNCTIONS = `GRANT %s ON ALL FUNCTIONS IN SCHEMA "%s" TO "%s"` + DEFAULT_PRIVS_FUNCTIONS = `ALTER DEFAULT PRIVILEGES IN SCHEMA "%s" GRANT %s ON FUNCTIONS TO "%s"` + GRANT_ALL_SEQUENCES = `GRANT %s ON ALL SEQUENCES IN SCHEMA "%s" TO "%s"` + DEFAULT_PRIVS_SEQUENCES = `ALTER DEFAULT PRIVILEGES IN SCHEMA "%s" GRANT %s ON SEQUENCES TO "%s"` + REVOKE_CONNECT = `REVOKE CONNECT ON DATABASE "%s" FROM public` + TERMINATE_BACKEND = `SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = '%s' AND pid <> pg_backend_pid()` + GET_DB_OWNER = `SELECT pg_catalog.pg_get_userbyid(d.datdba) FROM pg_catalog.pg_database d WHERE d.datname = '%s'` + GRANT_CREATE_SCHEMA = `GRANT CREATE ON DATABASE "%s" TO "%s"` ) func (c *pg) CreateDB(dbname, role string) error { @@ -44,6 +49,36 @@ func (c *pg) CreateDB(dbname, role string) error { return nil } +// reconcile the desired owner of the database +func (c *pg) AlterDatabaseOwner(dbname, owner string) error { + if owner == "" { + return nil + } + _, err := c.db.Exec(fmt.Sprintf(ALTER_DB_OWNER, dbname, owner)) + return err +} + +func (c *pg) ReassignDatabaseOwner(dbName, currentOwner, newOwner string, logger logr.Logger) error { + if currentOwner == "" || newOwner == "" || currentOwner == newOwner { + return nil + } + + tmpDb, err := GetConnection(c.user, c.pass, c.host, dbName, c.args, logger) + if err != nil { + return err + } + defer tmpDb.Close() + + _, err = tmpDb.Exec(fmt.Sprintf(REASSIGN_DB_OWNER, currentOwner, newOwner)) + if err != nil { + if pqErr, ok := err.(*pq.Error); ok && pqErr.Code == "42704" { + return nil + } + return err + } + return nil +} + func (c *pg) CreateSchema(db, role, schema string, logger logr.Logger) error { tmpDb, err := GetConnection(c.user, c.pass, c.host, db, c.args, logger) if err != nil { @@ -120,6 +155,34 @@ func (c *pg) SetSchemaPrivileges(schemaPrivileges PostgresSchemaPrivileges, logg return err } + if schemaPrivileges.SequencePrivs != "" { + // Grant role privs on existing sequences in schema + _, err = tmpDb.Exec(fmt.Sprintf(GRANT_ALL_SEQUENCES, schemaPrivileges.SequencePrivs, schemaPrivileges.Schema, schemaPrivileges.Role)) + if err != nil { + return err + } + + // Grant role privs on future sequences in schema + _, err = tmpDb.Exec(fmt.Sprintf(DEFAULT_PRIVS_SEQUENCES, schemaPrivileges.Schema, schemaPrivileges.SequencePrivs, schemaPrivileges.Role)) + if err != nil { + return err + } + } + + if schemaPrivileges.FunctionPrivs != "" { + // Grant role privs on existing functions in schema + _, err = tmpDb.Exec(fmt.Sprintf(GRANT_ALL_FUNCTIONS, schemaPrivileges.FunctionPrivs, schemaPrivileges.Schema, schemaPrivileges.Role)) + if err != nil { + return err + } + + // Grant role privs on future functions in schema + _, err = tmpDb.Exec(fmt.Sprintf(DEFAULT_PRIVS_FUNCTIONS, schemaPrivileges.Schema, schemaPrivileges.FunctionPrivs, schemaPrivileges.Role)) + if err != nil { + return err + } + } + // Grant role usage on schema if createSchema if schemaPrivileges.CreateSchema { _, err = tmpDb.Exec(fmt.Sprintf(GRANT_CREATE_TABLE, schemaPrivileges.Schema, schemaPrivileges.Role)) diff --git a/pkg/postgres/mock/postgres.go b/pkg/postgres/mock/postgres.go index 4f53e59ec..3c920cd09 100644 --- a/pkg/postgres/mock/postgres.go +++ b/pkg/postgres/mock/postgres.go @@ -97,6 +97,20 @@ func (mr *MockPGMockRecorder) CreateGroupRole(role any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateGroupRole", reflect.TypeOf((*MockPG)(nil).CreateGroupRole), role) } +// RenameGroupRole mocks base method. +func (m *MockPG) RenameGroupRole(currentRole, newRole string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RenameGroupRole", currentRole, newRole) + ret0, _ := ret[0].(error) + return ret0 +} + +// RenameGroupRole indicates an expected call of RenameGroupRole. +func (mr *MockPGMockRecorder) RenameGroupRole(currentRole, newRole any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RenameGroupRole", reflect.TypeOf((*MockPG)(nil).RenameGroupRole), currentRole, newRole) +} + // CreateSchema mocks base method. func (m *MockPG) CreateSchema(db, role, schema string, logger logr.Logger) error { m.ctrl.T.Helper() @@ -196,6 +210,34 @@ func (mr *MockPGMockRecorder) GrantRole(role, grantee any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantRole", reflect.TypeOf((*MockPG)(nil).GrantRole), role, grantee) } +// AlterDatabaseOwner mocks base method. +func (m *MockPG) AlterDatabaseOwner(dbName, owner string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AlterDatabaseOwner", dbName, owner) + ret0, _ := ret[0].(error) + return ret0 +} + +// AlterDatabaseOwner indicates an expected call of AlterDatabaseOwner. +func (mr *MockPGMockRecorder) AlterDatabaseOwner(dbName, owner any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AlterDatabaseOwner", reflect.TypeOf((*MockPG)(nil).AlterDatabaseOwner), dbName, owner) +} + +// ReassignDatabaseOwner mocks base method. +func (m *MockPG) ReassignDatabaseOwner(dbName, currentOwner, newOwner string, logger logr.Logger) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ReassignDatabaseOwner", dbName, currentOwner, newOwner, logger) + ret0, _ := ret[0].(error) + return ret0 +} + +// ReassignDatabaseOwner indicates an expected call of ReassignDatabaseOwner. +func (mr *MockPGMockRecorder) ReassignDatabaseOwner(dbName, currentOwner, newOwner, logger any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReassignDatabaseOwner", reflect.TypeOf((*MockPG)(nil).ReassignDatabaseOwner), dbName, currentOwner, newOwner, logger) +} + // RevokeRole mocks base method. func (m *MockPG) RevokeRole(role, revoked string) error { m.ctrl.T.Helper() diff --git a/pkg/postgres/postgres.go b/pkg/postgres/postgres.go index 033640abe..c4f12e527 100644 --- a/pkg/postgres/postgres.go +++ b/pkg/postgres/postgres.go @@ -14,9 +14,12 @@ type PG interface { CreateSchema(db, role, schema string, logger logr.Logger) error CreateExtension(db, extension string, logger logr.Logger) error CreateGroupRole(role string) error + RenameGroupRole(currentRole, newRole string) error CreateUserRole(role, password string) (string, error) UpdatePassword(role, password string) error GrantRole(role, grantee string) error + AlterDatabaseOwner(dbName, owner string) error + ReassignDatabaseOwner(dbName, currentOwner, newOwner string, logger logr.Logger) error SetSchemaPrivileges(schemaPrivileges PostgresSchemaPrivileges, logger logr.Logger) error RevokeRole(role, revoked string) error AlterDefaultLoginRole(role, setRole string) error @@ -37,11 +40,13 @@ type pg struct { } type PostgresSchemaPrivileges struct { - DB string - Role string - Schema string - Privs string - CreateSchema bool + DB string + Role string + Schema string + Privs string + SequencePrivs string + FunctionPrivs string + CreateSchema bool } func NewPG(cfg *config.Cfg, logger logr.Logger) (PG, error) { diff --git a/pkg/postgres/role.go b/pkg/postgres/role.go index 8bf4f4b71..0b4d4b240 100644 --- a/pkg/postgres/role.go +++ b/pkg/postgres/role.go @@ -9,6 +9,7 @@ import ( const ( CREATE_GROUP_ROLE = `CREATE ROLE "%s"` + RENAME_GROUP_ROLE = `ALTER ROLE "%s" RENAME TO "%s"` CREATE_USER_ROLE = `CREATE ROLE "%s" WITH LOGIN PASSWORD '%s'` GRANT_ROLE = `GRANT "%s" TO "%s"` ALTER_USER_SET_ROLE = `ALTER USER "%s" SET ROLE "%s"` @@ -28,6 +29,20 @@ func (c *pg) CreateGroupRole(role string) error { return nil } +func (c *pg) RenameGroupRole(currentRole, newRole string) error { + _, err := c.db.Exec(fmt.Sprintf(RENAME_GROUP_ROLE, currentRole, newRole)) + if err != nil { + if pqErr, ok := err.(*pq.Error); ok { + // 42704 => role does not exist; treat as success so caller can recreate + if pqErr.Code == "42704" { + return nil + } + } + return err + } + return nil +} + func (c *pg) CreateUserRole(role, password string) (string, error) { _, err := c.db.Exec(fmt.Sprintf(CREATE_USER_ROLE, role, password)) if err != nil {