Skip to content

Commit

Permalink
atlas/migration: protected_flows configurations for migrate down (#154
Browse files Browse the repository at this point in the history
)
  • Loading branch information
giautm authored Aug 8, 2024
1 parent 285e875 commit 0f9da61
Show file tree
Hide file tree
Showing 5 changed files with 257 additions and 7 deletions.
18 changes: 18 additions & 0 deletions docs/resources/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ resource "atlas_migration" "hello" {
- `dir` (String) the URL of the migration directory. dir or remote_dir block is required
- `env_name` (String) The name of the environment used for reporting runs to Atlas Cloud. Default: tf
- `exec_order` (String) How Atlas computes and executes pending migration files to the database. One of `linear`,`linear-skip` or `non-linear`. See https://atlasgo.io/versioned/apply#execution-order
- `protected_flows` (Block, Optional) ProtectedFlows defines the protected flows of a deployment. (see [below for nested schema](#nestedblock--protected_flows))
- `remote_dir` (Block, Optional, Deprecated) (see [below for nested schema](#nestedblock--remote_dir))
- `revisions_schema` (String) The name of the schema the revisions table resides in
- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts))
Expand All @@ -59,6 +60,23 @@ Optional:
- `url` (String)


<a id="nestedblock--protected_flows"></a>
### Nested Schema for `protected_flows`

Optional:

- `migrate_down` (Block, Optional) migrate_down defines policies for down migrations. (see [below for nested schema](#nestedblock--protected_flows--migrate_down))

<a id="nestedblock--protected_flows--migrate_down"></a>
### Nested Schema for `protected_flows.migrate_down`

Optional:

- `allow` (Boolean) Allow allows the flow to be executed.
- `auto_approve` (Boolean) AutoApprove allows the flow to be automatically approved.



<a id="nestedblock--remote_dir"></a>
### Nested Schema for `remote_dir`

Expand Down
73 changes: 71 additions & 2 deletions internal/provider/atlas_migration_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ type (
MigrationResource struct {
providerData
}
// DeploymentFlow defines the flow of a deployment.
DeploymentFlow struct {
Allow types.Bool `tfsdk:"allow"`
AutoApprove types.Bool `tfsdk:"auto_approve"`
}
// MigrationResourceModel describes the resource data model.
MigrationResourceModel struct {
Config types.String `tfsdk:"config"`
Expand All @@ -49,8 +54,11 @@ type (
Baseline types.String `tfsdk:"baseline"`
ExecOrder types.String `tfsdk:"exec_order"`

Cloud *AtlasCloudBlock `tfsdk:"cloud"`
RemoteDir *RemoteDirBlock `tfsdk:"remote_dir"`
Cloud *AtlasCloudBlock `tfsdk:"cloud"`
RemoteDir *RemoteDirBlock `tfsdk:"remote_dir"`
ProtectedFlows *struct {
MigrateDown *DeploymentFlow `tfsdk:"migrate_down"`
} `tfsdk:"protected_flows"`

EnvName types.String `tfsdk:"env_name"`
Status types.Object `tfsdk:"status"`
Expand Down Expand Up @@ -105,6 +113,24 @@ func (r *MigrationResource) Schema(ctx context.Context, _ resource.SchemaRequest
Blocks: map[string]schema.Block{
"cloud": cloudBlock,
"remote_dir": remoteDirBlock,
"protected_flows": schema.SingleNestedBlock{
Description: "ProtectedFlows defines the protected flows of a deployment.",
Blocks: map[string]schema.Block{
"migrate_down": schema.SingleNestedBlock{
Description: "migrate_down defines policies for down migrations.",
Attributes: map[string]schema.Attribute{
"allow": schema.BoolAttribute{
Description: "Allow allows the flow to be executed.",
Optional: true,
},
"auto_approve": schema.BoolAttribute{
Description: "AutoApprove allows the flow to be automatically approved.",
Optional: true,
},
},
},
},
},
"timeouts": timeouts.Block(ctx, timeouts.Opts{
Create: true,
Update: true,
Expand Down Expand Up @@ -288,13 +314,35 @@ func (r MigrationResource) ValidateConfig(ctx context.Context, req resource.Vali
"cloud is unset", "cloud is required when using atlas:// URL",
)
}
if f := data.ProtectedFlows; f != nil {
if d := f.MigrateDown; d != nil {
if d.Allow.ValueBool() && d.AutoApprove.ValueBool() {
resp.Diagnostics.AddError(
"Protected flow error",
"auto_approve is not allowed for a remote directory",
)
return
}
}
}
return
default:
// Local dir, validate config for dev-url
resp.Diagnostics.Append(r.validateConfig(ctx, req.Config)...)
if resp.Diagnostics.HasError() {
return
}
if f := data.ProtectedFlows; f != nil {
if d := f.MigrateDown; d != nil {
if d.Allow.ValueBool() && !d.AutoApprove.ValueBool() {
resp.Diagnostics.AddError(
"Protected flow error",
"allow cannot be true without auto_approve for local migration directory",
)
return
}
}
}
}
// Validate the remote_dir block
switch {
Expand Down Expand Up @@ -481,6 +529,13 @@ func (r *MigrationResource) migrate(ctx context.Context, data *MigrationResource
}
switch {
case len(status.Pending) == 0 && len(status.Applied) > 0 && len(status.Applied) > len(status.Available):
if !cfg.MigrateDown {
diags.AddError(
"Protected flow error",
"migrate down is not allowed, set `migrate_down.allow` to true to allow downgrade",
)
return
}
params := &atlas.MigrateDownParams{
Env: cfg.EnvName,
Vars: cfg.Vars,
Expand Down Expand Up @@ -787,6 +842,20 @@ func (d *MigrationResourceModel) projectConfig(cloud *AtlasCloudBlock, devURL st
if err != nil {
return nil, err
}
if f := d.ProtectedFlows; f != nil {
if d := f.MigrateDown; d != nil && d.Allow.ValueBool() {
if strings.HasPrefix(cfg.Env.Migration.DirURL, "atlas://") {
if d.AutoApprove.ValueBool() {
return nil, fmt.Errorf("auto_approve is not allowed for a remote directory")
}
} else {
if !d.AutoApprove.ValueBool() {
return nil, fmt.Errorf("allow cannot be true without auto_approve for local migration directory")
}
}
cfg.MigrateDown = true
}
}
if vars := d.Vars.ValueString(); vars != "" {
if err = json.Unmarshal([]byte(vars), &cfg.Vars); err != nil {
return nil, fmt.Errorf("failed to parse variables: %w", err)
Expand Down
158 changes: 158 additions & 0 deletions internal/provider/atlas_migration_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ HCL
func TestAccMigrationResource_WithLatestVersion(t *testing.T) {
schema := "test_1"
tempSchemas(t, mysqlURL, schema)
tempSchemas(t, mysqlDevURL, schema)

// Jump to the latest version
resource.Test(t, resource.TestCase{
Expand Down Expand Up @@ -353,6 +354,96 @@ func TestAccMigrationResource_WithLatestVersion(t *testing.T) {
},
},
})

// Test protected_flows block
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(`
resource "atlas_migration" "testdb" {
dir = "migrations?format=atlas"
version = "20221101165036"
url = "%[1]s"
}`, fmt.Sprintf("%s/%s", mysqlURL, schema)),
ExpectError: regexp.MustCompile("migrate down is not allowed, set `migrate_down.allow` to true to allow"),
},
},
})
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(`
resource "atlas_migration" "testdb" {
dir = "migrations?format=atlas"
version = "20221101165036"
url = "%[1]s"
protected_flows {
migrate_down {
allow = true
}
}
}`, fmt.Sprintf("%s/%s", mysqlURL, schema)),
ExpectError: regexp.MustCompile("allow cannot be true without auto_approve for local migration directory"),
},
},
})

// Dev-URL is required for migrate down
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(`
resource "atlas_migration" "testdb" {
dir = "migrations?format=atlas"
version = "20221101165036"
url = "%[1]s"
protected_flows {
migrate_down {
allow = true
auto_approve = true
}
}
}`, fmt.Sprintf("%s/%s", mysqlURL, schema)),
ExpectError: regexp.MustCompile("Error: --dev-url cannot be empty."),
},
},
})

// Migrate down to the previous version
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(`
resource "atlas_migration" "testdb" {
dir = "migrations?format=atlas"
version = "20221101165036"
url = "%[1]s"
dev_url = "%[2]s"
protected_flows {
migrate_down {
allow = true
auto_approve = true
}
}
}`,
fmt.Sprintf("%s/%s", mysqlURL, schema),
fmt.Sprintf("%s/%s", mysqlDevURL, schema),
),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("atlas_migration.testdb", "status.current", "20221101165036"),
resource.TestCheckResourceAttr("atlas_migration.testdb", "status.next", "20221101165147"),
),
},
},
})
}

