diff --git a/postgresql/helpers.go b/postgresql/helpers.go index 8ccf39d4..0c31674b 100644 --- a/postgresql/helpers.go +++ b/postgresql/helpers.go @@ -387,6 +387,18 @@ func schemaExists(txn *sql.Tx, schemaname string) (bool, error) { return true, nil } +func schemaExistsWithDB(db *DBConnection, schemaname string) (bool, error) { + err := db.QueryRow("SELECT 1 FROM pg_namespace WHERE nspname=$1", schemaname).Scan(&schemaname) + switch { + case err == sql.ErrNoRows: + return false, nil + case err != nil: + return false, fmt.Errorf("could not check if schema exists: %w", err) + } + + return true, nil +} + func getCurrentUser(db QueryAble) (string, error) { var currentUser string err := db.QueryRow("SELECT CURRENT_USER").Scan(¤tUser) diff --git a/postgresql/resource_postgresql_default_privileges.go b/postgresql/resource_postgresql_default_privileges.go index be52719b..be3ca65e 100644 --- a/postgresql/resource_postgresql_default_privileges.go +++ b/postgresql/resource_postgresql_default_privileges.go @@ -96,6 +96,12 @@ func resourcePostgreSQLDefaultPrivilegesRead(db *DBConnection, d *schema.Resourc return nil } + // CockroachDB does not support certain DDL operations within explicit transactions + // https://www.cockroachlabs.com/docs/v23.1/online-schema-changes + if db.dbType == dbTypeCockroachdb { + return readRoleDefaultPrivilegesWithDB(db, d) + } + txn, err := startTransaction(db.client, d.Get("database").(string)) if err != nil { return err @@ -130,6 +136,19 @@ func resourcePostgreSQLDefaultPrivilegesCreate(db *DBConnection, d *schema.Resou database := d.Get("database").(string) owner := d.Get("owner").(string) + // CockroachDB does not support certain DDL operations within explicit transactions + // https://www.cockroachlabs.com/docs/v23.1/online-schema-changes + if db.dbType == dbTypeCockroachdb { + if err := revokeRoleDefaultPrivilegesWithDB(db, d); err != nil { + return err + } + if err := grantRoleDefaultPrivilegesWithDB(db, d); err != nil { + return err + } + d.SetId(generateDefaultPrivilegesID(d)) + return readRoleDefaultPrivilegesWithDB(db, d) + } + txn, err := startTransaction(db.client, database) if err != nil { return err @@ -185,6 +204,15 @@ func resourcePostgreSQLDefaultPrivilegesDelete(db *DBConnection, d *schema.Resou ) } + // CockroachDB does not support certain DDL operations within explicit transactions + // https://www.cockroachlabs.com/docs/v23.1/online-schema-changes + if db.dbType == dbTypeCockroachdb { + if err := revokeRoleDefaultPrivilegesWithDB(db, d); err != nil { + return err + } + return nil + } + txn, err := startTransaction(db.client, d.Get("database").(string)) if err != nil { return err @@ -356,3 +384,105 @@ func generateDefaultPrivilegesID(d *schema.ResourceData) string { }, "_") } + +// grantRoleDefaultPrivilegesWithDB grants default privileges outside of a transaction for CockroachDB +func grantRoleDefaultPrivilegesWithDB(db *DBConnection, d *schema.ResourceData) error { + role := d.Get("role").(string) + pgSchema := d.Get("schema").(string) + + privileges := []string{} + for _, priv := range d.Get("privileges").(*schema.Set).List() { + privileges = append(privileges, priv.(string)) + } + + if len(privileges) == 0 { + log.Printf("[DEBUG] no default privileges to grant for role %s, owner %s in database: %s,", d.Get("role").(string), d.Get("owner").(string), d.Get("database").(string)) + return nil + } + + 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("ALTER DEFAULT PRIVILEGES FOR ROLE %s %s GRANT %s ON %sS TO %s", + pq.QuoteIdentifier(d.Get("owner").(string)), + inSchema, + strings.Join(privileges, ","), + strings.ToUpper(d.Get("object_type").(string)), + pq.QuoteIdentifier(role), + ) + + if d.Get("with_grant_option").(bool) { + query = query + " WITH GRANT OPTION" + } + _, err := db.Exec(query) + if err != nil { + return fmt.Errorf("could not alter default privileges: %w", err) + } + + return nil +} + +// revokeRoleDefaultPrivilegesWithDB revokes default privileges outside of a transaction for CockroachDB +func revokeRoleDefaultPrivilegesWithDB(db *DBConnection, d *schema.ResourceData) error { + pgSchema := d.Get("schema").(string) + + 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( + "ALTER DEFAULT PRIVILEGES FOR ROLE %s %s REVOKE ALL ON %sS FROM %s", + pq.QuoteIdentifier(d.Get("owner").(string)), + inSchema, + strings.ToUpper(d.Get("object_type").(string)), + pq.QuoteIdentifier(d.Get("role").(string)), + ) + if _, err := db.Exec(query); err != nil { + return fmt.Errorf("could not revoke default privileges: %w", err) + } + return nil +} + +// readRoleDefaultPrivilegesWithDB reads default privileges outside of a transaction for CockroachDB +func readRoleDefaultPrivilegesWithDB(db *DBConnection, d *schema.ResourceData) 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() + + var query string + 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)) + } + // CockroachDB uses SHOW DEFAULT PRIVILEGES instead of aclexplode + 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) + + var privileges pq.ByteaArray + if err := db.QueryRow(query).Scan(&privileges); err != nil { + return fmt.Errorf("could not read default privileges: %w", err) + } + + // We consider no privileges as "not exists" unless no privileges were provided as input + if len(privileges) == 0 { + log.Printf("[DEBUG] no default privileges for role %s in schema %s", role, pgSchema) + if len(privilegesInput) != 0 { + d.SetId("") + return nil + } + } + + privilegesSet := pgArrayToSet(privileges) + d.Set("privileges", privilegesSet) + d.SetId(generateDefaultPrivilegesID(d)) + + return nil +} diff --git a/postgresql/resource_postgresql_role.go b/postgresql/resource_postgresql_role.go index 4fd3f1cd..1e74c5b9 100644 --- a/postgresql/resource_postgresql_role.go +++ b/postgresql/resource_postgresql_role.go @@ -311,8 +311,16 @@ func resourcePostgreSQLRoleCreate(db *DBConnection, d *schema.ResourceData) erro } sql := fmt.Sprintf("CREATE ROLE %s%s", pq.QuoteIdentifier(roleName), createStr) - if _, err := txn.Exec(sql); err != nil { - return fmt.Errorf("error creating role %s: %w", roleName, err) + // CockroachDB does not support certain DDL operations within explicit transactions + // https://www.cockroachlabs.com/docs/v23.1/online-schema-changes + if db.dbType == dbTypeCockroachdb { + if _, err := db.Exec(sql); err != nil { + return fmt.Errorf("error creating role %s: %w", roleName, err) + } + } else { + if _, err := txn.Exec(sql); err != nil { + return fmt.Errorf("error creating role %s: %w", roleName, err) + } } if err = grantRoles(txn, d); err != nil { @@ -347,8 +355,12 @@ func resourcePostgreSQLRoleCreate(db *DBConnection, d *schema.ResourceData) erro } } - if err = txn.Commit(); err != nil { - return fmt.Errorf("could not commit transaction: %w", err) + // CockroachDB does not support certain DDL operations within explicit transactions + // Skip commit for CockroachDB as DDL was executed outside the transaction + if db.dbType != dbTypeCockroachdb { + if err = txn.Commit(); err != nil { + return fmt.Errorf("could not commit transaction: %w", err) + } } d.SetId(roleName) @@ -391,13 +403,25 @@ func resourcePostgreSQLRoleDelete(db *DBConnection, d *schema.ResourceData) erro } } if !d.Get(roleSkipDropRoleAttr).(bool) { - if _, err := txn.Exec(fmt.Sprintf("DROP ROLE %s", pq.QuoteIdentifier(roleName))); err != nil { - return fmt.Errorf("could not delete role %s: %w", roleName, err) + // CockroachDB does not support certain DDL operations within explicit transactions + // https://www.cockroachlabs.com/docs/v23.1/online-schema-changes + if db.dbType == dbTypeCockroachdb { + if _, err := db.Exec(fmt.Sprintf("DROP ROLE %s", pq.QuoteIdentifier(roleName))); err != nil { + return fmt.Errorf("could not delete role %s: %w", roleName, err) + } + } else { + if _, err := txn.Exec(fmt.Sprintf("DROP ROLE %s", pq.QuoteIdentifier(roleName))); err != nil { + return fmt.Errorf("could not delete role %s: %w", roleName, err) + } } } - if err := txn.Commit(); err != nil { - return fmt.Errorf("Error committing schema: %w", err) + // CockroachDB does not support certain DDL operations within explicit transactions + // Skip commit for CockroachDB as DDL was executed outside the transaction + if db.dbType != dbTypeCockroachdb { + if err := txn.Commit(); err != nil { + return fmt.Errorf("Error committing schema: %w", err) + } } d.SetId("") diff --git a/postgresql/resource_postgresql_schema.go b/postgresql/resource_postgresql_schema.go index c0a92bb1..b1c247a2 100644 --- a/postgresql/resource_postgresql_schema.go +++ b/postgresql/resource_postgresql_schema.go @@ -118,6 +118,17 @@ func resourcePostgreSQLSchema() *schema.Resource { func resourcePostgreSQLSchemaCreate(db *DBConnection, d *schema.ResourceData) error { database := getDatabase(d, db.client.databaseName) + + // CockroachDB does not support certain DDL operations within explicit transactions + // https://www.cockroachlabs.com/docs/v23.1/online-schema-changes + if db.dbType == dbTypeCockroachdb { + if err := createSchemaWithDB(db, d); err != nil { + return err + } + d.SetId(generateSchemaID(d, database)) + return resourcePostgreSQLSchemaReadImpl(db, d) + } + txn, err := startTransaction(db.client, database) if err != nil { return err @@ -228,8 +239,122 @@ func createSchema(db *DBConnection, txn *sql.Tx, d *schema.ResourceData) error { return nil } +// createSchemaWithDB creates a schema outside of a transaction for CockroachDB +func createSchemaWithDB(db *DBConnection, d *schema.ResourceData) error { + schemaName := d.Get(schemaNameAttr).(string) + + // Check if previous tasks haven't already created schema + var foundSchema bool + err := db.QueryRow(`SELECT TRUE FROM pg_catalog.pg_namespace WHERE nspname = $1`, schemaName).Scan(&foundSchema) + + queries := []string{} + switch { + case err == sql.ErrNoRows: + b := bytes.NewBufferString("CREATE SCHEMA ") + if db.featureSupported(featureSchemaCreateIfNotExist) { + if v := d.Get(schemaIfNotExists); v.(bool) { + fmt.Fprint(b, "IF NOT EXISTS ") + } + } + fmt.Fprint(b, pq.QuoteIdentifier(schemaName)) + + switch v, ok := d.GetOk(schemaOwnerAttr); { + case ok: + fmt.Fprint(b, " AUTHORIZATION ", pq.QuoteIdentifier(v.(string))) + } + queries = append(queries, b.String()) + + case err != nil: + return fmt.Errorf("Error looking for schema: %w", err) + + default: + // The schema already exists, we just set the owner. + if err := setSchemaOwnerWithDB(db, d); err != nil { + return err + } + } + + // ACL objects that can generate the necessary SQL + type RoleKey string + var schemaPolicies map[RoleKey]acl.Schema + + if policiesRaw, ok := d.GetOk(schemaPolicyAttr); ok { + policiesList := policiesRaw.(*schema.Set).List() + + schemaPolicies = make(map[RoleKey]acl.Schema, len(policiesList)) + + for _, policyRaw := range policiesList { + policyMap := policyRaw.(map[string]interface{}) + rolePolicy := schemaPolicyToACL(policyMap) + + roleKey := RoleKey(strings.ToLower(rolePolicy.Role)) + if existingRolePolicy, ok := schemaPolicies[roleKey]; ok { + schemaPolicies[roleKey] = existingRolePolicy.Merge(rolePolicy) + } else { + schemaPolicies[roleKey] = rolePolicy + } + } + } + + for _, policy := range schemaPolicies { + queries = append(queries, policy.Grants(schemaName)...) + } + + for _, query := range queries { + if _, err = db.Exec(query); err != nil { + return fmt.Errorf("Error creating schema %s: %w", schemaName, err) + } + } + + return nil +} + +// setSchemaOwnerWithDB sets the schema owner outside of a transaction for CockroachDB +func setSchemaOwnerWithDB(db *DBConnection, d *schema.ResourceData) error { + schemaName := d.Get(schemaNameAttr).(string) + owner := d.Get(schemaOwnerAttr).(string) + if owner == "" { + return nil + } + sql := fmt.Sprintf("ALTER SCHEMA %s OWNER TO %s", pq.QuoteIdentifier(schemaName), pq.QuoteIdentifier(owner)) + if _, err := db.Exec(sql); err != nil { + return fmt.Errorf("Error setting schema owner: %w", err) + } + return nil +} + func resourcePostgreSQLSchemaDelete(db *DBConnection, d *schema.ResourceData) error { database := getDatabase(d, db.client.databaseName) + schemaName := d.Get(schemaNameAttr).(string) + + // CockroachDB does not support certain DDL operations within explicit transactions + // https://www.cockroachlabs.com/docs/v23.1/online-schema-changes + if db.dbType == dbTypeCockroachdb { + if schemaName != "public" { + exists, err := schemaExistsWithDB(db, schemaName) + if err != nil { + return err + } + if !exists { + d.SetId("") + return nil + } + + dropMode := "RESTRICT" + if d.Get(schemaDropCascade).(bool) { + dropMode = "CASCADE" + } + + sql := fmt.Sprintf("DROP SCHEMA %s %s", pq.QuoteIdentifier(schemaName), dropMode) + if _, err = db.Exec(sql); err != nil { + return fmt.Errorf("Error deleting schema: %w", err) + } + d.SetId("") + } else { + log.Printf("cannot delete schema %s", schemaName) + } + return nil + } txn, err := startTransaction(db.client, database) if err != nil { @@ -237,8 +362,6 @@ func resourcePostgreSQLSchemaDelete(db *DBConnection, d *schema.ResourceData) er } defer deferredRollback(txn) - schemaName := d.Get(schemaNameAttr).(string) - if schemaName != "public" { exists, err := schemaExists(txn, schemaName)