From d4501f7f7687b6babfa1f9fc1bdafbb4a359a003 Mon Sep 17 00:00:00 2001 From: Tamara Bernshtein Date: Thu, 21 Dec 2023 17:36:58 +0200 Subject: [PATCH] Cockroachdb competability --- postgresql/config.go | 190 +++++++++++++++++- postgresql/helpers.go | 46 +++-- postgresql/resource_postgresql_database.go | 73 ++++--- .../resource_postgresql_default_privileges.go | 26 ++- postgresql/resource_postgresql_grant.go | 38 ++-- postgresql/resource_postgresql_role.go | 60 ++++-- postgresql/resource_postgresql_schema.go | 101 ++++++---- 7 files changed, 383 insertions(+), 151 deletions(-) diff --git a/postgresql/config.go b/postgresql/config.go index c4ed30e8..d941db40 100644 --- a/postgresql/config.go +++ b/postgresql/config.go @@ -17,6 +17,13 @@ import ( _ "gocloud.dev/postgres/gcppostgres" ) +type dbTypes uint + +const ( + dbTypeCockroachdb dbTypes = iota + dbTypePostgresql +) + type featureName uint const ( @@ -24,6 +31,8 @@ const ( featureDatabaseOwnerRole featureDBAllowConnections featureDBIsTemplate + featureDBTablespace + featureUseDBTemplate featureFallbackApplicationName featureRLS featureSchemaCreateIfNotExist @@ -41,6 +50,14 @@ const ( featurePubWithoutTruncate featureFunction featureServer + fetureAclExplode + fetureAclItem + fetureTerminateBackendFunc + fetureRoleConnectionLimit + fetureRoleSuperuser + featureRoleroleInherit + fetureRoleEncryptedPass + featureAdvisoryXactLock ) var ( @@ -48,7 +65,7 @@ var ( dbRegistry map[string]*DBConnection = make(map[string]*DBConnection, 1) // Mapping of feature flags to versions - featureSupported = map[featureName]semver.Range{ + featureSupportedPostgres = map[featureName]semver.Range{ // CREATE ROLE WITH featureCreateRoleWith: semver.MustParseRange(">=8.1.0"), @@ -58,6 +75,12 @@ var ( // CREATE DATABASE has IS_TEMPLATE support featureDBIsTemplate: semver.MustParseRange(">=9.5.0"), + // CREATE DATABASE use template + featureUseDBTemplate: semver.MustParseRange(">=7.1.0"), + + // CREATE DATABASE in tablespace + featureDBTablespace: semver.MustParseRange(">=8.0.0"), + // https://www.postgresql.org/docs/9.0/static/libpq-connect.html featureFallbackApplicationName: semver.MustParseRange(">=9.0.0"), @@ -112,6 +135,132 @@ var ( featureServer: semver.MustParseRange(">=10.0.0"), featureDatabaseOwnerRole: semver.MustParseRange(">=15.0.0"), + + fetureAclExplode: semver.MustParseRange(">=9.0.0"), + + fetureAclItem: semver.MustParseRange(">=12.0.0"), + + fetureTerminateBackendFunc: semver.MustParseRange(">=8.0.0"), + + fetureRoleConnectionLimit: semver.MustParseRange(">=8.1.0"), + + fetureRoleSuperuser: semver.MustParseRange(">=8.1.0"), + + featureRoleroleInherit: semver.MustParseRange(">=8.1.0"), + + fetureRoleEncryptedPass: semver.MustParseRange(">=8.1.0"), + + featureAdvisoryXactLock: semver.MustParseRange(">=9.1.0"), + } + + // Mapping of feature flags to versions + featureSupportedCockroachdb = map[featureName]semver.Range{ + // CREATE ROLE WITH + featureCreateRoleWith: semver.MustParseRange(">=1.0.0"), + + // CREATE DATABASE has ALLOW_CONNECTIONS support + featureDBAllowConnections: semver.MustParseRange("<1.0.0"), + + // CREATE DATABASE has IS_TEMPLATE support + featureDBIsTemplate: semver.MustParseRange("<1.0.0"), + + // CREATE DATABASE use template + featureUseDBTemplate: semver.MustParseRange("<1.0.0"), + + // CREATE DATABASE in tablespace + featureDBTablespace: semver.MustParseRange("<1.0.0"), + + // https://www.postgresql.org/docs/9.0/static/libpq-connect.html + // not supported in Cockroachdb + featureFallbackApplicationName: semver.MustParseRange("<1.0.0"), + + // CREATE SCHEMA IF NOT EXISTS + featureSchemaCreateIfNotExist: semver.MustParseRange(">=1.0.0"), + + // row-level security + featureRLS: semver.MustParseRange("<1.0.0"), + + // CREATE ROLE has REPLICATION support. + // not supported in Cockroachdb + featureReplication: semver.MustParseRange("<1.0.0"), + + // CREATE EXTENSION support. + // not supported in Cockroachdb + featureExtension: semver.MustParseRange("<1.0.0"), + + // We do not support postgresql_grant and postgresql_default_privileges + // for Cockroachdb < 21.2.17 + featurePrivileges: semver.MustParseRange(">=21.2.17"), + + // Object PROCEDURE support + // not supported in Cockroachdb + featureProcedure: semver.MustParseRange("<1.0.0"), + + // Object ROUTINE support + // not supported in Cockroachdb + featureRoutine: semver.MustParseRange("<1.0.0"), + + // ALTER DEFAULT PRIVILEGES has ON SCHEMAS support + // for Cockroachdb < 21.2.17 + featurePrivilegesOnSchemas: semver.MustParseRange(">=21.2.17"), + + // DROP DATABASE WITH FORCE + // not supported in Cockroachdb + featureForceDropDatabase: semver.MustParseRange("<1.0.0"), + + // for CockroachDB pg_catalog >= 20.2.19 and above + featurePid: semver.MustParseRange(">=20.2.19"), + + // attribute publish_via_partition_root for partition is supported + // not supported in Cockroachdb + featurePublishViaRoot: semver.MustParseRange("<1.0.0"), + + // attribute pubtruncate for publications is supported + // not supported in Cockroachdb + featurePubTruncate: semver.MustParseRange("<1.0.0"), + + // attribute pubtruncate for publications is supported + // not supported in Cockroachdb + featurePubWithoutTruncate: semver.MustParseRange("<1.0.0"), + + // publication is Supported + // not supported in Cockroachdb + featurePublication: semver.MustParseRange("<1.0.0"), + + // We do not support CREATE FUNCTION for Cockroachdb < 22.2.17 + featureFunction: semver.MustParseRange(">=22.2.17"), + + // CREATE SERVER support + // not supported in Cockroachdb + featureServer: semver.MustParseRange("<1.0.0"), + + featureDatabaseOwnerRole: semver.MustParseRange(">=20.2.19"), + + //aclexplode function not supported in Cockroachdb + // see https://www.cockroachlabs.com/docs/stable/functions-and-operators + fetureAclExplode: semver.MustParseRange("<1.0.0"), + + //cockroachdb does not support aclitem + fetureAclItem: semver.MustParseRange("<1.0.0"), + + //pg_terminate_backend function not supported in Cockroachdb + fetureTerminateBackendFunc: semver.MustParseRange("<1.0.0"), + + //Cockroachdb does not support connection limit + fetureRoleConnectionLimit: semver.MustParseRange("<1.0.0"), + + //Cockroachdb does not support superuser + fetureRoleSuperuser: semver.MustParseRange("<1.0.0"), + + //Cockroachdb does not role inherit + featureRoleroleInherit: semver.MustParseRange("<1.0.0"), + + //Cockroachdb does not encrypt password + fetureRoleEncryptedPass: semver.MustParseRange("<1.0.0"), + + // cockroach does not support pg_advisory_xact_lock + // https://github.com/cockroachdb/cockroach/issues/13546 + featureAdvisoryXactLock: semver.MustParseRange("<1.0.0"), } ) @@ -123,13 +272,20 @@ type DBConnection struct { // version is the version number of the database as determined by parsing the // output of `SELECT VERSION()`.x version semver.Version + dbType dbTypes } // featureSupported returns true if a given feature is supported or not. This is // slightly different from Config's featureSupported in that here we're // evaluating against the fingerprinted version, not the expected version. func (db *DBConnection) featureSupported(name featureName) bool { - fn, found := featureSupported[name] + var fn semver.Range + var found bool + if db.dbType == dbTypeCockroachdb { + fn, found = featureSupportedCockroachdb[name] + } else { + fn, found = featureSupportedPostgres[name] + } if !found { // panic'ing because this is a provider-only bug panic(fmt.Sprintf("unknown feature flag %v", name)) @@ -194,7 +350,7 @@ func (c *Config) NewClient(database string) *Client { // is slightly different from Client's featureSupported in that here we're // evaluating against the expected version, not the fingerprinted version. func (c *Config) featureSupported(name featureName) bool { - fn, found := featureSupported[name] + fn, found := featureSupportedPostgres[name] if !found { // panic'ing because this is a provider-only bug panic(fmt.Sprintf("unknown feature flag %v", name)) @@ -278,6 +434,7 @@ func (c *Client) Connect() (*DBConnection, error) { var db *sql.DB var err error + var dbType dbTypes if c.config.Scheme == "postgres" { db, err = sql.Open(proxyDriverName, dsn) } else { @@ -302,7 +459,7 @@ func (c *Client) Connect() (*DBConnection, error) { version := &c.config.ExpectedVersion if defaultVersion.Equals(c.config.ExpectedVersion) { // Version hint not set by user, need to fingerprint - version, err = fingerprintCapabilities(db) + version, dbType, err = fingerprintCapabilities(db) if err != nil { _ = db.Close() return nil, fmt.Errorf("error detecting capabilities: %w", err) @@ -313,6 +470,7 @@ func (c *Client) Connect() (*DBConnection, error) { db, c, *version, + dbType, } dbRegistry[dsn] = conn } @@ -322,11 +480,13 @@ func (c *Client) Connect() (*DBConnection, error) { // fingerprintCapabilities queries PostgreSQL to populate a local catalog of // capabilities. This is only run once per Client. -func fingerprintCapabilities(db *sql.DB) (*semver.Version, error) { +func fingerprintCapabilities(db *sql.DB) (*semver.Version, dbTypes, error) { var pgVersion string + var dbType dbTypes + var version semver.Version err := db.QueryRow(`SELECT VERSION()`).Scan(&pgVersion) if err != nil { - return nil, fmt.Errorf("error PostgreSQL version: %w", err) + return nil, dbType, fmt.Errorf("error PostgreSQL version: %w", err) } // PostgreSQL 9.2.21 on x86_64-apple-darwin16.5.0, compiled by Apple LLVM version 8.1.0 (clang-802.0.42), 64-bit @@ -335,13 +495,23 @@ func fingerprintCapabilities(db *sql.DB) (*semver.Version, error) { return unicode.IsSpace(c) || c == ',' }) if len(fields) < 2 { - return nil, fmt.Errorf("error determining the server version: %q", pgVersion) + return nil, dbType, fmt.Errorf("error determining the server version: %q", pgVersion) + } + + //version, err = semver.ParseTolerant(fields[1]) + dbTypeStr := fields[0] + if dbTypeStr == "CockroachDB" { + dbType = dbTypeCockroachdb + version, err = semver.ParseTolerant(fields[2]) + version = semver.MustParse(strings.TrimPrefix(version.String(), "v")) + } else { + dbType = dbTypePostgresql + version, err = semver.ParseTolerant(fields[1]) } - version, err := semver.ParseTolerant(fields[1]) if err != nil { - return nil, fmt.Errorf("error parsing version: %w", err) + return nil, dbType, fmt.Errorf("error parsing version: %w", err) } - return &version, nil + return &version, dbType, nil } diff --git a/postgresql/helpers.go b/postgresql/helpers.go index 1cc0cd1d..01b20681 100644 --- a/postgresql/helpers.go +++ b/postgresql/helpers.go @@ -529,35 +529,37 @@ func getRoleOID(db QueryAble, role string) (uint32, error) { } // Lock a role and all his members to avoid concurrent updates on some resources -func pgLockRole(txn *sql.Tx, role string) error { - // Disable statement timeout for this connection otherwise the lock could fail - if _, err := txn.Exec("SET statement_timeout = 0"); err != nil { - return fmt.Errorf("could not disable statement_timeout: %w", err) - } - if _, err := txn.Exec("SELECT pg_advisory_xact_lock(oid::bigint) FROM pg_roles WHERE rolname = $1", role); err != nil { - return fmt.Errorf("could not get advisory lock for role %s: %w", role, err) - } +func pgLockRole(txn *sql.Tx, db *DBConnection, role string) error { + if db.featureSupported(featureAdvisoryXactLock) { + // Disable statement timeout for this connection otherwise the lock could fail + if _, err := txn.Exec("SET statement_timeout = 0"); err != nil { + return fmt.Errorf("could not disable statement_timeout: %w", err) + } + if _, err := txn.Exec("SELECT pg_advisory_xact_lock(oid::bigint) FROM pg_roles WHERE rolname = $1", role); err != nil { + return fmt.Errorf("could not get advisory lock for role %s: %w", role, err) + } - if _, err := txn.Exec( - "SELECT pg_advisory_xact_lock(member::bigint) FROM pg_auth_members JOIN pg_roles ON roleid = pg_roles.oid WHERE rolname = $1", - role, - ); err != nil { - return fmt.Errorf("could not get advisory lock for members of role %s: %w", role, err) + if _, err := txn.Exec( + "SELECT pg_advisory_xact_lock(member::bigint) FROM pg_auth_members JOIN pg_roles ON roleid = pg_roles.oid WHERE rolname = $1", + role, + ); err != nil { + return fmt.Errorf("could not get advisory lock for members of role %s: %w", role, err) + } } - return nil } // Lock a database and all his members to avoid concurrent updates on some resources -func pgLockDatabase(txn *sql.Tx, database string) error { - // Disable statement timeout for this connection otherwise the lock could fail - if _, err := txn.Exec("SET statement_timeout = 0"); err != nil { - return fmt.Errorf("could not disable statement_timeout: %w", err) - } - if _, err := txn.Exec("SELECT pg_advisory_xact_lock(oid::bigint) FROM pg_database WHERE datname = $1", database); err != nil { - return fmt.Errorf("could not get advisory lock for database %s: %w", database, err) +func pgLockDatabase(txn *sql.Tx, db *DBConnection, database string) error { + if db.featureSupported(featureAdvisoryXactLock) { + // Disable statement timeout for this connection otherwise the lock could fail + if _, err := txn.Exec("SET statement_timeout = 0"); err != nil { + return fmt.Errorf("could not disable statement_timeout: %w", err) + } + if _, err := txn.Exec("SELECT pg_advisory_xact_lock(oid::bigint) FROM pg_database WHERE datname = $1", database); err != nil { + return fmt.Errorf("could not get advisory lock for database %s: %w", database, err) + } } - return nil } diff --git a/postgresql/resource_postgresql_database.go b/postgresql/resource_postgresql_database.go index 9cc9da55..efd4226c 100644 --- a/postgresql/resource_postgresql_database.go +++ b/postgresql/resource_postgresql_database.go @@ -128,7 +128,7 @@ func createDatabase(db *DBConnection, d *schema.ResourceData) error { if err != nil { return err } - if err := pgLockRole(lockTxn, currentUser); err != nil { + if err := pgLockRole(lockTxn, db, currentUser); err != nil { return err } defer deferredRollback(lockTxn) @@ -152,31 +152,28 @@ func createDatabase(db *DBConnection, d *schema.ResourceData) error { // Handle each option individually and stream results into the query // buffer. - switch v, ok := d.GetOk(dbOwnerAttr); { - case ok: - fmt.Fprint(b, " OWNER ", pq.QuoteIdentifier(v.(string))) - default: - // No owner specified in the config, default to using - // the connecting username. - fmt.Fprint(b, " OWNER ", pq.QuoteIdentifier(currentUser)) - } - switch v, ok := d.GetOk(dbTemplateAttr); { - case ok && strings.ToUpper(v.(string)) == "DEFAULT": - fmt.Fprint(b, " TEMPLATE DEFAULT") - case ok: - fmt.Fprint(b, " TEMPLATE ", pq.QuoteIdentifier(v.(string))) - case v.(string) == "": - fmt.Fprint(b, " TEMPLATE template0") + if db.featureSupported(featureUseDBTemplate) { + switch v, ok := d.GetOk(dbTemplateAttr); { + case ok && strings.ToUpper(v.(string)) == "DEFAULT": + fmt.Fprint(b, " TEMPLATE DEFAULT") + case ok: + fmt.Fprint(b, " TEMPLATE ", pq.QuoteIdentifier(v.(string))) + case v.(string) == "": + fmt.Fprint(b, " TEMPLATE template0") + } } + //cockroachdb support only encoding = 'UTF-8' (instead of UTF8), not supports DEFAULT switch v, ok := d.GetOk(dbEncodingAttr); { - case ok && strings.ToUpper(v.(string)) == "DEFAULT": - fmt.Fprintf(b, " ENCODING DEFAULT") + case ok && strings.ToUpper(v.(string)) == "DEFAULT" && db.dbType == dbTypePostgresql: + fmt.Fprintf(b, " ENCODING = DEFAULT") case ok: - fmt.Fprintf(b, " ENCODING '%s' ", pqQuoteLiteral(v.(string))) - case v.(string) == "": - fmt.Fprint(b, ` ENCODING 'UTF8'`) + fmt.Fprintf(b, " ENCODING = '%s' ", pqQuoteLiteral(v.(string))) + case v.(string) == "" && db.dbType == dbTypePostgresql: + fmt.Fprint(b, ` ENCODING = 'UTF8'`) + case v.(string) == "" && db.dbType == dbTypeCockroachdb: + fmt.Fprint(b, ` ENCODING = 'UTF-8'`) } // Don't specify LC_COLLATE if user didn't specify it @@ -197,11 +194,13 @@ func createDatabase(db *DBConnection, d *schema.ResourceData) error { fmt.Fprintf(b, " LC_CTYPE '%s' ", pqQuoteLiteral(v.(string))) } - switch v, ok := d.GetOk(dbTablespaceAttr); { - case ok && strings.ToUpper(v.(string)) == "DEFAULT": - fmt.Fprint(b, " TABLESPACE DEFAULT") - case ok: - fmt.Fprint(b, " TABLESPACE ", pq.QuoteIdentifier(v.(string))) + if db.featureSupported(featureDBTablespace) { + switch v, ok := d.GetOk(dbTablespaceAttr); { + case ok && strings.ToUpper(v.(string)) == "DEFAULT": + fmt.Fprint(b, " TABLESPACE DEFAULT") + case ok: + fmt.Fprint(b, " TABLESPACE ", pq.QuoteIdentifier(v.(string))) + } } if db.featureSupported(featureDBAllowConnections) { @@ -219,6 +218,16 @@ func createDatabase(db *DBConnection, d *schema.ResourceData) error { fmt.Fprint(b, " IS_TEMPLATE ", val) } + //cockroachdb OWNER needs to be at the end of the command + switch v, ok := d.GetOk(dbOwnerAttr); { + case ok: + fmt.Fprint(b, " OWNER ", pq.QuoteIdentifier(v.(string))) + default: + // No owner specified in the config, default to using + // the connecting username. + fmt.Fprint(b, " OWNER ", pq.QuoteIdentifier(currentUser)) + } + sql := b.String() if _, err := db.Exec(sql); err != nil { return fmt.Errorf("Error creating database %q: %w", dbName, err) @@ -237,7 +246,7 @@ func resourcePostgreSQLDatabaseDelete(db *DBConnection, d *schema.ResourceData) var err error if owner != "" { lockTxn, err := startTransaction(db.client, "") - if err := pgLockRole(lockTxn, currentUser); err != nil { + if err := pgLockRole(lockTxn, db, currentUser); err != nil { return err } defer deferredRollback(lockTxn) @@ -451,7 +460,7 @@ func setDBOwner(db *DBConnection, d *schema.ResourceData) error { currentUser := db.client.config.getDatabaseUsername() lockTxn, err := startTransaction(db.client, "") - if err := pgLockRole(lockTxn, currentUser); err != nil { + if err := pgLockRole(lockTxn, db, currentUser); err != nil { return err } defer deferredRollback(lockTxn) @@ -570,9 +579,11 @@ func terminateBConnections(db *DBConnection, dbName string) error { if db.featureSupported(featurePid) { pid = "pid" } - terminateSql = fmt.Sprintf("SELECT pg_terminate_backend(%s) FROM pg_stat_activity WHERE datname = '%s' AND %s <> pg_backend_pid()", pid, dbName, pid) - if _, err := db.Exec(terminateSql); err != nil { - return fmt.Errorf("Error terminating database connections: %w", err) + if db.featureSupported(fetureTerminateBackendFunc) { + terminateSql = fmt.Sprintf("SELECT pg_terminate_backend(%s) FROM pg_stat_activity WHERE datname = '%s' AND %s <> pg_backend_pid()", pid, dbName, pid) + if _, err := db.Exec(terminateSql); err != nil { + return fmt.Errorf("Error terminating database connections: %w", err) + } } return nil diff --git a/postgresql/resource_postgresql_default_privileges.go b/postgresql/resource_postgresql_default_privileges.go index 351ec803..be52719b 100644 --- a/postgresql/resource_postgresql_default_privileges.go +++ b/postgresql/resource_postgresql_default_privileges.go @@ -102,7 +102,7 @@ func resourcePostgreSQLDefaultPrivilegesRead(db *DBConnection, d *schema.Resourc } defer deferredRollback(txn) - return readRoleDefaultPrivileges(txn, d) + return readRoleDefaultPrivileges(txn, d, db) } func resourcePostgreSQLDefaultPrivilegesCreate(db *DBConnection, d *schema.ResourceData) error { @@ -136,7 +136,7 @@ func resourcePostgreSQLDefaultPrivilegesCreate(db *DBConnection, d *schema.Resou } defer deferredRollback(txn) - if err := pgLockRole(txn, owner); err != nil { + if err := pgLockRole(txn, db, owner); err != nil { return err } @@ -170,7 +170,7 @@ func resourcePostgreSQLDefaultPrivilegesCreate(db *DBConnection, d *schema.Resou } defer deferredRollback(txn) - return readRoleDefaultPrivileges(txn, d) + return readRoleDefaultPrivileges(txn, d, db) } func resourcePostgreSQLDefaultPrivilegesDelete(db *DBConnection, d *schema.ResourceData) error { @@ -191,7 +191,7 @@ func resourcePostgreSQLDefaultPrivilegesDelete(db *DBConnection, d *schema.Resou } defer deferredRollback(txn) - if err := pgLockRole(txn, owner); err != nil { + if err := pgLockRole(txn, db, owner); err != nil { return err } @@ -209,14 +209,14 @@ func resourcePostgreSQLDefaultPrivilegesDelete(db *DBConnection, d *schema.Resou return nil } -func readRoleDefaultPrivileges(txn *sql.Tx, d *schema.ResourceData) error { +func readRoleDefaultPrivileges(txn *sql.Tx, d *schema.ResourceData, db *DBConnection) error { role := d.Get("role").(string) owner := d.Get("owner").(string) pgSchema := d.Get("schema").(string) objectType := d.Get("object_type").(string) privilegesInput := d.Get("privileges").(*schema.Set).List() - if err := pgLockRole(txn, owner); err != nil { + if err := pgLockRole(txn, db, owner); err != nil { return err } @@ -227,8 +227,14 @@ func readRoleDefaultPrivileges(txn *sql.Tx, d *schema.ResourceData) error { var query string var queryArgs []interface{} - - if pgSchema != "" { + if !db.featureSupported(fetureAclExplode) { + var inSchema string + // If a schema is specified we need to build the part of the query string to action this + if pgSchema != "" { + inSchema = fmt.Sprintf("IN SCHEMA %s", pq.QuoteIdentifier(pgSchema)) + } + query = fmt.Sprintf("with a as (show DEFAULT PRIVILEGES for role %s %s) select array_agg(privilege_type) from a where grantee = '%s' and object_type = '%ss';", owner, inSchema, role, objectType) + } else if pgSchema != "" { query = `SELECT array_agg(prtype) FROM ( SELECT defaclnamespace, (aclexplode(defaclacl)).* FROM pg_default_acl WHERE defaclobjtype = $3 @@ -250,8 +256,8 @@ func readRoleDefaultPrivileges(txn *sql.Tx, d *schema.ResourceData) error { // This query aggregates the list of default privileges type (prtype) // for the role (grantee), owner (grantor), schema (namespace name) // and the specified object type (defaclobjtype). - var privileges pq.ByteaArray + if err := txn.QueryRow( query, queryArgs..., ).Scan(&privileges); err != nil { @@ -306,7 +312,6 @@ func grantRoleDefaultPrivileges(txn *sql.Tx, d *schema.ResourceData) error { if d.Get("with_grant_option").(bool) { query = query + " WITH GRANT OPTION" } - _, err := txn.Exec( query, ) @@ -333,7 +338,6 @@ func revokeRoleDefaultPrivileges(txn *sql.Tx, d *schema.ResourceData) error { strings.ToUpper(d.Get("object_type").(string)), pq.QuoteIdentifier(d.Get("role").(string)), ) - if _, err := txn.Exec(query); err != nil { return fmt.Errorf("could not revoke default privileges: %w", err) } diff --git a/postgresql/resource_postgresql_grant.go b/postgresql/resource_postgresql_grant.go index f4a5a6cb..7a8e33a9 100644 --- a/postgresql/resource_postgresql_grant.go +++ b/postgresql/resource_postgresql_grant.go @@ -125,7 +125,7 @@ func resourcePostgreSQLGrantRead(db *DBConnection, d *schema.ResourceData) error } defer deferredRollback(txn) - return readRolePrivileges(txn, d) + return readRolePrivileges(txn, db, d) } func resourcePostgreSQLGrantCreate(db *DBConnection, d *schema.ResourceData) error { @@ -169,12 +169,12 @@ func resourcePostgreSQLGrantCreate(db *DBConnection, d *schema.ResourceData) err defer deferredRollback(txn) role := d.Get("role").(string) - if err := pgLockRole(txn, role); err != nil { + if err := pgLockRole(txn, db, role); err != nil { return err } if objectType == "database" { - if err := pgLockDatabase(txn, database); err != nil { + if err := pgLockDatabase(txn, db, database); err != nil { return err } } @@ -210,7 +210,7 @@ func resourcePostgreSQLGrantCreate(db *DBConnection, d *schema.ResourceData) err } defer deferredRollback(txn) - return readRolePrivileges(txn, d) + return readRolePrivileges(txn, db, d) } func resourcePostgreSQLGrantDelete(db *DBConnection, d *schema.ResourceData) error { @@ -226,13 +226,13 @@ func resourcePostgreSQLGrantDelete(db *DBConnection, d *schema.ResourceData) err defer deferredRollback(txn) role := d.Get("role").(string) - if err := pgLockRole(txn, role); err != nil { + if err := pgLockRole(txn, db, role); err != nil { return err } objectType := d.Get("object_type").(string) if objectType == "database" { - if err := pgLockDatabase(txn, database); err != nil { + if err := pgLockDatabase(txn, db, database); err != nil { return err } } @@ -255,16 +255,21 @@ func resourcePostgreSQLGrantDelete(db *DBConnection, d *schema.ResourceData) err return nil } -func readDatabaseRolePriviges(txn *sql.Tx, d *schema.ResourceData, roleOID uint32) error { +func readDatabaseRolePriviges(txn *sql.Tx, db *DBConnection, d *schema.ResourceData, roleOID uint32, role string) error { dbName := d.Get("database").(string) - query := ` + var query string + //cockroachdb does not support aclexplode + if !db.featureSupported(fetureAclExplode) { + query = fmt.Sprintf(`with a as (show grants on database %s for %s) select array_agg(privilege_type) from a`, dbName, role) + } else { + query = ` SELECT array_agg(privilege_type) FROM ( SELECT (aclexplode(datacl)).* FROM pg_database WHERE datname=$1 ) as privileges WHERE grantee = $2 ` - + } var privileges pq.ByteaArray if err := txn.QueryRow(query, dbName, roleOID).Scan(&privileges); err != nil { return fmt.Errorf("could not read privileges for database %s: %w", dbName, err) @@ -274,15 +279,20 @@ WHERE grantee = $2 return nil } -func readSchemaRolePriviges(txn *sql.Tx, d *schema.ResourceData, roleOID uint32) error { +func readSchemaRolePriviges(txn *sql.Tx, db *DBConnection, d *schema.ResourceData, roleOID uint32, role string) error { dbName := d.Get("schema").(string) - query := ` + var query string + if db.featureSupported(fetureAclExplode) { + query = fmt.Sprintf(`with a as ( show grants on schema %s for %s) select array_agg(privilege_type) from a;`, dbName, role) + } else { + query = ` SELECT array_agg(privilege_type) FROM ( SELECT (aclexplode(nspacl)).* FROM pg_namespace WHERE nspname=$1 ) as privileges WHERE grantee = $2 ` + } var privileges pq.ByteaArray if err := txn.QueryRow(query, dbName, roleOID).Scan(&privileges); err != nil { @@ -411,7 +421,7 @@ ORDER BY col_privs.attname return nil } -func readRolePrivileges(txn *sql.Tx, d *schema.ResourceData) error { +func readRolePrivileges(txn *sql.Tx, db *DBConnection, d *schema.ResourceData) error { role := d.Get("role").(string) objectType := d.Get("object_type").(string) objects := d.Get("objects").(*schema.Set) @@ -426,10 +436,10 @@ func readRolePrivileges(txn *sql.Tx, d *schema.ResourceData) error { switch objectType { case "database": - return readDatabaseRolePriviges(txn, d, roleOID) + return readDatabaseRolePriviges(txn, db, d, roleOID, role) case "schema": - return readSchemaRolePriviges(txn, d, roleOID) + return readSchemaRolePriviges(txn, db, d, roleOID, role) case "foreign_data_wrapper": return readForeignDataWrapperRolePrivileges(txn, d, roleOID) diff --git a/postgresql/resource_postgresql_role.go b/postgresql/resource_postgresql_role.go index b7cb0fab..7dc937fb 100644 --- a/postgresql/resource_postgresql_role.go +++ b/postgresql/resource_postgresql_role.go @@ -194,29 +194,33 @@ func resourcePostgreSQLRoleCreate(db *DBConnection, d *schema.ResourceData) erro intOpts := []struct { hclKey string sqlKey string - }{ - {roleConnLimitAttr, "CONNECTION LIMIT"}, + }{} + if db.featureSupported(fetureRoleConnectionLimit) { + intOpts = append(intOpts, struct { + hclKey string + sqlKey string + }{roleConnLimitAttr, "CONNECTION LIMIT"}) } - type boolOptType struct { hclKey string sqlKeyEnable string sqlKeyDisable string } boolOpts := []boolOptType{ - {roleSuperuserAttr, "SUPERUSER", "NOSUPERUSER"}, {roleCreateDBAttr, "CREATEDB", "NOCREATEDB"}, {roleCreateRoleAttr, "CREATEROLE", "NOCREATEROLE"}, - {roleInheritAttr, "INHERIT", "NOINHERIT"}, {roleLoginAttr, "LOGIN", "NOLOGIN"}, - // roleEncryptedPassAttr is used only when rolePasswordAttr is set. - // {roleEncryptedPassAttr, "ENCRYPTED", "UNENCRYPTED"}, } + if db.featureSupported(fetureRoleSuperuser) { + boolOpts = append(boolOpts, boolOptType{roleSuperuserAttr, "SUPERUSER", "NOSUPERUSER"}) + } + if db.featureSupported(featureRoleroleInherit) { + boolOpts = append(boolOpts, boolOptType{roleInheritAttr, "INHERIT", "NOINHERIT"}) + } if db.featureSupported(featureRLS) { boolOpts = append(boolOpts, boolOptType{roleBypassRLSAttr, "BYPASSRLS", "NOBYPASSRLS"}) } - if db.featureSupported(featureReplication) { boolOpts = append(boolOpts, boolOptType{roleReplicationAttr, "REPLICATION", "NOREPLICATION"}) } @@ -235,7 +239,7 @@ func resourcePostgreSQLRoleCreate(db *DBConnection, d *schema.ResourceData) erro case opt.hclKey == rolePasswordAttr: if strings.ToUpper(v.(string)) == "NULL" { createOpts = append(createOpts, "PASSWORD NULL") - } else { + } else if db.featureSupported(fetureRoleEncryptedPass) { if d.Get(roleEncryptedPassAttr).(bool) { createOpts = append(createOpts, "ENCRYPTED") } else { @@ -246,7 +250,13 @@ func resourcePostgreSQLRoleCreate(db *DBConnection, d *schema.ResourceData) erro case opt.hclKey == roleValidUntilAttr: switch { case v.(string) == "", strings.ToLower(v.(string)) == "infinity": - createOpts = append(createOpts, fmt.Sprintf("%s '%s'", opt.sqlKey, "infinity")) + //temp fix for cockroachdb valid until bug fixed + //https://github.com/cockroachdb/cockroach/issues/116714 + if db.dbType == dbTypeCockroachdb { + createOpts = append(createOpts, fmt.Sprintf("%s '%s'", opt.sqlKey, "294276-12-31 23:59:59")) + } else { + createOpts = append(createOpts, fmt.Sprintf("%s '%s'", opt.sqlKey, "infinity")) + } default: createOpts = append(createOpts, fmt.Sprintf("%s '%s'", opt.sqlKey, pqQuoteLiteral(val))) } @@ -322,14 +332,13 @@ func resourcePostgreSQLRoleCreate(db *DBConnection, d *schema.ResourceData) erro func resourcePostgreSQLRoleDelete(db *DBConnection, d *schema.ResourceData) error { roleName := d.Get(roleNameAttr).(string) - txn, err := startTransaction(db.client, "") if err != nil { return err } defer deferredRollback(txn) - if err := pgLockRole(txn, roleName); err != nil { + if err := pgLockRole(txn, db, roleName); err != nil { return err } @@ -339,9 +348,16 @@ func resourcePostgreSQLRoleDelete(db *DBConnection, d *schema.ResourceData) erro if _, err := txn.Exec(fmt.Sprintf("REASSIGN OWNED BY %s TO %s", pq.QuoteIdentifier(roleName), pq.QuoteIdentifier(currentUser))); err != nil { return fmt.Errorf("could not reassign owned by role %s to %s: %w", roleName, currentUser, err) } - - if _, err := txn.Exec(fmt.Sprintf("DROP OWNED BY %s", pq.QuoteIdentifier(roleName))); err != nil { - return fmt.Errorf("could not drop owned by role %s: %w", roleName, err) + //cockroach does not support schema changes within explicit transactions + // https://www.cockroachlabs.com/docs/v23.1/online-schema-changes + if db.dbType == dbTypeCockroachdb { + if _, err := db.Exec(fmt.Sprintf("DROP OWNED BY %s", pq.QuoteIdentifier(roleName))); err != nil { + return fmt.Errorf("could not drop owned by role %s: %w", roleName, err) + } + } else { + if _, err := txn.Exec(fmt.Sprintf("DROP OWNED BY %s", pq.QuoteIdentifier(roleName))); err != nil { + return fmt.Errorf("could not drop owned by role %s: %w", roleName, err) + } } return nil }); err != nil { @@ -616,7 +632,7 @@ func resourcePostgreSQLRoleUpdate(db *DBConnection, d *schema.ResourceData) erro defer deferredRollback(txn) oldName, _ := d.GetChange(roleNameAttr) - if err := pgLockRole(txn, oldName.(string)); err != nil { + if err := pgLockRole(txn, db, oldName.(string)); err != nil { return err } @@ -660,7 +676,7 @@ func resourcePostgreSQLRoleUpdate(db *DBConnection, d *schema.ResourceData) erro return err } - if err := setRoleValidUntil(txn, d); err != nil { + if err := setRoleValidUntil(txn, d, db); err != nil { return err } @@ -887,7 +903,7 @@ func setRoleSuperuser(txn *sql.Tx, d *schema.ResourceData) error { return nil } -func setRoleValidUntil(txn *sql.Tx, d *schema.ResourceData) error { +func setRoleValidUntil(txn *sql.Tx, d *schema.ResourceData, db *DBConnection) error { if !d.HasChange(roleValidUntilAttr) { return nil } @@ -896,7 +912,13 @@ func setRoleValidUntil(txn *sql.Tx, d *schema.ResourceData) error { if validUntil == "" { return nil } else if strings.ToLower(validUntil) == "infinity" { - validUntil = "infinity" + //temp fix for cockroachdb valid until bug fixed + //https://github.com/cockroachdb/cockroach/issues/116714 + if db.dbType == dbTypeCockroachdb { + validUntil = "294276-12-31 23:59:59" + } else { + validUntil = "infinity" + } } roleName := d.Get(roleNameAttr).(string) diff --git a/postgresql/resource_postgresql_schema.go b/postgresql/resource_postgresql_schema.go index d06ed816..c0a92bb1 100644 --- a/postgresql/resource_postgresql_schema.go +++ b/postgresql/resource_postgresql_schema.go @@ -239,39 +239,45 @@ func resourcePostgreSQLSchemaDelete(db *DBConnection, d *schema.ResourceData) er schemaName := d.Get(schemaNameAttr).(string) - exists, err := schemaExists(txn, schemaName) - if err != nil { - return err - } - if !exists { - d.SetId("") - return nil - } + if schemaName != "public" { + + exists, err := schemaExists(txn, schemaName) + if err != nil { + return err + } + if !exists { + d.SetId("") + return nil + } + + owner := d.Get("owner").(string) + + if err = withRolesGranted(txn, []string{owner}, func() error { + dropMode := "RESTRICT" + if d.Get(schemaDropCascade).(bool) { + dropMode = "CASCADE" + } - owner := d.Get("owner").(string) + sql := fmt.Sprintf("DROP SCHEMA %s %s", pq.QuoteIdentifier(schemaName), dropMode) + if _, err = txn.Exec(sql); err != nil { + return fmt.Errorf("Error deleting schema: %w", err) + } - if err = withRolesGranted(txn, []string{owner}, func() error { - dropMode := "RESTRICT" - if d.Get(schemaDropCascade).(bool) { - dropMode = "CASCADE" + return nil + }); err != nil { + return err } - sql := fmt.Sprintf("DROP SCHEMA %s %s", pq.QuoteIdentifier(schemaName), dropMode) - if _, err = txn.Exec(sql); err != nil { - return fmt.Errorf("Error deleting schema: %w", err) + if err := txn.Commit(); err != nil { + return fmt.Errorf("Error committing schema: %w", err) } - return nil - }); err != nil { - return err - } + d.SetId("") - if err := txn.Commit(); err != nil { - return fmt.Errorf("Error committing schema: %w", err) + } else { + log.Printf("cannot delete schema %s", schemaName) } - d.SetId("") - return nil } @@ -322,7 +328,12 @@ func resourcePostgreSQLSchemaReadImpl(db *DBConnection, d *schema.ResourceData) var schemaOwner string var schemaACLs []string - err = txn.QueryRow("SELECT pg_catalog.pg_get_userbyid(n.nspowner), COALESCE(n.nspacl, '{}'::aclitem[])::TEXT[] FROM pg_catalog.pg_namespace n WHERE n.nspname=$1", schemaName).Scan(&schemaOwner, pq.Array(&schemaACLs)) + + if !db.featureSupported(fetureAclItem) { + err = txn.QueryRow("SELECT pg_catalog.pg_get_userbyid(n.nspowner) FROM pg_catalog.pg_namespace n WHERE n.nspname=$1", schemaName).Scan(&schemaOwner) + } else { + err = txn.QueryRow("SELECT pg_catalog.pg_get_userbyid(n.nspowner), COALESCE(n.nspacl, '{}'::aclitem[])::TEXT[] FROM pg_catalog.pg_namespace n WHERE n.nspname=$1", schemaName).Scan(&schemaOwner, pq.Array(&schemaACLs)) + } switch { case err == sql.ErrNoRows: log.Printf("[WARN] PostgreSQL schema (%s) not found in database %s", schemaName, database) @@ -332,26 +343,28 @@ func resourcePostgreSQLSchemaReadImpl(db *DBConnection, d *schema.ResourceData) return fmt.Errorf("Error reading schema: %w", err) default: type RoleKey string - schemaPolicies := make(map[RoleKey]acl.Schema, len(schemaACLs)) - for _, aclStr := range schemaACLs { - aclItem, err := acl.Parse(aclStr) - if err != nil { - return fmt.Errorf("Error parsing aclitem: %w", err) - } - - schemaACL, err := acl.NewSchema(aclItem) - if err != nil { - return fmt.Errorf("invalid perms for schema: %w", err) - } - - roleKey := RoleKey(strings.ToLower(schemaACL.Role)) - var mergedPolicy acl.Schema - if existingRolePolicy, ok := schemaPolicies[roleKey]; ok { - mergedPolicy = existingRolePolicy.Merge(schemaACL) - } else { - mergedPolicy = schemaACL + if db.featureSupported(fetureAclItem) { + schemaPolicies := make(map[RoleKey]acl.Schema, len(schemaACLs)) + for _, aclStr := range schemaACLs { + aclItem, err := acl.Parse(aclStr) + if err != nil { + return fmt.Errorf("Error parsing aclitem: %w", err) + } + + schemaACL, err := acl.NewSchema(aclItem) + if err != nil { + return fmt.Errorf("invalid perms for schema: %w", err) + } + + roleKey := RoleKey(strings.ToLower(schemaACL.Role)) + var mergedPolicy acl.Schema + if existingRolePolicy, ok := schemaPolicies[roleKey]; ok { + mergedPolicy = existingRolePolicy.Merge(schemaACL) + } else { + mergedPolicy = schemaACL + } + schemaPolicies[roleKey] = mergedPolicy } - schemaPolicies[roleKey] = mergedPolicy } d.Set(schemaNameAttr, schemaName)