Skip to content

Commit

Permalink
Implement a command line interface
Browse files Browse the repository at this point in the history
  • Loading branch information
minamijoyo committed Aug 17, 2020
1 parent 160a3b4 commit 8da91d3
Show file tree
Hide file tree
Showing 9 changed files with 259 additions and 19 deletions.
80 changes: 80 additions & 0 deletions command/apply.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package command

import (
"context"
"flag"
"fmt"
"io/ioutil"
"strings"

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

// ApplyCommand is a command which computes a new state and pushes it to remote state.
type ApplyCommand struct {
Meta
path string
}

// Run runs the procedure of this command.
func (c *ApplyCommand) Run(args []string) int {
cmdFlags := flag.NewFlagSet("apply", flag.ContinueOnError)
if err := cmdFlags.Parse(args); err != nil {
c.UI.Error(fmt.Sprintf("failed to parse arguments: %s", err))
return 1
}

if len(cmdFlags.Args()) != 1 {
c.UI.Error(fmt.Sprintf("The command expects 1 argument, but got %d", len(cmdFlags.Args())))
c.UI.Error(c.Help())
return 1
}

c.Option = newOption()
c.path = cmdFlags.Arg(0)

source, err := ioutil.ReadFile(c.path)
if err != nil {
c.UI.Error(err.Error())
return 1
}

config, err := config.ParseMigrationFile(c.path, source)
if err != nil {
c.UI.Error(err.Error())
return 1
}

m, err := config.NewMigrator(c.Option)
if err != nil {
c.UI.Error(err.Error())
return 1
}

err = m.Apply(context.Background())
if err != nil {
c.UI.Error(err.Error())
return 1
}

return 0
}

// Help returns long-form help text.
func (c *ApplyCommand) Help() string {
helpText := `
Usage: tfmigrate apply <PATH>
Apply computes a new state and pushes it to remote state.
It will fail if terraform plan detects any diffs with the new state.
Arguments
PATH A path of migration file
`
return strings.TrimSpace(helpText)
}

// Synopsis returns one-line help text.
func (c *ApplyCommand) Synopsis() string {
return "Computes a new state and pushes it to remote state"
}
24 changes: 24 additions & 0 deletions command/meta.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package command

import (
"os"

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

// Meta are the meta-options that are available on all or most commands.
type Meta struct {
// UI is a user interface representing input and output.
UI cli.Ui

// Option customizes a behaviror of Migrator.
// It is used for shared settings across Migrator instances.
Option *tfmigrate.MigratorOption
}

func newOption() *tfmigrate.MigratorOption {
return &tfmigrate.MigratorOption{
ExecPath: os.Getenv("TFMIGRATE_EXEC_PATH"),
}
}
81 changes: 81 additions & 0 deletions command/plan.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package command

import (
"context"
"flag"
"fmt"
"io/ioutil"
"strings"

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

// PlanCommand is a command which computes a new state by applying state
// migration operations to a temporary state.
type PlanCommand struct {
Meta
path string
}

// Run runs the procedure of this command.
func (c *PlanCommand) Run(args []string) int {
cmdFlags := flag.NewFlagSet("plan", flag.ContinueOnError)
if err := cmdFlags.Parse(args); err != nil {
c.UI.Error(fmt.Sprintf("failed to parse arguments: %s", err))
return 1
}

if len(cmdFlags.Args()) != 1 {
c.UI.Error(fmt.Sprintf("The command expects 1 argument, but got %d", len(cmdFlags.Args())))
c.UI.Error(c.Help())
return 1
}

c.Option = newOption()
c.path = cmdFlags.Arg(0)

source, err := ioutil.ReadFile(c.path)
if err != nil {
c.UI.Error(err.Error())
return 1
}

config, err := config.ParseMigrationFile(c.path, source)
if err != nil {
c.UI.Error(err.Error())
return 1
}

m, err := config.NewMigrator(c.Option)
if err != nil {
c.UI.Error(err.Error())
return 1
}

err = m.Plan(context.Background())
if err != nil {
c.UI.Error(err.Error())
return 1
}

return 0
}

// Help returns long-form help text.
func (c *PlanCommand) Help() string {
helpText := `
Usage: tfmigrate plan <PATH>
Plan computes a new state by applying state migration operations to a temporary state.
It will fail if terraform plan detects any diffs with the new state.
Arguments
PATH A path of migration file
`
return strings.TrimSpace(helpText)
}

// Synopsis returns one-line help text.
func (c *PlanCommand) Synopsis() string {
return "Computes a new state"
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ require (
github.com/hashicorp/hcl/v2 v2.6.0
github.com/hashicorp/logutils v1.0.0
github.com/mattn/go-shellwords v1.0.10
github.com/mitchellh/cli v1.1.1
golang.org/x/lint v0.0.0-20200302205851-738671d3881b
)
20 changes: 20 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,23 @@ github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2
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/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26FmByPp2eC2qla6E1Tw+scpcg/to=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
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/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
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/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
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=
Expand All @@ -23,12 +33,20 @@ 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-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
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/cli v1.1.1 h1:J64v/xD7Clql+JVKSvkYojLOXu1ibnY9ZjGLwSt/89w=
github.com/mitchellh/cli v1.1.1/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
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/posener/complete v1.1.1 h1:ccV59UEOTzVDnDUEFdT95ZzHVZ+5+158q8+SJb2QV5w=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
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=
Expand All @@ -48,8 +66,10 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
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-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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 h1:vsphBvatvfbhlb4PO1BYSr9dzugGxJ/SQHoNufZJq1w=
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=
Expand Down
56 changes: 45 additions & 11 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,32 +1,45 @@
package main

import (
"context"
"fmt"
"io"
"io/ioutil"
"log"
"os"

"github.com/hashicorp/logutils"
"github.com/minamijoyo/tfmigrate/tfexec"
"github.com/minamijoyo/tfmigrate/command"
"github.com/mitchellh/cli"
)

// Version is a version number.
var version = "0.0.1"

func main() {
log.SetOutput(logOutput())
log.Printf("[INFO] CLI args: %#v", os.Args)

e := tfexec.NewExecutor("tmp/test", os.Environ())
e.AppendEnv("TF_LOG", "TRACE")
e.AppendEnv("DIRENV_LOG_FORMAT", "")
terraformCLI := tfexec.NewTerraformCLI(e)
terraformCLI.SetExecPath("direnv exec . terraform")
v, err := terraformCLI.Version(context.Background())
ui := &cli.BasicUi{
Writer: os.Stdout,
}
commands := initCommands(ui)

args := os.Args[1:]

c := &cli.CLI{
Name: "tfmigrate",
Version: version,
Args: args,
Commands: commands,
HelpWriter: os.Stdout,
}

exitStatus, err := c.Run()
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
ui.Error(fmt.Sprintf("Failed to execute CLI: %s", err))
}
fmt.Fprintln(os.Stdout, v)

os.Exit(exitStatus)
}

func logOutput() io.Writer {
Expand All @@ -47,3 +60,24 @@ func logOutput() io.Writer {

return filter
}

func initCommands(ui cli.Ui) map[string]cli.CommandFactory {
meta := command.Meta{
UI: ui,
}

commands := map[string]cli.CommandFactory{
"plan": func() (cli.Command, error) {
return &command.PlanCommand{
Meta: meta,
}, nil
},
"apply": func() (cli.Command, error) {
return &command.ApplyCommand{
Meta: meta,
}, nil
},
}

return commands
}
6 changes: 3 additions & 3 deletions tfmigrate/migrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import (
// Migrator abstracts migration operations.
type Migrator interface {
// Plan computes a new state by applying state migration operations to a temporary state.
// It will fail if terraform plan detects any diffs with a new state.
// It will fail if terraform plan detects any diffs with the new state.
Plan(ctx context.Context) error

// Apply computes a new state and push it to remote state.
// It will fail if terraform plan detects any diffs with a new state.
// Apply computes a new state and pushes it to remote state.
// It will fail if terraform plan detects any diffs with the new state.
// We are intended to this is used for state refactoring.
// Any state migration operations should not break any real resources.
Apply(ctx context.Context) error
Expand Down
2 changes: 1 addition & 1 deletion tfmigrate/multi_state_migrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func (m *MultiStateMigrator) Plan(ctx context.Context) error {
return err
}

// Apply computes new states and push them to remote states.
// Apply computes new states and pushes them to remote states.
// It will fail if terraform plan detects any diffs with at least one new state.
// We are intended to this is used for state refactoring.
// Any state migration operations should not break any real resources.
Expand Down
8 changes: 4 additions & 4 deletions tfmigrate/state_migrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func NewStateMigrator(dir string, actions []StateAction, o *MigratorOption) *Sta
}

// plan computes a new state by applying state migration operations to a temporary state.
// It will fail if terraform plan detects any diffs with a new state.
// It will fail if terraform plan detects any diffs with the new state.
// We intentional private this method not to expose internal states and unify
// the Migrator interface between a single and multi state migrator.
func (m *StateMigrator) plan(ctx context.Context) (*tfexec.State, error) {
Expand Down Expand Up @@ -64,14 +64,14 @@ func (m *StateMigrator) plan(ctx context.Context) (*tfexec.State, error) {
}

// Plan computes a new state by applying state migration operations to a temporary state.
// It will fail if terraform plan detects any diffs with a new state.
// It will fail if terraform plan detects any diffs with the new state.
func (m *StateMigrator) Plan(ctx context.Context) error {
_, err := m.plan(ctx)
return err
}

// Apply computes a new state and push it to remote state.
// It will fail if terraform plan detects any diffs with a new state.
// Apply computes a new state and pushes it to remote state.
// It will fail if terraform plan detects any diffs with the new state.
// We are intended to this is used for state refactoring.
// Any state migration operations should not break any real resources.
func (m *StateMigrator) Apply(ctx context.Context) error {
Expand Down

0 comments on commit 8da91d3

Please sign in to comment.