Skip to content

Commit

Permalink
Define a migration file with HCL
Browse files Browse the repository at this point in the history
  • Loading branch information
minamijoyo committed Aug 15, 2020
1 parent 3d90b10 commit 47ebc3e
Show file tree
Hide file tree
Showing 8 changed files with 364 additions and 13 deletions.
144 changes: 144 additions & 0 deletions config/migration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package config

import (
"fmt"

"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclsimple"
"github.com/minamijoyo/tfmigrate/tfmigrate"
)

// MigrationFile represents a config for migration written in HCL.
type MigrationFile struct {
// Migration is a migration block.
// It must contain only one block, and multiple blocks are not allowed,
// because it's hard to re-run the file if partially failed.
Migration MigrationBlock `hcl:"migration,block"`
}

// MigrationBlock represents a migration block in HCL.
type MigrationBlock struct {
// Type is a type for migration.
// Valid values are `state` and `multi_state`.
Type string `hcl:"type,label"`
// Name is an arbitrary name for migration.
Name string `hcl:"name,label"`
// Remain is a body of migration block.
// We first decode only a block header and then decode schema depending on
// its type label.
Remain hcl.Body `hcl:",remain"`
}

// StateMigratorConfig is a config for StateMigrator.
type StateMigratorConfig struct {
// Dir is a working directory for executing terraform command.
// Default to `.` (current directory).
Dir string `hcl:"dir,optional"`
// Actions is a list of state action.
// action is a plain text for state operation.
// Valid formats are the following.
// "mv <source> <destination>"
// "rm <addresses>...
// "import <address> <id>"
// We could define strict block schema for action, but intentionally use a
// schema-less string to allow us to easily copy terraform state command to
// action.
Actions []string `hcl:"actions"`
}

// MultiStateMigratorConfig is a config for MultiStateMigrator.
type MultiStateMigratorConfig struct {
// FromDir is a working directory where states of resources move from.
FromDir string `hcl:"from_dir"`
// ToDir is a working directory where states of rsources move to.
ToDir string `hcl:"to_dir"`
// Actions is a list of multi state action.
// action is a plain text for state operation.
// Valid formats are the following.
// "mv <source> <destination>"
Actions []string `hcl:"actions"`
}

// MigratorConfig is an interface of factory method for Migrator.
type MigratorConfig interface {
// NewMigrator returns a new instance of Migrator.
NewMigrator(o *tfmigrate.MigratorOption) (tfmigrate.Migrator, error)
}

// StateMigratorConfig implements a MigratorConfig.
var _ MigratorConfig = (*StateMigratorConfig)(nil)

// MultiStateMigratorConfig implements a MigratorConfig.
var _ MigratorConfig = (*MultiStateMigratorConfig)(nil)

// NewMigrator returns a new instance of StateMigrator.
func (c *StateMigratorConfig) NewMigrator(o *tfmigrate.MigratorOption) (tfmigrate.Migrator, error) {
// default working directory
dir := "."
if len(c.Dir) > 0 {
dir = c.Dir
}

// build actions from config.
actions := []tfmigrate.StateAction{}
for _, cmdStr := range c.Actions {
action, err := tfmigrate.NewStateActionFromString(cmdStr)
if err != nil {
return nil, err
}
actions = append(actions, action)
}

return tfmigrate.NewStateMigrator(dir, actions, o), nil
}

// NewMigrator returns a new instance of MultiStateMigrator.
func (c *MultiStateMigratorConfig) NewMigrator(o *tfmigrate.MigratorOption) (tfmigrate.Migrator, error) {
// build actions from config.
actions := []tfmigrate.MultiStateAction{}
for _, cmdStr := range c.Actions {
action, err := tfmigrate.NewMultiStateActionFromString(cmdStr)
if err != nil {
return nil, err
}
actions = append(actions, action)
}

return tfmigrate.NewMultiStateMigrator(c.FromDir, c.ToDir, actions, o), nil
}

// ParseMigrationFile parses a given source of migration file and returns a MigratorConfig.
// 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 .hcl.json).
func ParseMigrationFile(filename string, source []byte) (MigratorConfig, error) {
// Decode migration block header.
var f MigrationFile
err := hclsimple.Decode(filename, source, nil, &f)
if err != nil {
return nil, fmt.Errorf("failed to decode migration file: %s, err: %s", filename, err)
}

// switch schema by migration type.
var config MigratorConfig
m := f.Migration
switch m.Type {
case "state":
var state StateMigratorConfig
diags := gohcl.DecodeBody(m.Remain, nil, &state)
if diags.HasErrors() {
return nil, diags
}
config = &state
case "multi_state":
var multiState MultiStateMigratorConfig
diags := gohcl.DecodeBody(m.Remain, nil, &multiState)
if diags.HasErrors() {
return nil, diags
}
config = &multiState
default:
return nil, fmt.Errorf("unknown migration type in %s: %s", filename, m.Type)
}
return config, nil
}
65 changes: 65 additions & 0 deletions config/migration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package config

import (
"testing"

"github.com/davecgh/go-spew/spew"
"github.com/minamijoyo/tfmigrate/tfmigrate"
)

