diff --git a/README.md b/README.md index 91c9cab..de02bce 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,9 @@ Contrary to existing tools, Atlas intelligently plans schema migrations for you, * SQLite * TiDB -Link to Terraform documentation. +### Docs +* [Provider Docs](https://registry.terraform.io/providers/ariga/atlas/latest/docs) +* [Atlas Docs](https://atlasgo.io) ## Installation @@ -60,13 +62,13 @@ provider "atlas" {} ```terraform data "atlas_schema" "my_schema" { src = file("${path.module}/schema.hcl") - dev_db_url = "mysql://root:pass@localhost:3307/test" + dev_db_url = "mysql://root:pass@localhost:3307/example" } resource "atlas_schema" "example_db" { hcl = data.atlas_schema.my_schema.hcl - url = "mysql://root:pass@localhost:3306/test" - dev_db_url = "mysql://root:pass@localhost:3307/test" + url = "mysql://root:pass@localhost:3306/example" + dev_db_url = "mysql://root:pass@localhost:3307/example" } ``` diff --git a/atlas/resource_atlas_schema.go b/atlas/resource_atlas_schema.go index f4f4a64..cdc4b38 100644 --- a/atlas/resource_atlas_schema.go +++ b/atlas/resource_atlas_schema.go @@ -76,7 +76,11 @@ func readSchema(ctx context.Context, d *schema.ResourceData, m interface{}) diag if err != nil { return diag.FromErr(err) } - realm, err := cli.InspectRealm(ctx, nil) + var schemas []string + if cli.URL.Schema != "" { + schemas = append(schemas, cli.URL.Schema) + } + realm, err := cli.InspectRealm(ctx, &atlaschema.InspectRealmOption{Schemas: schemas}) if err != nil { return diag.FromErr(err) } diff --git a/atlas/resource_atlas_schema_test.go b/atlas/resource_atlas_schema_test.go index 323e3ee..56fddcd 100644 --- a/atlas/resource_atlas_schema_test.go +++ b/atlas/resource_atlas_schema_test.go @@ -3,6 +3,7 @@ package atlas import ( "context" "fmt" + "regexp" "testing" "ariga.io/atlas/sql/sqlclient" @@ -11,16 +12,22 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) -const testAccActionConfigCreate = ` +const ( + mysqlURL = "mysql://root:pass@localhost:3306" + mysqlDevURL = "mysql://root:pass@localhost:3307" +) + +func TestAccAtlasDatabase(t *testing.T) { + var testAccActionConfigCreate = fmt.Sprintf(` data "atlas_schema" "market" { - dev_db_url = "mysql://root:pass@localhost:3307" + dev_db_url = "%s" src = <<-EOT - schema "test" { + schema "test1" { charset = "utf8mb4" collate = "utf8mb4_0900_ai_ci" } table "foo" { - schema = schema.test + schema = schema.test1 column "id" { null = false type = int @@ -34,20 +41,20 @@ data "atlas_schema" "market" { } resource "atlas_schema" "testdb" { hcl = data.atlas_schema.market.hcl - url = "mysql://root:pass@localhost:3306" + url = "%s" } -` +`, mysqlDevURL, mysqlURL) -const testAccActionConfigUpdate = ` + var testAccActionConfigUpdate = fmt.Sprintf(` data "atlas_schema" "market" { - dev_db_url = "mysql://root:pass@localhost:3307" + dev_db_url = "%s" src = <<-EOT - schema "test" { + schema "test1" { charset = "utf8mb4" collate = "utf8mb4_0900_ai_ci" } table "foo" { - schema = schema.test + schema = schema.test1 column "id" { null = false type = int @@ -65,11 +72,9 @@ data "atlas_schema" "market" { } resource "atlas_schema" "testdb" { hcl = data.atlas_schema.market.hcl - url = "mysql://root:pass@localhost:3306" + url = "%s" } -` - -func TestAccAtlasDatabase(t *testing.T) { +`, mysqlDevURL, mysqlURL) resource.Test(t, resource.TestCase{ Providers: map[string]*schema.Provider{ "atlas": Provider(), @@ -78,20 +83,20 @@ func TestAccAtlasDatabase(t *testing.T) { { Config: testAccActionConfigCreate, Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("atlas_schema.testdb", "id", "mysql://root:pass@localhost:3306"), + resource.TestCheckResourceAttr("atlas_schema.testdb", "id", mysqlURL), ), }, { Config: testAccActionConfigUpdate, Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("atlas_schema.testdb", "id", "mysql://root:pass@localhost:3306"), + resource.TestCheckResourceAttr("atlas_schema.testdb", "id", mysqlURL), func(s *terraform.State) error { res := s.RootModule().Resources["atlas_schema.testdb"] - cli, err := sqlclient.Open(context.TODO(), res.Primary.ID) + cli, err := sqlclient.Open(context.Background(), res.Primary.ID) if err != nil { return err } - realm, err := cli.InspectRealm(context.TODO(), nil) + realm, err := cli.InspectRealm(context.Background(), nil) if err != nil { return err } @@ -107,30 +112,198 @@ func TestAccAtlasDatabase(t *testing.T) { }) } +func TestAccInvalidSchemaReturnsError(t *testing.T) { + testAccValidSchema := fmt.Sprintf(` + resource "atlas_schema" "testdb" { + hcl = <<-EOT + schema "test2" { + charset = "utf8mb4" + collate = "utf8mb4_0900_ai_ci" + } + table "foo" { + schema = schema.test2 + column "id" { + null = false + type = int + auto_increment = true + } + primary_key { + columns = [column.id] + } + } + EOT + url = "%s" + } + `, mysqlURL) + // invalid hcl file (missing `"` in 'table "orders...') + testAccInvalidSchema := fmt.Sprintf(` + resource "atlas_schema" "testdb" { + hcl = <<-EOT + schema "test2" { + charset = "utf8mb4" + collate = "utf8mb4_0900_ai_ci" + } + table "orders { + schema = schema.test2 + column "id" { + null = false + type = int + auto_increment = true + } + primary_key { + columns = [column.id] + } + } + EOT + url = "%s" + } + `, mysqlURL) + + resource.Test(t, resource.TestCase{ + Providers: map[string]*schema.Provider{ + "atlas": Provider(), + }, + IsUnitTest: true, + Steps: []resource.TestStep{ + { + Config: testAccValidSchema, + ExpectNonEmptyPlan: true, + Destroy: false, + }, + { + Config: testAccInvalidSchema, + ExpectError: regexp.MustCompile("schemahcl: failed decoding"), + Destroy: false, + ExpectNonEmptyPlan: true, + Check: func(s *terraform.State) error { + cli, err := sqlclient.Open(context.Background(), mysqlURL) + if err != nil { + return err + } + realm, err := cli.InspectRealm(context.Background(), nil) + if err != nil { + return err + } + tbl, ok := realm.Schemas[0].Table("orders") + if !ok { + return fmt.Errorf("expected database to have table \"orders\"") + } + if _, ok := tbl.Column("id"); !ok { + return fmt.Errorf("expected database to have table \"orders\" but got: %s", realm.Schemas[0].Tables[0].Name) + } + return nil + }, + }, + }, + }) +} + +func TestAccRemoveColumns(t *testing.T) { + const createTableStmt = `create table type_table +( + tBit bit(10) default 4 null, + tInt int(10) default 4 not null, + tTinyInt tinyint(10) default 8 null +) CHARSET = utf8mb4 COLLATE utf8mb4_0900_ai_ci;` + + var testAccSanity = fmt.Sprintf(` +data "atlas_schema" "sanity" { + dev_db_url = "%s" + src = <<-EOT + table "type_table" { + schema = schema.test3 + charset = "utf8mb4" + collate = "utf8mb4_0900_ai_ci" + column "tInt" { + null = false + type = int + default = 4 + } + } + schema "test3" { + charset = "utf8mb4" + collate = "utf8mb4_0900_ai_ci" + } + EOT +} +resource "atlas_schema" "testdb" { + hcl = data.atlas_schema.sanity.hcl + url = "%s" +} +`, mysqlDevURL, mysqlURL) + + const sanityState = `table "type_table" { + schema = schema.test3 + column "tInt" { + null = false + type = int + default = 4 + } +} +schema "test3" { + charset = "utf8mb4" + collate = "utf8mb4_0900_ai_ci" +} +` + resource.Test(t, resource.TestCase{ + Providers: map[string]*schema.Provider{ + "atlas": Provider(), + }, + Steps: []resource.TestStep{ + { + PreConfig: func() { + cli, err := sqlclient.Open(context.Background(), mysqlURL) + if err != nil { + t.Error(err) + } + defer cli.Close() + _, err = cli.DB.Exec("CREATE DATABASE IF NOT EXISTS test3;") + if err != nil { + t.Error(err) + } + _, err = cli.DB.Exec("USE test3;") + if err != nil { + t.Error(err) + } + _, err = cli.DB.Exec(createTableStmt) + if err != nil { + t.Error(err) + } + }, + Config: testAccSanity, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("atlas_schema.testdb", "id", mysqlURL), + resource.TestCheckResourceAttr("atlas_schema.testdb", "hcl", sanityState), + ), + }, + }, + }) +} + func TestAccDestroySchemas(t *testing.T) { - // Create schemas "main" and "do-not-delete". - preExistingSchema := `resource "atlas_schema" "testdb" { + // Create schemas "test4" and "do-not-delete". + preExistingSchema := fmt.Sprintf(`resource "atlas_schema" "testdb" { hcl = <<-EOT schema "do-not-delete" {} - schema "main" {} + schema "test4" {} EOT - url = "mysql://root:pass@localhost:3306" - }` - // When the following destroys, it only deletes schema "main". - tfSchema := `resource "atlas_schema" "testdb" { + url = "%s" + }`, mysqlURL) + // When the following destroys, it only deletes schema "test4". + tfSchema := fmt.Sprintf(`resource "atlas_schema" "testdb" { hcl = <<-EOT table "orders" { - schema = schema.main + schema = schema.test4 column "id" { null = true type = int } } - schema "main" { + schema "test4" { } EOT - url = "mysql://root:pass@localhost:3306/main" - }` + url = "%s/test4" + }`, mysqlURL) resource.Test(t, resource.TestCase{ Providers: map[string]*schema.Provider{ "atlas": Provider(), @@ -149,8 +322,7 @@ func TestAccDestroySchemas(t *testing.T) { }, }, CheckDestroy: func(s *terraform.State) error { - url := "mysql://root:pass@localhost:3306" - cli, err := sqlclient.Open(context.Background(), url) + cli, err := sqlclient.Open(context.Background(), mysqlURL) if err != nil { return err } @@ -161,8 +333,68 @@ func TestAccDestroySchemas(t *testing.T) { if _, ok := realm.Schema("do-not-delete"); !ok { return fmt.Errorf("schema 'do-not-delete' does not exist, but expected to not be destroyed.") } - if _, ok := realm.Schema("main"); ok { - return fmt.Errorf("schema 'main' wasn't deleted.") + if _, ok := realm.Schema("test4"); ok { + return fmt.Errorf("schema 'test4' wasn't deleted.") + } + return nil + }, + }) +} + +func TestAccMultipleSchemas(t *testing.T) { + mulSchema := fmt.Sprintf(`resource "atlas_schema" "testdb" { + hcl = <<-EOT + schema "m_test1" {} + schema "m_test2" {} + schema "m_test3" {} + schema "m_test4" {} + schema "m_test5" {} + EOT + url = "%s" + }`, mysqlURL) + resource.Test(t, resource.TestCase{ + Providers: map[string]*schema.Provider{ + "atlas": Provider(), + }, + Steps: []resource.TestStep{ + { + Config: mulSchema, + Destroy: false, + // ignore non-normalized schema + ExpectNonEmptyPlan: true, + Check: func(s *terraform.State) error { + cli, err := sqlclient.Open(context.Background(), mysqlURL) + if err != nil { + return err + } + realm, err := cli.InspectRealm(context.Background(), nil) + if err != nil { + return err + } + schemas := []string{"m_test1", "m_test2", "m_test3", "m_test4", "m_test5"} + for _, s := range schemas { + if _, ok := realm.Schema(s); !ok { + return fmt.Errorf("schema '%s' does not exist.", s) + } + } + return nil + }, + }, + }, + CheckDestroy: func(s *terraform.State) error { + cli, err := sqlclient.Open(context.Background(), mysqlURL) + if err != nil { + return err + } + realm, err := cli.InspectRealm(context.Background(), nil) + if err != nil { + return err + } + schemas := []string{"m_test1", "m_test2", "m_test3", "m_test4", "m_test5"} + for _, s := range schemas { + if _, ok := realm.Schema(s); ok { + return fmt.Errorf("schema '%s' exists.", s) + } } return nil }, diff --git a/go.mod b/go.mod index 4ce8136..d460797 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module ariga.io/ariga/terraform-provider-atlas go 1.17 require ( - ariga.io/atlas v0.3.8-0.20220501150530-25b6dbb99ff8 + ariga.io/atlas v0.3.8-0.20220508082407-70f71a80d0a1 github.com/go-sql-driver/mysql v1.6.0 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/terraform-plugin-docs v0.7.0 @@ -13,7 +13,7 @@ require ( ) require ( - entgo.io/ent v0.10.2-0.20220428114225-0c8f9a977c99 // indirect + entgo.io/ent v0.10.2-0.20220502113020-4ac82f5bb3f0 // indirect github.com/Masterminds/goutils v1.1.0 // indirect github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/sprig v2.22.0+incompatible // indirect diff --git a/go.sum b/go.sum index b7a728b..e706862 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,14 @@ ariga.io/atlas v0.3.8-0.20220501150530-25b6dbb99ff8 h1:KlX16MG8+3AjFl3CK33wTLg2FtLvApubha9VTjT98sI= ariga.io/atlas v0.3.8-0.20220501150530-25b6dbb99ff8/go.mod h1:URT1boACQmnulqRF07NkSOEkzx/OmO/48AmVX7pPaJg= +ariga.io/atlas v0.3.8-0.20220504100457-95b8a765a96f h1:1NC8dEB9hUl4o7sU/llxgn3WD/4LMYMgXIBqhDjUX4A= +ariga.io/atlas v0.3.8-0.20220504100457-95b8a765a96f/go.mod h1:D/d0a5QyMFU2R5E8ArmpnWbMjFP9LOr0TsQNbKqhT20= +ariga.io/atlas v0.3.8-0.20220508082407-70f71a80d0a1 h1:4EMvZC/fEsUNMKBWQGTS6V39kyOM0hkLuduMI+QRKkQ= +ariga.io/atlas v0.3.8-0.20220508082407-70f71a80d0a1/go.mod h1:D/d0a5QyMFU2R5E8ArmpnWbMjFP9LOr0TsQNbKqhT20= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= entgo.io/ent v0.10.2-0.20220428114225-0c8f9a977c99 h1:++Y53GODlpz4OZOJ4VjANc9OxKUR6NZRgXcFg+CEtOc= entgo.io/ent v0.10.2-0.20220428114225-0c8f9a977c99/go.mod h1:rUH4yiUVADR2XHnv0ZERDnwF/YjR42m3Qra6CZ4ZxBA= +entgo.io/ent v0.10.2-0.20220502113020-4ac82f5bb3f0/go.mod h1:Zh61BPvB+cL6VWEyN8f1YoDacrMjQf2KDlDeX26xq2k= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg=