diff --git a/docs/resources/datashare.md b/docs/resources/datashare.md index e94a13c..10e5441 100644 --- a/docs/resources/datashare.md +++ b/docs/resources/datashare.md @@ -35,6 +35,13 @@ resource "redshift_datashare" "my_datashare" { "public", "other", ] + # Optional. Specifies which schema tables to expose to the datashare. + schema_tables = [ + "schema1.table1", + "schema1.table2", + "schema2.table1", + "schema2.table2", + ] } ``` @@ -50,6 +57,7 @@ resource "redshift_datashare" "my_datashare" { - **id** (String) The ID of this resource. - **owner** (String) The user who owns the datashare. - **publicly_accessible** (Boolean) Specifies whether the datashare can be shared to clusters that are publicly accessible. Default is `false`. +- **schema_tables** (Set of String) Defines which schema tables are exposed to the data share. - **schemas** (Set of String) Defines which schemas are exposed to the data share. ### Read-Only diff --git a/examples/resources/redshift_datashare/resource.tf b/examples/resources/redshift_datashare/resource.tf index 0ae9ecd..1cb46df 100644 --- a/examples/resources/redshift_datashare/resource.tf +++ b/examples/resources/redshift_datashare/resource.tf @@ -8,4 +8,11 @@ resource "redshift_datashare" "my_datashare" { "public", "other", ] + # Optional. Specifies which schema tables to expose to the datashare. + schema_tables = [ + "schema1.table1", + "schema1.table2", + "schema2.table1", + "schema2.table2", + ] } diff --git a/go.mod b/go.mod index f25bc8c..c6194ec 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 // indirect github.com/aws/aws-sdk-go v1.25.3 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.3.0 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.3.0 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.2.0 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.1.0 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.2.0 // indirect diff --git a/go.sum b/go.sum index af770ab..8740bb9 100644 --- a/go.sum +++ b/go.sum @@ -51,6 +51,7 @@ github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/andybalholm/crlf v0.0.0-20171020200849-670099aa064f/go.mod h1:k8feO4+kXDxro6ErPXBRTJ/ro2mf0SsFG8s7doP9kJE= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/apparentlymart/go-cidr v1.0.1 h1:NmIwLZ/KdsjIUlhf+/Np40atNXm/+lZ5txfTJ/SpF+U= github.com/apparentlymart/go-cidr v1.0.1/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= @@ -64,6 +65,7 @@ github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6 github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26FmByPp2eC2qla6E1Tw+scpcg/to= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM= github.com/aws/aws-sdk-go v1.25.3 h1:uM16hIw9BotjZKMZlX05SN2EFtaWfi/NonPKIARiBLQ= @@ -111,13 +113,16 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= github.com/go-git/go-billy/v5 v5.3.1 h1:CPiOUAzKtMRvolEKw+bG1PLRpT7D3LIs3/3ey4Aiu34= github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= +github.com/go-git/go-git-fixtures/v4 v4.2.1 h1:n9gGL1Ct/yIw+nfsfr8s4+sbhT+Ncu2SubfXjIWgci8= github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0= github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4= github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc= @@ -277,6 +282,7 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= @@ -315,6 +321,7 @@ github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce/go.mod h1:uFMI8w+ref4 github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/redshift/helpers.go b/redshift/helpers.go index 55ab625..391a225 100644 --- a/redshift/helpers.go +++ b/redshift/helpers.go @@ -203,3 +203,12 @@ func setToPgIdentList(identifiers *schema.Set, prefix string) string { return strings.Join(quoted, ",") } + +func contains(s []string, e string) bool { + for _, a := range s { + if a == e { + return true + } + } + return false +} diff --git a/redshift/resource_redshift_datashare.go b/redshift/resource_redshift_datashare.go index 7670ff7..a72fe9a 100644 --- a/redshift/resource_redshift_datashare.go +++ b/redshift/resource_redshift_datashare.go @@ -18,6 +18,7 @@ const ( dataShareProducerNamespaceAttr = "producer_namespace" dataShareCreatedAttr = "created" dataShareSchemasAttr = "schemas" + dataShareSchemaTablesAttr = "schema_tables" ) func redshiftDatashare() *schema.Resource { @@ -92,6 +93,18 @@ such as RA3. }, }, }, + dataShareSchemaTablesAttr: { + Type: schema.TypeSet, + Optional: true, + Description: "Defines which schema tables are exposed to the data share.", + Set: schema.HashString, + Elem: &schema.Schema{ + Type: schema.TypeString, + StateFunc: func(val interface{}) string { + return strings.ToLower(val.(string)) + }, + }, + }, }, } } @@ -152,6 +165,20 @@ func resourceRedshiftDatashareCreate(db *DBConnection, d *schema.ResourceData) e } } + var schemas []string + var tables []string + for _, schemaTable := range d.Get(dataShareSchemaTablesAttr).(*schema.Set).List() { + schemaName := strings.Split(schemaTable.(string), ".")[0] + appendIfTrue(contains(schemas, schemaName), schemaName, &schemas) + tableName := schemaTable.(string) + appendIfTrue(contains(tables, tableName), tableName, &tables) + } + + err = addSchemaTablesToDatashare(tx, shareName, schemas, tables) + if err != nil { + return err + } + if err = tx.Commit(); err != nil { return fmt.Errorf("could not commit transaction: %w", err) } @@ -172,6 +199,19 @@ func addSchemaToDatashare(tx *sql.Tx, shareName string, schemaName string) error return err } +func addSchemaTablesToDatashare(tx *sql.Tx, shareName string, schemas []string, tables []string) error { + schemasString := strings.Join(schemas, ",") + tablesString := strings.Join(tables, ",") + err := resourceRedshiftDatashareAddTablesInSchema(tx, shareName, schemasString, tablesString) + return err +} + +func removeSchemaTablesFromDatashare(tx *sql.Tx, shareName string, tables []string) error { + tablesString := strings.Join(tables, ",") + err := resourceRedshiftDatashareRemoveTablesInSchema(tx, shareName, tablesString) + return err +} + func resourceRedshiftDatashareAddSchema(tx *sql.Tx, shareName string, schemaName string) error { query := fmt.Sprintf("ALTER DATASHARE %s ADD SCHEMA %s", pq.QuoteIdentifier(shareName), pq.QuoteIdentifier(schemaName)) log.Printf("[DEBUG] %s\n", query) @@ -208,6 +248,35 @@ func resourceRedshiftDatashareAddAllTables(tx *sql.Tx, shareName string, schemaN return err } +func resourceRedshiftDatashareAddTablesInSchema(tx *sql.Tx, shareName string, schemas string, tables string) error { + query := fmt.Sprintf("ALTER DATASHARE %s ADD SCHEMA %s", pq.QuoteIdentifier(shareName), pq.QuoteIdentifier(schemas)) + log.Printf("[DEBUG] %s\n", query) + _, err := tx.Exec(query) + if err != nil { + // if the schema is already in the datashare we get a "duplicate schema" error code. This is fine. + if pqErr, ok := err.(*pq.Error); ok { + if string(pqErr.Code) == pqErrorCodeDuplicateSchema { + log.Printf("[WARN] Schema %s already exists in datashare %s\n", schemas, shareName) + } else { + return err + } + } else { + return err + } + } + query = fmt.Sprintf("ALTER DATASHARE %s ADD TABLE %s", pq.QuoteIdentifier(shareName), pq.QuoteIdentifier(tables)) + log.Printf("[DEBUG] %s\n", query) + _, err = tx.Exec(query) + return err +} + +func resourceRedshiftDatashareRemoveTablesInSchema(tx *sql.Tx, shareName string, tables string) error { + query := fmt.Sprintf("ALTER DATASHARE %s REMOVE TABLE %s", pq.QuoteIdentifier(shareName), pq.QuoteIdentifier(tables)) + log.Printf("[DEBUG] %s\n", query) + _, err := tx.Exec(query) + return err +} + func removeSchemaFromDatashare(tx *sql.Tx, shareName string, schemaName string) error { err := resourceRedshiftDatashareRemoveAllFunctions(tx, shareName, schemaName) if err != nil { @@ -293,6 +362,10 @@ func resourceRedshiftDatashareRead(db *DBConnection, d *schema.ResourceData) err return err } + if err = readDatashareSchemaTables(tx, shareName, d); err != nil { + return err + } + if err = tx.Commit(); err != nil { return err } @@ -328,6 +401,34 @@ func readDatashareSchemas(tx *sql.Tx, shareName string, d *schema.ResourceData) return nil } +func readDatashareSchemaTables(tx *sql.Tx, shareName string, d *schema.ResourceData) error { + query := ` + SELECT + object_name + FROM svv_datashare_objects + WHERE share_type = 'OUTBOUND' + AND object_type = 'table' + AND share_name = $1 +` + log.Printf("[DEBUG] %s, $1=%s\n", query, shareName) + rows, err := tx.Query(query, shareName) + if err != nil { + return err + } + defer rows.Close() + + schemaTables := schema.NewSet(schema.HashString, nil) + for rows.Next() { + var schemaTableName string + if err = rows.Scan(&schemaTableName); err != nil { + return err + } + schemaTables.Add(schemaTableName) + } + d.Set(dataShareSchemaTablesAttr, schemaTables) + return nil +} + func resourceRedshiftDatashareUpdate(db *DBConnection, d *schema.ResourceData) error { tx, err := startTransaction(db.client, "") if err != nil { @@ -347,6 +448,10 @@ func resourceRedshiftDatashareUpdate(db *DBConnection, d *schema.ResourceData) e return err } + if err := setDatashareTableSchemas(tx, d); err != nil { + return err + } + if err = tx.Commit(); err != nil { return fmt.Errorf("could not commit transaction: %w", err) } @@ -420,6 +525,48 @@ func setDatashareSchemas(tx *sql.Tx, d *schema.ResourceData) error { return nil } +func setDatashareTableSchemas(tx *sql.Tx, d *schema.ResourceData) error { + if !d.HasChange(dataShareSchemaTablesAttr) { + return nil + } + before, after := d.GetChange(dataShareSchemaTablesAttr) + if before == nil { + before = schema.NewSet(schema.HashString, nil) + } + if after == nil { + after = schema.NewSet(schema.HashString, nil) + } + + add := after.(*schema.Set).Difference(before.(*schema.Set)) + remove := before.(*schema.Set).Difference(after.(*schema.Set)) + + shareName := d.Get(dataShareSchemaTablesAttr).(string) + + var schemas []string + var tables []string + for _, schemaTable := range add.List() { + schemaName := strings.Split(schemaTable.(string), ".")[0] + appendIfTrue(contains(schemas, schemaName), schemaName, &schemas) + tableName := schemaTable.(string) + appendIfTrue(contains(tables, tableName), tableName, &tables) + } + err := addSchemaTablesToDatashare(tx, shareName, schemas, tables) + if err != nil { + return err + } + + var tablesToBeRemoved []string + for _, schemaTable := range remove.List() { + tableName := schemaTable.(string) + appendIfTrue(contains(tablesToBeRemoved, tableName), tableName, &tablesToBeRemoved) + } + if err := removeSchemaTablesFromDatashare(tx, shareName, tablesToBeRemoved); err != nil { + return err + } + + return nil +} + func resourceRedshiftDatashareDelete(db *DBConnection, d *schema.ResourceData) error { tx, err := startTransaction(db.client, "") if err != nil { diff --git a/redshift/resource_redshift_datashare_test.go b/redshift/resource_redshift_datashare_test.go index 84d2992..b838048 100644 --- a/redshift/resource_redshift_datashare_test.go +++ b/redshift/resource_redshift_datashare_test.go @@ -28,11 +28,12 @@ resource "redshift_user" "user" { resource "redshift_datashare" "basic" { %[5]s = %[2]q + %[6]s = ["schema1.table1"] depends_on = [ redshift_user.user, ] } -`, schemaNameAttr, shareName, schemaCascadeOnDeleteAttr, userNameAttr, dataShareNameAttr) +`, schemaNameAttr, shareName, schemaCascadeOnDeleteAttr, userNameAttr, dataShareNameAttr, dataShareSchemaTablesAttr) configUpdate := fmt.Sprintf(` resource "redshift_schema" "schema" { @@ -51,8 +52,9 @@ resource "redshift_datashare" "basic" { %[8]s = [ redshift_schema.schema.%[1]s, ] + %[9]s = ["schema1.table1", "schema1.table2", "schema2.table1", "schema2.table2"] } -`, schemaNameAttr, shareName, schemaCascadeOnDeleteAttr, userNameAttr, dataShareNameAttr, dataShareOwnerAttr, dataSharePublicAccessibleAttr, dataShareSchemasAttr) +`, schemaNameAttr, shareName, schemaCascadeOnDeleteAttr, userNameAttr, dataShareNameAttr, dataShareOwnerAttr, dataSharePublicAccessibleAttr, dataShareSchemasAttr, dataShareSchemaTablesAttr) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, @@ -69,6 +71,8 @@ resource "redshift_datashare" "basic" { resource.TestCheckResourceAttrSet("redshift_datashare.basic", dataShareProducerNamespaceAttr), resource.TestCheckResourceAttrSet("redshift_datashare.basic", dataShareCreatedAttr), resource.TestCheckResourceAttr("redshift_datashare.basic", fmt.Sprintf("%s.#", dataShareSchemasAttr), "0"), + resource.TestCheckResourceAttr("redshift_datashare.basic", fmt.Sprintf("%s.#", dataShareSchemaTablesAttr), "1"), + resource.TestCheckTypeSetElemAttr("redshift_datashare.basic", fmt.Sprintf("%s.*", dataShareSchemaTablesAttr), shareName), ), }, { @@ -83,6 +87,8 @@ resource "redshift_datashare" "basic" { resource.TestCheckResourceAttrSet("redshift_datashare.basic", dataShareCreatedAttr), resource.TestCheckResourceAttr("redshift_datashare.basic", fmt.Sprintf("%s.#", dataShareSchemasAttr), "1"), resource.TestCheckTypeSetElemAttr("redshift_datashare.basic", fmt.Sprintf("%s.*", dataShareSchemasAttr), shareName), + resource.TestCheckResourceAttr("redshift_datashare.basic", fmt.Sprintf("%s.#", dataShareSchemaTablesAttr), "4"), + resource.TestCheckTypeSetElemAttr("redshift_datashare.basic", fmt.Sprintf("%s.*", dataShareSchemaTablesAttr), shareName), ), }, { @@ -96,6 +102,8 @@ resource "redshift_datashare" "basic" { resource.TestCheckResourceAttrSet("redshift_datashare.basic", dataShareProducerNamespaceAttr), resource.TestCheckResourceAttrSet("redshift_datashare.basic", dataShareCreatedAttr), resource.TestCheckResourceAttr("redshift_datashare.basic", fmt.Sprintf("%s.#", dataShareSchemasAttr), "0"), + resource.TestCheckResourceAttr("redshift_datashare.basic", fmt.Sprintf("%s.#", dataShareSchemaTablesAttr), "1"), + resource.TestCheckTypeSetElemAttr("redshift_datashare.basic", fmt.Sprintf("%s.*", dataShareSchemaTablesAttr), shareName), ), }, {