func TestAccMigrationResource_NoLongerExists(t *testing.T) {
Expand Down Expand Up @@ -627,6 +718,11 @@ func TestAccMigrationResource_AtlasURL_WithTag(t *testing.T) {
resource "atlas_migration" "hello" {
url = "%[3]s"
dir = "atlas://test?tag=one-down"
protected_flows {
migrate_down {
allow = true
}
}
}
`, devURL, srv.URL, dbURL)
resource.Test(t, resource.TestCase{
Expand Down Expand Up @@ -784,6 +880,7 @@ func TestAccMigrationResource_RequireApproval(t *testing.T) {
byTag["tag3"] = tag3
byTag["tag2"] = tag2
byTag["tag1"] = tag1
// Initial the migration to the latest version
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Expand Down Expand Up @@ -811,6 +908,57 @@ func TestAccMigrationResource_RequireApproval(t *testing.T) {
},
},
})
// Test the protected_flows block
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(`
provider "atlas" {
dev_url = "%[1]s"
cloud {
token = "aci_bearer_token"
url = "%[2]s"
project = "test"
}
}
resource "atlas_migration" "hello" {
url = "%[3]s"
dir = "atlas://test?tag=tag3"
}`, devURL, srv.URL, dbURL),
ExpectError: regexp.MustCompile("migrate down is not allowed, set `migrate_down.allow` to true to allow"),
},
},
})
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(`
provider "atlas" {
dev_url = "%[1]s"
cloud {
token = "aci_bearer_token"
url = "%[2]s"
project = "test"
}
}
resource "atlas_migration" "hello" {
url = "%[3]s"
dir = "atlas://test?tag=tag3"
protected_flows {
migrate_down {
allow = true
auto_approve = true
}
}
}`, devURL, srv.URL, dbURL),
ExpectError: regexp.MustCompile("auto_approve is not allowed for a remote directory"),
},
},
})
newS := func(s DeploymentApprovalsStatus) *DeploymentApprovalsStatus { return &s }
// plan is waiting for approval, and then approved
flow = append(flow, newS(PlanPendingApproval), newS(PlanApproved))
Expand All @@ -831,6 +979,11 @@ func TestAccMigrationResource_RequireApproval(t *testing.T) {
resource "atlas_migration" "hello" {
url = "%[3]s"
dir = "atlas://test?tag=tag3"
protected_flows {
migrate_down {
allow = true
}
}
}`, devURL, srv.URL, dbURL),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("atlas_migration.hello", "id", "remote_dir://test"),
Expand Down Expand Up @@ -860,6 +1013,11 @@ func TestAccMigrationResource_RequireApproval(t *testing.T) {
resource "atlas_migration" "hello" {
url = "%[3]s"
dir = "atlas://test?tag=tag2"
protected_flows {
migrate_down {
allow = true
}
}
}`, devURL, srv.URL, dbURL),
ExpectError: regexp.MustCompile("migration plan was aborted"),
},
Expand Down
3 changes: 3 additions & 0 deletions internal/provider/atlas_schema_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,7 @@ func TestAccMultipleSchemas(t *testing.T) {
}

func tempSchemas(t *testing.T, url string, schemas ...string) *sqlclient.Client {
t.Helper()
c, err := sqlclient.Open(context.Background(), url)
if err != nil {
t.Fatal(err)
Expand All @@ -580,7 +581,9 @@ func createTables(t *testing.T, c *sqlclient.Client, tables ...string) {
}

func drop(t *testing.T, c *sqlclient.Client, schemas ...string) {
t.Helper()
t.Cleanup(func() {
t.Helper()
t.Log("Dropping all schemas")
for _, s := range schemas {
_, err := c.ExecContext(context.Background(), fmt.Sprintf("DROP DATABASE IF EXISTS `%s`", s))
Expand Down
12 changes: 7 additions & 5 deletions internal/provider/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ import (
type (
// projectConfig is the builder for the atlas.hcl file.
projectConfig struct {
EnvName string
Cloud *cloudConfig
Env *envConfig
Config string
Vars atlas.Vars2
Cloud *cloudConfig
Env *envConfig

Config string // The base atlas.hcl to merge with, provided by the user
Vars atlas.Vars2 // Variable supplied for atlas.hcl
EnvName string // The env name to report
MigrateDown bool // Allow TF run migrate down when detected
}
envConfig struct {
URL string
Expand Down

0 comments on commit 0f9da61

Please sign in to comment.