Skip to content

Commit

Permalink
atlas: use custom differ to detect unsynced first runs (#18)
Browse files Browse the repository at this point in the history
* atlas: use custom differ to detect unsynced first runs

* Update atlas/resource_atlas_schema.go

Co-authored-by: Rotem Tamir <[email protected]>

* Update atlas/resource_atlas_schema.go

* Update atlas/resource_atlas_schema.go

Co-authored-by: Rotem Tamir <[email protected]>
  • Loading branch information
a8m and rotemtam authored Jun 10, 2022
1 parent 91832cf commit 5aa4922
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 58 deletions.
61 changes: 59 additions & 2 deletions atlas/resource_atlas_schema.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package atlas

import (
"context"

atlaschema "ariga.io/atlas/sql/schema"
"ariga.io/atlas/sql/sqlclient"
"context"
"fmt"
"strings"

"github.com/hashicorp/go-cty/cty"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
Expand All @@ -19,6 +20,7 @@ func newSchemaResource() *schema.Resource {
UpdateContext: applySchema,
ReadContext: readSchema,
DeleteContext: deleteSchema,
CustomizeDiff: customizeDiff,
Schema: map[string]*schema.Schema{
"hcl": {
Type: schema.TypeString,
Expand All @@ -41,6 +43,61 @@ func newSchemaResource() *schema.Resource {
}
}

func customizeDiff(ctx context.Context, diff *schema.ResourceDiff, i interface{}) error {
oldV, newV := diff.GetChange("hcl")
if oldV == nil {
return nil
}
s, ok := oldV.(string)
if !ok || s != "" {
return nil
}
url := diff.Get("url").(string)
cli, err := sqlclient.Open(ctx, url)
if err != nil {
return err
}
var schemas []string
if cli.URL.Schema != "" {
schemas = append(schemas, cli.URL.Schema)
}
current, err := cli.InspectRealm(ctx, &atlaschema.InspectRealmOption{Schemas: schemas})
if err != nil {
return err
}
desired := &atlaschema.Realm{}
if err = cli.Evaluator.Eval([]byte(newV.(string)), desired, nil); err != nil {
return err
}
changes, err := cli.RealmDiff(current, desired)
if err != nil {
return err
}
var causes []string
for _, c := range changes {
switch c := c.(type) {
case *atlaschema.DropSchema:
causes = append(causes, fmt.Sprintf("DROP SCHEMA %q", c.S.Name))
case *atlaschema.DropTable:
causes = append(causes, fmt.Sprintf("DROP TABLE %q", c.T.Name))
case *atlaschema.ModifyTable:
for _, c1 := range c.Changes {
if d, ok := c1.(*atlaschema.DropColumn); ok {
causes = append(causes, fmt.Sprintf("DROP COLUMN %q.%q", c.T.Name, d.C.Name))
}
}
}
}
if len(causes) > 0 {
return fmt.Errorf(`The database contains resources that Atlas wants to drop because they are not defined in the HCL file on the first run.
- %s
To learn how to add an existing database to a project, read:
https://atlasgo.io/terraform-provider#working-with-an-existing-database`, strings.Join(causes, "\n- "))
}
return nil
}

func deleteSchema(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
var diags diag.Diagnostics
url := d.Get("url").(string)
Expand Down
172 changes: 121 additions & 51 deletions atlas/resource_atlas_schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,17 @@ const (
)

func TestAccAtlasDatabase(t *testing.T) {
tempSchemas(t, "test")
var testAccActionConfigCreate = fmt.Sprintf(`
data "atlas_schema" "market" {
dev_db_url = "%s"
src = <<-EOT
schema "test1" {
schema "test" {
charset = "utf8mb4"
collate = "utf8mb4_0900_ai_ci"
}
table "foo" {
schema = schema.test1
schema = schema.test
column "id" {
null = false
type = int
Expand All @@ -49,12 +50,12 @@ resource "atlas_schema" "testdb" {
data "atlas_schema" "market" {
dev_db_url = "%s"
src = <<-EOT
schema "test1" {
schema "test" {
charset = "utf8mb4"
collate = "utf8mb4_0900_ai_ci"
}
table "foo" {
schema = schema.test1
schema = schema.test
column "id" {
null = false
type = int
Expand Down Expand Up @@ -113,15 +114,16 @@ resource "atlas_schema" "testdb" {
}

func TestAccInvalidSchemaReturnsError(t *testing.T) {
tempSchemas(t, "test")
testAccValidSchema := fmt.Sprintf(`
resource "atlas_schema" "testdb" {
hcl = <<-EOT
schema "test2" {
schema "test" {
charset = "utf8mb4"
collate = "utf8mb4_0900_ai_ci"
}
table "foo" {
schema = schema.test2
schema = schema.test
column "id" {
null = false
type = int
Expand All @@ -139,12 +141,12 @@ func TestAccInvalidSchemaReturnsError(t *testing.T) {
testAccInvalidSchema := fmt.Sprintf(`
resource "atlas_schema" "testdb" {
hcl = <<-EOT
schema "test2" {
schema "test" {
charset = "utf8mb4"
collate = "utf8mb4_0900_ai_ci"
}
table "orders {
schema = schema.test2
schema = schema.test
column "id" {
null = false
type = int
Expand Down Expand Up @@ -198,53 +200,91 @@ func TestAccInvalidSchemaReturnsError(t *testing.T) {
})
}

func TestEnsureSyncOnFirstRun(t *testing.T) {
tempSchemas(t, "test1", "test2")
hcl := fmt.Sprintf(`
resource "atlas_schema" "new_schema" {
hcl = <<-EOT
schema "test1" {
charset = "utf8mb4"
collate = "utf8mb4_0900_ai_ci"
}
EOT
url = "%s"
}
`, mysqlURL)

resource.Test(t, resource.TestCase{
Providers: map[string]*schema.Provider{
"atlas": Provider(),
},
IsUnitTest: true,
Steps: []resource.TestStep{
{
Config: hcl,
ExpectError: regexp.MustCompile("Error: The database contains resources that Atlas wants to drop because they are not defined in the HCL file on the first run."),
},
},
})
}

func TestAccRemoveColumns(t *testing.T) {
const createTableStmt = `create table type_table
tempSchemas(t, "test")
const createTableStmt = `create table test.type_table
(
tBit bit(10) default 4 null,
tInt int(10) default 4 not null,
tTinyInt tinyint(10) default 8 null
tBit bit(10) not null,
tInt int(10) not null,
tTinyInt tinyint(10) not null
) CHARSET = utf8mb4 COLLATE utf8mb4_0900_ai_ci;`

var testAccSanity = fmt.Sprintf(`
var (
steps = []string{
`table "type_table" {
schema = schema.test
column "tBit" {
null = false
type = bit
}
column "tInt" {
null = false
type = int
}
column "tTinyInt" {
null = false
type = tinyint
}
}
schema "test" {
charset = "utf8mb4"
collate = "utf8mb4_0900_ai_ci"
}
`,
`table "type_table" {
schema = schema.test
column "tInt" {
null = false
type = int
}
}
schema "test" {
charset = "utf8mb4"
collate = "utf8mb4_0900_ai_ci"
}
`,
}
testAccSanityT = `
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"
}
%s
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(),
Expand All @@ -257,30 +297,30 @@ schema "test3" {
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,
Config: fmt.Sprintf(testAccSanityT, mysqlDevURL, steps[0], mysqlURL),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("atlas_schema.testdb", "id", mysqlURL),
resource.TestCheckResourceAttr("atlas_schema.testdb", "hcl", sanityState),
resource.TestCheckResourceAttr("atlas_schema.testdb", "hcl", steps[0]),
),
},
{
Config: fmt.Sprintf(testAccSanityT, mysqlDevURL, steps[1], mysqlURL),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("atlas_schema.testdb", "id", mysqlURL),
resource.TestCheckResourceAttr("atlas_schema.testdb", "hcl", steps[1]),
),
},
},
})
}

func TestAccDestroySchemas(t *testing.T) {
tempSchemas(t, "test4", "do-not-delete")
// Create schemas "test4" and "do-not-delete".
preExistingSchema := fmt.Sprintf(`resource "atlas_schema" "testdb" {
hcl = <<-EOT
Expand Down Expand Up @@ -342,6 +382,7 @@ func TestAccDestroySchemas(t *testing.T) {
}

func TestAccMultipleSchemas(t *testing.T) {
tempSchemas(t, "m_test1", "m_test2", "m_test3", "m_test4", "m_test5")
mulSchema := fmt.Sprintf(`resource "atlas_schema" "testdb" {
hcl = <<-EOT
schema "m_test1" {}
Expand Down Expand Up @@ -400,3 +441,32 @@ func TestAccMultipleSchemas(t *testing.T) {
},
})
}

func tempSchemas(t *testing.T, schemas ...string) {
c, err := sqlclient.Open(context.Background(), mysqlURL)
if err != nil {
t.Fatal(err)
}
for _, s := range schemas {
_, err := c.ExecContext(context.Background(), fmt.Sprintf("CREATE DATABASE IF NOT EXISTS `%s`", s))
if err != nil {
t.Errorf("failed creating schema: %s", err)
}
}
drop(t, c, schemas...)
}

func drop(t *testing.T, c *sqlclient.Client, schemas ...string) {
t.Cleanup(func() {
t.Log("Dropping all schemas")
for _, s := range schemas {
_, err := c.ExecContext(context.Background(), fmt.Sprintf("DROP DATABASE IF EXISTS `%s`", s))
if err != nil {
t.Errorf("failed dropping schema: %s", err)
}
}
if err := c.Close(); err != nil {
t.Errorf("failed closing client: %s", err)
}
})
}
10 changes: 5 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,23 @@ require (
github.com/fatih/color v1.13.0 // indirect
github.com/go-openapi/inflect v0.19.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.7 // indirect
github.com/google/go-cmp v0.5.8 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-checkpoint v0.5.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-hclog v1.2.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-plugin v1.4.3 // indirect
github.com/hashicorp/go-uuid v1.0.2 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/go-version v1.4.0 // indirect
github.com/hashicorp/hc-install v0.3.1 // indirect
github.com/hashicorp/hcl/v2 v2.11.1 // indirect
github.com/hashicorp/logutils v1.0.0 // indirect
github.com/hashicorp/terraform-exec v0.16.0 // indirect
github.com/hashicorp/terraform-json v0.13.0 // indirect
github.com/hashicorp/terraform-plugin-go v0.8.0 // indirect
github.com/hashicorp/terraform-plugin-log v0.3.0 // indirect
github.com/hashicorp/terraform-plugin-go v0.9.0 // indirect
github.com/hashicorp/terraform-plugin-log v0.4.0 // indirect
github.com/hashicorp/terraform-registry-address v0.0.0-20210412075316-9b2996cce896 // indirect
github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734 // indirect
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect
Expand Down Expand Up @@ -70,5 +70,5 @@ require (
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect
google.golang.org/grpc v1.45.0 // indirect
google.golang.org/protobuf v1.27.1 // indirect
google.golang.org/protobuf v1.28.0 // indirect
)
Loading

0 comments on commit 5aa4922

Please sign in to comment.