func TestParseMigrationFileWithState(t *testing.T) {
const source = `
migration "state" "test" {
dir = "dir1"
actions = [
"mv aws_security_group.foo aws_security_group.foo2",
"mv aws_security_group.bar aws_security_group.bar2",
"rm aws_security_group.bar",
"import aws_security_group.piyo piyo",
]
}
`

config, err := ParseMigrationFile("test.hcl", []byte(source))
if err != nil {
t.Fatalf("failed to ParseMigration: %s", err)
}
spew.Dump(config)

o := &tfmigrate.MigratorOption{
ExecPath: "direnv exec . terraform",
}
m, err := config.NewMigrator(o)
if err != nil {
t.Fatalf("failed to NewMigrator: %s", err)
}
spew.Dump(m)
}

func TestParseMigrationFileWithMultiState(t *testing.T) {
const source = `
migration "multi_state" "mv_dir1_dir2" {
from_dir = "dir1"
to_dir = "dir2"
actions = [
"mv aws_security_group.foo aws_security_group.foo2",
"mv aws_security_group.bar aws_security_group.bar2",
]
}
`

config, err := ParseMigrationFile("test.hcl", []byte(source))
if err != nil {
t.Fatalf("failed to ParseMigration: %s", err)
}
spew.Dump(config)

o := &tfmigrate.MigratorOption{
ExecPath: "direnv exec . terraform",
}
m, err := config.NewMigrator(o)
if err != nil {
t.Fatalf("failed to NewMigrator: %s", err)
}
spew.Dump(m)
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.14

require (
github.com/davecgh/go-spew v1.1.1
github.com/hashicorp/hcl/v2 v2.6.0
github.com/hashicorp/logutils v1.0.0
github.com/mattn/go-shellwords v1.0.10
golang.org/x/lint v0.0.0-20200302205851-738671d3881b
Expand Down
42 changes: 42 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,20 +1,62 @@
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=
github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0=
github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=
github.com/apparentlymart/go-textseg/v12 v12.0.0 h1:bNEQyAGak9tojivJNkoqWErVCQbjdL7GzRt3F8NvfJ0=
github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/hashicorp/hcl/v2 v2.6.0 h1:3krZOfGY6SziUXa6H9PJU6TyohHn7I+ARYnhbeNBz+o=
github.com/hashicorp/hcl/v2 v2.6.0/go.mod h1:bQTN5mpo+jewjJgh8jr0JUguIi7qPHUF6yIfAEN3jqY=
github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/mattn/go-shellwords v1.0.10 h1:Y7Xqm8piKOO3v10Thp7Z36h4FYFjt5xB//6XvOrs2Gw=
github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/zclconf/go-cty v1.2.0 h1:sPHsy7ADcIZQP3vILvTjrh74ZA175TFP5vqiNK1UmlI=
github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7 h1:EBZoQjiKKPaLbPrbpssUfuHtwM6KV/vb4U85g/cigFY=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
48 changes: 48 additions & 0 deletions tfmigrate/multi_state_action.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package tfmigrate

import (
"context"
"fmt"
"strings"

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

// MultiStateAction abstracts multi state migration operations.
// It's used for moving resources from a state to another.
type MultiStateAction interface {
// MultiStateUpdate updates given two states and returns new two states.
MultiStateUpdate(ctx context.Context, fromTf tfexec.TerraformCLI, toTf tfexec.TerraformCLI, fromState *tfexec.State, toState *tfexec.State) (*tfexec.State, *tfexec.State, error)
}

// NewMultiStateActionFromString is a factory method which returns a new
// MultiStateAction from a given string.
// cmdStr is a plain text for state operation.
// This method is useful to build an action from terraform state command.
// Valid formats are the following.
// "mv <source> <destination>"
func NewMultiStateActionFromString(cmdStr string) (MultiStateAction, error) {
// split cmdStr using Fields instead of Split to allow cmdStr to have duplicated white spaces.
args := strings.Fields(cmdStr)
if len(args) == 0 {
return nil, fmt.Errorf("multi state action is empty: %s", cmdStr)
}
actionType := args[0]

// switch by action type and parse arguments and build an action.
var action MultiStateAction
switch actionType {
case "mv":
if len(args) != 3 {
return nil, fmt.Errorf("multi state mv action is invalid: %s", cmdStr)
}
src := args[1]
dst := args[2]
action = NewMultiStateMvAction(src, dst)

default:
return nil, fmt.Errorf("unknown multi state action type: %s", cmdStr)
}

return action, nil
}
7 changes: 0 additions & 7 deletions tfmigrate/multi_state_migrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,6 @@ import (
"github.com/minamijoyo/tfmigrate/tfexec"
)

// MultiStateAction abstracts multi state migration operations.
// It's used for moving resources from a state to another.
type MultiStateAction interface {
// MultiStateUpdate updates given two states and returns new two states.
MultiStateUpdate(ctx context.Context, fromTf tfexec.TerraformCLI, toTf tfexec.TerraformCLI, fromState *tfexec.State, toState *tfexec.State) (*tfexec.State, *tfexec.State, error)
}

// MultiStateMigrator implements the Migrator interface.
type MultiStateMigrator struct {
// fromTf is an instance of TerraformCLI which executes terraform command in a fromDir.
Expand Down

0 comments on commit 47ebc3e

Please sign in to comment.