Skip to content

Commit

Permalink
Pass environment variables as "env"
Browse files Browse the repository at this point in the history
Allows migration scripts to access the "env" variable, which can be used
to change behaviour dynamically.
  • Loading branch information
rco-ableton committed Apr 5, 2024
1 parent 0bb0234 commit bc7ebae
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 12 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,20 @@ migration "state" "test" {
}
```

### Environment Variables

Environment variables can be accessed in migration files via the `env` variable:

```hcl
migration "state" "test" {
dir = "dir1"
workspace = env.TFMIGRATE_WORKSPACE
actions = [
"mv aws_security_group.foo aws_security_group.foo2"
]
}
```

### migration block

- The file must contain exactly one `migration` block.
Expand Down
45 changes: 33 additions & 12 deletions config/migration.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ package config

import (
"fmt"
"os"
"strings"

"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclsimple"
"github.com/zclconf/go-cty/cty"

"github.com/minamijoyo/tfmigrate/tfmigrate"
)

Expand All @@ -30,18 +34,35 @@ type MigrationBlock struct {
Remain hcl.Body `hcl:",remain"`
}

// Return a map of environment variables.
func envVarMap() cty.Value {
envMap := make(map[string]cty.Value)
for _, env := range os.Environ() {
pair := strings.SplitN(env, "=", 2)
envMap[pair[0]] = cty.StringVal(pair[1])
}
return cty.MapVal(envMap)
}

// ParseMigrationFile parses a given source of migration file and returns a *tfmigrate.MigrationConfig.
// Note that this method does not read a file and you should pass source of config in bytes.
// The filename is used for error message and selecting HCL syntax (.hcl and .json).
func ParseMigrationFile(filename string, source []byte) (*tfmigrate.MigrationConfig, error) {
// Decode migration block header.
var f MigrationFile
err := hclsimple.Decode(filename, source, nil, &f)

ctx := &hcl.EvalContext{
Variables: map[string]cty.Value{
"env": envVarMap(),
},
}

err := hclsimple.Decode(filename, source, ctx, &f)
if err != nil {
return nil, fmt.Errorf("failed to decode migration file: %s, err: %s", filename, err)
}

migrator, err := parseMigrationBlock(f.Migration)
migrator, err := parseMigrationBlock(f.Migration, ctx)
if err != nil {
return nil, err
}
Expand All @@ -56,26 +77,26 @@ func ParseMigrationFile(filename string, source []byte) (*tfmigrate.MigrationCon
}

// parseMigrationBlock parses a migration block and returns a tfmigrate.MigratorConfig.
func parseMigrationBlock(b MigrationBlock) (tfmigrate.MigratorConfig, error) {
func parseMigrationBlock(b MigrationBlock, ctx *hcl.EvalContext) (tfmigrate.MigratorConfig, error) {
switch b.Type {
case "mock": // only for testing
return parseMockMigrationBlock(b)
return parseMockMigrationBlock(b, ctx)

case "state":
return parseStateMigrationBlock(b)
return parseStateMigrationBlock(b, ctx)

case "multi_state":
return parseMultiStateMigrationBlock(b)
return parseMultiStateMigrationBlock(b, ctx)

default:
return nil, fmt.Errorf("unknown migration type: %s", b.Type)
}
}

// parseMockMigrationBlock parses a migration block for mock and returns a tfmigrate.MigratorConfig.
func parseMockMigrationBlock(b MigrationBlock) (tfmigrate.MigratorConfig, error) {
func parseMockMigrationBlock(b MigrationBlock, ctx *hcl.EvalContext) (tfmigrate.MigratorConfig, error) {
var config tfmigrate.MockMigratorConfig
diags := gohcl.DecodeBody(b.Remain, nil, &config)
diags := gohcl.DecodeBody(b.Remain, ctx, &config)
if diags.HasErrors() {
return nil, diags
}
Expand All @@ -84,9 +105,9 @@ func parseMockMigrationBlock(b MigrationBlock) (tfmigrate.MigratorConfig, error)
}

// parseStateMigrationBlock parses a migration block for state and returns a tfmigrate.MigratorConfig.
func parseStateMigrationBlock(b MigrationBlock) (tfmigrate.MigratorConfig, error) {
func parseStateMigrationBlock(b MigrationBlock, ctx *hcl.EvalContext) (tfmigrate.MigratorConfig, error) {
var config tfmigrate.StateMigratorConfig
diags := gohcl.DecodeBody(b.Remain, nil, &config)
diags := gohcl.DecodeBody(b.Remain, ctx, &config)
if diags.HasErrors() {
return nil, diags
}
Expand All @@ -96,9 +117,9 @@ func parseStateMigrationBlock(b MigrationBlock) (tfmigrate.MigratorConfig, error

// parseMultiStateMigrationBlock parses a migration block for multi_state and
// returns a tfmigrate.MigratorConfig.
func parseMultiStateMigrationBlock(b MigrationBlock) (tfmigrate.MigratorConfig, error) {
func parseMultiStateMigrationBlock(b MigrationBlock, ctx *hcl.EvalContext) (tfmigrate.MigratorConfig, error) {
var config tfmigrate.MultiStateMigratorConfig
diags := gohcl.DecodeBody(b.Remain, nil, &config)
diags := gohcl.DecodeBody(b.Remain, ctx, &config)
if diags.HasErrors() {
return nil, diags
}
Expand Down
23 changes: 23 additions & 0 deletions config/migration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
func TestParseMigrationFileWithNativeSyntax(t *testing.T) {
cases := []struct {
desc string
env map[string]string
source string
want *tfmigrate.MigrationConfig
ok bool
Expand Down Expand Up @@ -342,10 +343,32 @@ foo "bar" "baz" {}
want: nil,
ok: false,
},
{
desc: "envirionment variable",
env: map[string]string{"TFMIGRATE_TEST_WORKSPACE": "test-workspace"},
source: `
migration "state" "test" {
workspace = env.TFMIGRATE_TEST_WORKSPACE
actions = []
}
`,
want: &tfmigrate.MigrationConfig{
Type: "state",
Name: "test",
Migrator: &tfmigrate.StateMigratorConfig{
Actions: []string{},
Workspace: "test-workspace",
},
},
ok: true,
},
}

for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
for k, v := range tc.env {
t.Setenv(k, v)
}
got, err := ParseMigrationFile("test.hcl", []byte(tc.source))
if tc.ok && err != nil {
t.Fatalf("unexpected err: %s", err)
Expand Down

0 comments on commit bc7ebae

Please sign in to comment.