Skip to content

Commit

Permalink
all: support custom atlas.hcl
Browse files Browse the repository at this point in the history
  • Loading branch information
giautm committed Jul 29, 2024
1 parent d325187 commit 8ca64a8
Show file tree
Hide file tree
Showing 10 changed files with 646 additions and 488 deletions.
3 changes: 2 additions & 1 deletion docs/data-sources/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ data "atlas_migration" "hello" {
### Optional

- `cloud` (Block, Optional) (see [below for nested schema](#nestedblock--cloud))
- `config` (String) The configuration file for the migration
- `dir` (String) Select migration directory using URL format
- `remote_dir` (Block, Optional) (see [below for nested schema](#nestedblock--remote_dir))
- `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

### Read-Only
Expand Down
8 changes: 3 additions & 5 deletions docs/resources/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,19 @@ resource "atlas_migration" "hello" {
<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `url` (String, Sensitive) The url of the database see https://atlasgo.io/cli/url

### Optional

- `baseline` (String) An optional version to start the migration history from. See https://atlasgo.io/versioned/apply#existing-databases
- `cloud` (Block, Optional) (see [below for nested schema](#nestedblock--cloud))
- `config` (String) The content of atlas.hcl config
- `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
- `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
- `remote_dir` (Block, Optional) (see [below for nested schema](#nestedblock--remote_dir))
- `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))
- `url` (String, Sensitive) The url of the database see https://atlasgo.io/cli/url
- `version` (String) The version of the migration to apply, if not specified the latest version will be applied

### Read-Only
Expand Down
137 changes: 88 additions & 49 deletions internal/provider/atlas_migration_data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ package provider
import (
"context"
"fmt"
"os"
"path/filepath"
"net/url"

"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"

atlas "ariga.io/atlas-go-sdk/atlasexec"
"ariga.io/atlas-go-sdk/atlasexec"
)

type (
Expand All @@ -20,6 +20,7 @@ type (
}
// MigrationDataSourceModel describes the data source data model.
MigrationDataSourceModel struct {
Config types.String `tfsdk:"config"`
URL types.String `tfsdk:"url"`
RevisionsSchema types.String `tfsdk:"revisions_schema"`

Expand Down Expand Up @@ -48,6 +49,7 @@ var (
latestVersion = "Already at latest version"
noMigration = "No migration applied yet"
remoteDirBlock = schema.SingleNestedBlock{
DeprecationMessage: "Use the dir attribute with (atlas://<name>?tag=<tag>) URL format",
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Description: "The name of the remote directory. This attribute is required when remote_dir is set",
Expand Down Expand Up @@ -85,6 +87,11 @@ func (d *MigrationDataSource) Schema(_ context.Context, _ datasource.SchemaReque
"remote_dir": remoteDirBlock,
},
Attributes: map[string]schema.Attribute{
"config": schema.StringAttribute{
Description: "The configuration file for the migration",
Optional: true,
Sensitive: false,
},
"url": schema.StringAttribute{
Description: "[driver://username:password@address/dbname?param=value] select a resource using the URL format",
Required: true,
Expand Down Expand Up @@ -130,53 +137,70 @@ func (d *MigrationDataSource) Read(ctx context.Context, req datasource.ReadReque
if resp.Diagnostics.HasError() {
return
}
dir, err := os.MkdirTemp(os.TempDir(), "tf-atlas-*")
cfg, err := data.projectConfig(d.cloud)
if err != nil {
resp.Diagnostics.AddError("Generate config failure",
fmt.Sprintf("Failed to create temporary directory: %s", err.Error()))
resp.Diagnostics.AddError("Generate config failure", err.Error())
return
}
defer os.RemoveAll(dir)
cfgPath := filepath.Join(dir, "atlas.hcl")
if err := data.AtlasHCL(cfgPath, d.cloud); err != nil {
wd, err := atlasexec.NewWorkingDir(atlasexec.WithAtlasHCL(cfg.Render))
if err != nil {
resp.Diagnostics.AddError("Generate config failure",
fmt.Sprintf("Failed to write configuration file: %s", err.Error()))
fmt.Sprintf("Failed to create temporary directory: %s", err.Error()))
return
}
r, err := d.client.MigrateStatus(ctx, &atlas.MigrateStatusParams{
ConfigURL: fmt.Sprintf("file://%s", filepath.ToSlash(cfgPath)),
Env: "tf",
defer func() {
if err := wd.Close(); err != nil {
tflog.Debug(ctx, "Failed to cleanup working directory", map[string]any{
"error": err,
})
}
}()
err = d.client.WithWorkDir(wd.Path(), func(c *atlasexec.Client) error {
r, err := d.client.MigrateStatus(ctx, &atlasexec.MigrateStatusParams{
Env: cfg.EnvName,
})
if err != nil {
return err
}
data.Status = types.StringValue(r.Status)
if r.Status == "PENDING" && r.Current == noMigration {
data.Current = types.StringValue("")
} else {
data.Current = types.StringValue(r.Current)
}
if r.Status == "OK" && r.Next == latestVersion {
data.Next = types.StringValue("")
} else {
data.Next = types.StringValue(r.Next)
}
v := r.LatestVersion()
data.ID = dirToID(data.RemoteDir, data.DirURL)
if v == "" {
data.Latest = types.StringNull()
} else {
data.Latest = types.StringValue(v)
}
return nil
})
if err != nil {
resp.Diagnostics.AddError("Failed to read migration status", err.Error())
return
}
data.Status = types.StringValue(r.Status)
if r.Status == "PENDING" && r.Current == noMigration {
data.Current = types.StringValue("")
} else {
data.Current = types.StringValue(r.Current)
}
if r.Status == "OK" && r.Next == latestVersion {
data.Next = types.StringValue("")
} else {
data.Next = types.StringValue(r.Next)
}
v := r.LatestVersion()
data.ID = dirToID(data.RemoteDir, data.DirURL)
if v == "" {
data.Latest = types.StringNull()
} else {
data.Latest = types.StringValue(v)
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (d *MigrationDataSourceModel) AtlasHCL(path string, cloud *AtlasCloudBlock) error {
cfg := atlasHCL{
URL: d.URL.ValueString(),
Migration: &migrationConfig{
RevisionsSchema: d.RevisionsSchema.ValueString(),
func (d *MigrationDataSourceModel) projectConfig(cloud *AtlasCloudBlock) (*projectConfig, error) {
dbURL, err := absoluteSqliteURL(d.URL.ValueString())
if err != nil {
return nil, err
}
cfg := projectConfig{
Config: defaultString(d.Config, baseAtlasHCL),
EnvName: "tf",
Env: &envConfig{
URL: dbURL,
Migration: &migrationConfig{
RevisionsSchema: d.RevisionsSchema.ValueString(),
},
},
}
if d.Cloud != nil && d.Cloud.Token.ValueString() != "" {
Expand All @@ -190,19 +214,34 @@ func (d *MigrationDataSourceModel) AtlasHCL(path string, cloud *AtlasCloudBlock)
URL: cloud.URL.ValueStringPointer(),
}
}
switch {
case d.RemoteDir != nil:
if rd := d.RemoteDir; rd != nil {
if cfg.Cloud == nil {
return fmt.Errorf("cloud configuration is not set")
return nil, fmt.Errorf("cloud configuration is not set")
}
cfg.Migration.DirURL = "atlas://" + d.RemoteDir.Name.ValueString()
if !d.RemoteDir.Tag.IsNull() {
cfg.Migration.DirURL += "?tag=" + d.RemoteDir.Tag.ValueString()
}
case !d.DirURL.IsNull():
cfg.Migration.DirURL = fmt.Sprintf("file://%s", d.DirURL.ValueString())
default:
cfg.Migration.DirURL = "file://migrations"
cfg.Env.Migration.DirURL, err = rd.AtlasURL()
} else {
cfg.Env.Migration.DirURL, err = absoluteFileURL(
defaultString(d.DirURL, "migrations"))
}
if err != nil {
return nil, err
}
return cfg.CreateFile(path)
return &cfg, nil
}

// AtlasURL returns the atlas URL for the remote directory.
func (r *RemoteDirBlock) AtlasURL() (string, error) {
q := url.Values{}
n := r.Name.ValueString()
if n == "" {
return "", fmt.Errorf("remote_dir.name is required")
}
if t := r.Tag.ValueString(); t != "" {
q.Set("tag", t)
}
return (&url.URL{
Scheme: SchemaTypeAtlas,
Path: n,
RawQuery: q.Encode(),
}).String(), nil
}
Loading

0 comments on commit 8ca64a8

Please sign in to comment.