Skip to content

Commit

Permalink
migration: support remote_dir block (#71)
Browse files Browse the repository at this point in the history
* migration: support `remote_dir` block

* migration: added test for data source

* migration: added test for resource

* fix: ensure token doesn't escape

* fix: update test for resource

* template: fixed remote_dir tag
  • Loading branch information
giautm authored Jun 11, 2023
1 parent 9486ff3 commit aa90095
Show file tree
Hide file tree
Showing 14 changed files with 422 additions and 38 deletions.
22 changes: 21 additions & 1 deletion docs/data-sources/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ data "atlas_migration" "hello" {

### Required

- `dir` (String) Select migration directory using URL format
- `url` (String, Sensitive) [driver://username:password@address/dbname?param=value] select a resource using the URL format

### Optional

- `cloud` (Block, Optional) (see [below for nested schema](#nestedblock--cloud))
- `dir` (String) Select migration directory using URL format
- `remote_dir` (Block, Optional) (see [below for nested schema](#nestedblock--remote_dir))
- `revisions_schema` (String) The name of the schema the revisions table resides in

### Read-Only
Expand All @@ -38,3 +40,21 @@ data "atlas_migration" "hello" {
- `latest` (String) The latest version of the migration is in the migration directory
- `next` (String) Next migration version
- `status` (String) The Status of migration (OK, PENDING)

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

Optional:

- `project` (String)
- `token` (String)
- `url` (String)


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

Optional:

- `name` (String) The name of the remote directory. This attribute is required when remote_dir is set
- `tag` (String) The tag of the remote directory
10 changes: 10 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,14 @@ resource "atlas_schema" "market" {

### Optional

- `cloud` (Block, Optional) (see [below for nested schema](#nestedblock--cloud))
- `dev_url` (String, Sensitive) The URL of the dev database. This configuration is shared for all resources if there is no config on the resource.

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

Optional:

- `project` (String)
- `token` (String)
- `url` (String)
23 changes: 22 additions & 1 deletion docs/resources/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@ resource "atlas_migration" "hello" {

### Required

- `dir` (String) the URL of the migration directory, by default it is file://migrations, e.g a directory named migrations in the current working directory.
- `url` (String, Sensitive) The url of the database see https://atlasgo.io/cli/url

### Optional

- `cloud` (Block, Optional) (see [below for nested schema](#nestedblock--cloud))
- `dev_url` (String, Sensitive) The url of the dev-db see https://atlasgo.io/cli/url
- `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
- `remote_dir` (Block, Optional) (see [below for nested schema](#nestedblock--remote_dir))
- `revisions_schema` (String) The name of the schema the revisions table resides in
- `version` (String) The version of the migration to apply, if not specified the latest version will be applied

Expand All @@ -45,6 +47,25 @@ resource "atlas_migration" "hello" {
- `id` (String) The ID of this resource
- `status` (Object) The status of the migration (see [below for nested schema](#nestedatt--status))

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

Optional:

- `project` (String)
- `token` (String)
- `url` (String)


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

Optional:

- `name` (String) The name of the remote directory. This attribute is required when remote_dir is set
- `tag` (String) The tag of the remote directory


<a id="nestedatt--status"></a>
### Nested Schema for `status`

Expand Down
54 changes: 47 additions & 7 deletions internal/provider/atlas_migration_data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,23 @@ type (
}
// MigrationDataSourceModel describes the data source data model.
MigrationDataSourceModel struct {
DirURL types.String `tfsdk:"dir"`
URL types.String `tfsdk:"url"`
RevisionsSchema types.String `tfsdk:"revisions_schema"`

DirURL types.String `tfsdk:"dir"`
Cloud *AtlasCloudBlock `tfsdk:"cloud"`
RemoteDir *RemoteDirBlock `tfsdk:"remote_dir"`

Status types.String `tfsdk:"status"`
Current types.String `tfsdk:"current"`
Next types.String `tfsdk:"next"`
Latest types.String `tfsdk:"latest"`
ID types.String `tfsdk:"id"`
}
RemoteDirBlock struct {
Name types.String `tfsdk:"name"`
Tag types.String `tfsdk:"tag"`
}
)

// Ensure provider defined types fully satisfy framework interfaces
Expand All @@ -38,8 +45,20 @@ var (
_ datasource.DataSourceWithConfigure = &MigrationDataSource{}
)
var (
latestVersion = "Already at latest version"
noMigration = "No migration applied yet"
latestVersion = "Already at latest version"
noMigration = "No migration applied yet"
remoteDirBlock = schema.SingleNestedBlock{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Description: "The name of the remote directory. This attribute is required when remote_dir is set",
Optional: true,
},
"tag": schema.StringAttribute{
Description: "The tag of the remote directory",
Optional: true,
},
},
}
)

// NewMigrationDataSource returns a new AtlasSchemaDataSource.
Expand All @@ -61,6 +80,10 @@ func (d *MigrationDataSource) Configure(ctx context.Context, req datasource.Conf
func (d *MigrationDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Data source returns the information about the current migration.",
Blocks: map[string]schema.Block{
"cloud": cloudBlock,
"remote_dir": remoteDirBlock,
},
Attributes: map[string]schema.Attribute{
"url": schema.StringAttribute{
Description: "[driver://username:password@address/dbname?param=value] select a resource using the URL format",
Expand All @@ -69,7 +92,7 @@ func (d *MigrationDataSource) Schema(_ context.Context, _ datasource.SchemaReque
},
"dir": schema.StringAttribute{
Description: "Select migration directory using URL format",
Required: true,
Optional: true,
},
"revisions_schema": schema.StringAttribute{
Description: "The name of the schema the revisions table resides in",
Expand Down Expand Up @@ -115,7 +138,7 @@ func (d *MigrationDataSource) Read(ctx context.Context, req datasource.ReadReque
}
defer os.RemoveAll(dir)
cfgPath := filepath.Join(dir, "atlas.hcl")
if err := data.AtlasHCL(cfgPath); err != nil {
if err := data.AtlasHCL(cfgPath, d.cloud); err != nil {
resp.Diagnostics.AddError("Generate config failure",
fmt.Sprintf("Failed to write configuration file: %s", err.Error()))
return
Expand All @@ -140,7 +163,7 @@ func (d *MigrationDataSource) Read(ctx context.Context, req datasource.ReadReque
data.Next = types.StringValue(r.Next)
}
v := r.LatestVersion()
data.ID = data.DirURL
data.ID = dirToID(data.RemoteDir, data.DirURL)
if v == "" {
data.Latest = types.StringNull()
} else {
Expand All @@ -149,11 +172,28 @@ func (d *MigrationDataSource) Read(ctx context.Context, req datasource.ReadReque
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (d *MigrationDataSourceModel) AtlasHCL(path string) error {
func (d *MigrationDataSourceModel) AtlasHCL(path string, cloud *AtlasCloudBlock) error {
cfg := templateData{
URL: d.URL.ValueString(),
DirURL: d.DirURL.ValueStringPointer(),
RevisionsSchema: d.RevisionsSchema.ValueString(),
}
if d.Cloud != nil && d.Cloud.Token.ValueString() != "" {
// Use the data source cloud block if it is set
cloud = d.Cloud
}
if cloud != nil {
cfg.Cloud = &cloudConfig{
Token: cloud.Token.ValueString(),
Project: cloud.Project.ValueStringPointer(),
URL: cloud.URL.ValueStringPointer(),
}
}
if d := d.RemoteDir; d != nil {
cfg.RemoteDir = &remoteDir{
Name: d.Name.ValueString(),
Tag: d.Tag.ValueStringPointer(),
}
}
return cfg.CreateFile(path)
}
99 changes: 98 additions & 1 deletion internal/provider/atlas_migration_data_source_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
package provider_test

import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"path/filepath"
"regexp"
"strings"
"testing"

"ariga.io/atlas/sql/migrate"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/stretchr/testify/require"
)

func TestAccMigrationDataSource(t *testing.T) {
Expand All @@ -23,7 +32,7 @@ func TestAccMigrationDataSource(t *testing.T) {
}
`, mysqlURL, schema),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("data.atlas_migration.hello", "id", "migrations?format=atlas"),
resource.TestCheckResourceAttr("data.atlas_migration.hello", "id", "file://migrations?format=atlas"),
resource.TestCheckResourceAttr("data.atlas_migration.hello", "status", "PENDING"),
resource.TestCheckResourceAttr("data.atlas_migration.hello", "current", ""),
resource.TestCheckResourceAttr("data.atlas_migration.hello", "next", "20221101163823"),
Expand All @@ -49,3 +58,91 @@ func TestAccMigrationDataSource(t *testing.T) {
},
})
}

func TestAccMigrationDataSource_RemoteDir(t *testing.T) {
var (
dir = migrate.MemDir{}
dbURL = fmt.Sprintf("sqlite://%s?_fk=true", filepath.Join(t.TempDir(), "sqlite.db"))
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var m struct {
Query string `json:"query"`
Variables struct {
Input json.RawMessage `json:"input"`
} `json:"variables"`
}
require.NoError(t, json.NewDecoder(r.Body).Decode(&m))
switch {
case strings.Contains(m.Query, "query"):
writeDir(t, &dir, w)
default:
t.Fatalf("unexpected query: %s", m.Query)
}
}))
config = fmt.Sprintf(`
data "atlas_migration" "hello" {
url = "%s"
remote_dir {
name = "test"
}
cloud {
token = "aci_bearer_token"
url = "%s"
}
}`, dbURL, srv.URL)
)
t.Cleanup(srv.Close)

t.Run("NoPendingFiles", func(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: config,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("data.atlas_migration.hello", "id", "remote_dir://test"),
resource.TestCheckResourceAttr("data.atlas_migration.hello", "status", "OK"),
resource.TestCheckResourceAttr("data.atlas_migration.hello", "current", "No migration applied yet"),
resource.TestCheckResourceAttr("data.atlas_migration.hello", "next", ""),
resource.TestCheckNoResourceAttr("data.atlas_migration.hello", "latest"),
),
},
},
})
})

t.Run("WithFiles", func(t *testing.T) {
require.NoError(t, dir.WriteFile("1.sql", []byte("create table foo (id int)")))
require.NoError(t, dir.WriteFile("2.sql", []byte("create table bar (id int)")))
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: config,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("data.atlas_migration.hello", "id", "remote_dir://test"),
resource.TestCheckResourceAttr("data.atlas_migration.hello", "status", "PENDING"),
resource.TestCheckResourceAttr("data.atlas_migration.hello", "current", ""),
resource.TestCheckResourceAttr("data.atlas_migration.hello", "next", "1"),
resource.TestCheckResourceAttr("data.atlas_migration.hello", "latest", "2"),
),
},
},
})
})
}

func writeDir(t *testing.T, dir migrate.Dir, w io.Writer) {
// Checksum before archiving.
hf, err := dir.Checksum()
require.NoError(t, err)
ht, err := hf.MarshalText()
require.NoError(t, err)
require.NoError(t, dir.WriteFile(migrate.HashFileName, ht))
// Archive and send.
arc, err := migrate.ArchiveDir(dir)
require.NoError(t, err)
_, err = fmt.Fprintf(w, `{"data":{"dir":{"content":%q}}}`, base64.StdEncoding.EncodeToString(arc))
require.NoError(t, err)
}
Loading

0 comments on commit aa90095

Please sign in to comment.