command/apply: can take a plan as an argument
This commit is contained in:
parent
36ccb3408d
commit
fe79e5df03
154
command/apply.go
154
command/apply.go
|
@ -20,72 +20,31 @@ type ApplyCommand struct {
|
||||||
|
|
||||||
func (c *ApplyCommand) Run(args []string) int {
|
func (c *ApplyCommand) Run(args []string) int {
|
||||||
var init bool
|
var init bool
|
||||||
var statePath, stateOutPath string
|
var stateOutPath string
|
||||||
|
|
||||||
cmdFlags := flag.NewFlagSet("apply", flag.ContinueOnError)
|
cmdFlags := flag.NewFlagSet("apply", flag.ContinueOnError)
|
||||||
cmdFlags.BoolVar(&init, "init", false, "init")
|
cmdFlags.BoolVar(&init, "init", false, "init")
|
||||||
cmdFlags.StringVar(&statePath, "state", "terraform.tfstate", "path")
|
cmdFlags.StringVar(&stateOutPath, "out", "", "path")
|
||||||
cmdFlags.StringVar(&stateOutPath, "state-out", "", "path")
|
|
||||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||||
if err := cmdFlags.Parse(args); err != nil {
|
if err := cmdFlags.Parse(args); err != nil {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
args = cmdFlags.Args()
|
args = cmdFlags.Args()
|
||||||
if len(args) != 1 {
|
if len(args) != 2 {
|
||||||
c.Ui.Error(
|
c.Ui.Error("The apply command expects two arguments.\n")
|
||||||
"The apply command expects only one argument with the path\n" +
|
|
||||||
"to a Terraform configuration.\n")
|
|
||||||
cmdFlags.Usage()
|
cmdFlags.Usage()
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if statePath == "" {
|
statePath := args[0]
|
||||||
c.Ui.Error("-state cannot be blank")
|
configPath := args[1]
|
||||||
return 1
|
|
||||||
}
|
|
||||||
if stateOutPath == "" {
|
if stateOutPath == "" {
|
||||||
stateOutPath = statePath
|
stateOutPath = statePath
|
||||||
}
|
}
|
||||||
if !init {
|
|
||||||
if _, err := os.Stat(statePath); err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf(
|
|
||||||
"There was an error reading the state file. The path\n"+
|
|
||||||
"and error are shown below. If you're trying to build a\n"+
|
|
||||||
"brand new infrastructure, explicitly pass the '-init'\n"+
|
|
||||||
"flag to Terraform to tell it it is okay to build new\n"+
|
|
||||||
"state.\n\n"+
|
|
||||||
"Path: %s\n"+
|
|
||||||
"Error: %s",
|
|
||||||
statePath,
|
|
||||||
err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load up the state
|
|
||||||
var state *terraform.State
|
|
||||||
if !init {
|
|
||||||
f, err := os.Open(statePath)
|
|
||||||
if err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Error loading state: %s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
state, err = terraform.ReadState(f)
|
|
||||||
f.Close()
|
|
||||||
if err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Error loading state: %s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err := config.Load(args[0])
|
|
||||||
if err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Error loading blueprint: %s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Initialize Terraform right away
|
||||||
c.TFConfig.Hooks = append(c.TFConfig.Hooks, &UiHook{Ui: c.Ui})
|
c.TFConfig.Hooks = append(c.TFConfig.Hooks, &UiHook{Ui: c.Ui})
|
||||||
tf, err := terraform.New(c.TFConfig)
|
tf, err := terraform.New(c.TFConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -93,27 +52,43 @@ func (c *ApplyCommand) Run(args []string) int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
plan, err := tf.Plan(b, state, nil)
|
// Attempt to read a plan from the path given. This is how we test that
|
||||||
|
// it is a plan or not (kind of jank, but if it quacks like a duck...)
|
||||||
|
var plan *terraform.Plan
|
||||||
|
f, err := os.Open(configPath)
|
||||||
|
if err == nil {
|
||||||
|
plan, err = terraform.ReadPlan(f)
|
||||||
|
f.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error running plan: %s", err))
|
// Make sure the plan is nil so that we try to load as
|
||||||
return 1
|
// configuration.
|
||||||
|
plan = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state, err = tf.Apply(plan)
|
if plan == nil {
|
||||||
|
// No plan was given, so we're loading from configuration. Generate
|
||||||
|
// the plan given the configuration.
|
||||||
|
plan, err = c.configToPlan(tf, init, statePath, configPath)
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(err.Error())
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state, err := tf.Apply(plan)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error applying plan: %s", err))
|
c.Ui.Error(fmt.Sprintf("Error applying plan: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write state out to the file
|
// Write state out to the file
|
||||||
f, err := os.Create(stateOutPath)
|
f, err = os.Create(stateOutPath)
|
||||||
if err != nil {
|
if err == nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to save state: %s", err))
|
err = terraform.WriteState(state, f)
|
||||||
return 1
|
f.Close()
|
||||||
}
|
}
|
||||||
defer f.Close()
|
if err != nil {
|
||||||
|
|
||||||
if err := terraform.WriteState(state, f); err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to save state: %s", err))
|
c.Ui.Error(fmt.Sprintf("Failed to save state: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
@ -125,7 +100,7 @@ func (c *ApplyCommand) Run(args []string) int {
|
||||||
|
|
||||||
func (c *ApplyCommand) Help() string {
|
func (c *ApplyCommand) Help() string {
|
||||||
helpText := `
|
helpText := `
|
||||||
Usage: terraform apply [terraform.tf]
|
Usage: terraform apply [options] STATE PATH
|
||||||
|
|
||||||
Builds or changes infrastructure according to the Terraform configuration
|
Builds or changes infrastructure according to the Terraform configuration
|
||||||
file.
|
file.
|
||||||
|
@ -135,17 +110,60 @@ Options:
|
||||||
-init If specified, it is okay to build brand new
|
-init If specified, it is okay to build brand new
|
||||||
infrastructure (with no state file specified).
|
infrastructure (with no state file specified).
|
||||||
|
|
||||||
-state=terraform.tfstate Path to the state file to build off of. This file
|
-out=file.tfstate Path to save the new state. If not specified, the
|
||||||
will also be written to with updated state unless
|
state path argument will be used.
|
||||||
-state-out is specified.
|
|
||||||
|
|
||||||
-state-out=file.tfstate Path to save the new state. If not specified, the
|
|
||||||
-state value will be used.
|
|
||||||
|
|
||||||
`
|
`
|
||||||
return strings.TrimSpace(helpText)
|
return strings.TrimSpace(helpText)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ApplyCommand) Synopsis() string {
|
func (c *ApplyCommand) Synopsis() string {
|
||||||
return "Builds or changes infrastructure according to Terrafiles"
|
return "Builds or changes infrastructure"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ApplyCommand) configToPlan(
|
||||||
|
tf *terraform.Terraform,
|
||||||
|
init bool,
|
||||||
|
statePath string,
|
||||||
|
configPath string) (*terraform.Plan, error) {
|
||||||
|
if !init {
|
||||||
|
if _, err := os.Stat(statePath); err != nil {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"There was an error reading the state file. The path\n"+
|
||||||
|
"and error are shown below. If you're trying to build a\n"+
|
||||||
|
"brand new infrastructure, explicitly pass the '-init'\n"+
|
||||||
|
"flag to Terraform to tell it it is okay to build new\n"+
|
||||||
|
"state.\n\n"+
|
||||||
|
"Path: %s\n"+
|
||||||
|
"Error: %s",
|
||||||
|
statePath,
|
||||||
|
err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load up the state
|
||||||
|
var state *terraform.State
|
||||||
|
if !init {
|
||||||
|
f, err := os.Open(statePath)
|
||||||
|
if err == nil {
|
||||||
|
state, err = terraform.ReadState(f)
|
||||||
|
f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Error loading state: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := config.Load(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Error loading config: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
plan, err := tf.Plan(config, state, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Error running plan: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return plan, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -11,13 +10,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestApply(t *testing.T) {
|
func TestApply(t *testing.T) {
|
||||||
tf, err := ioutil.TempFile("", "tf")
|
statePath := testTempFile(t)
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
statePath := tf.Name()
|
|
||||||
tf.Close()
|
|
||||||
os.Remove(tf.Name())
|
|
||||||
|
|
||||||
p := testProvider()
|
p := testProvider()
|
||||||
ui := new(cli.MockUi)
|
ui := new(cli.MockUi)
|
||||||
|
@ -28,7 +21,7 @@ func TestApply(t *testing.T) {
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"-init",
|
"-init",
|
||||||
"-state", statePath,
|
statePath,
|
||||||
testFixturePath("apply"),
|
testFixturePath("apply"),
|
||||||
}
|
}
|
||||||
if code := c.Run(args); code != 0 {
|
if code := c.Run(args); code != 0 {
|
||||||
|
@ -54,7 +47,10 @@ func TestApply(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestApply_noState(t *testing.T) {
|
func TestApply_plan(t *testing.T) {
|
||||||
|
planPath := testPlanFile(t, new(terraform.Plan))
|
||||||
|
statePath := testTempFile(t)
|
||||||
|
|
||||||
p := testProvider()
|
p := testProvider()
|
||||||
ui := new(cli.MockUi)
|
ui := new(cli.MockUi)
|
||||||
c := &ApplyCommand{
|
c := &ApplyCommand{
|
||||||
|
@ -63,23 +59,33 @@ func TestApply_noState(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"-state=",
|
statePath,
|
||||||
testFixturePath("apply"),
|
planPath,
|
||||||
}
|
}
|
||||||
if code := c.Run(args); code != 1 {
|
if code := c.Run(args); code != 0 {
|
||||||
t.Fatalf("bad: \n%s", ui.OutputWriter.String())
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(statePath); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open(statePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
state, err := terraform.ReadState(f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
if state == nil {
|
||||||
|
t.Fatal("state should not be nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestApply_state(t *testing.T) {
|
func TestApply_state(t *testing.T) {
|
||||||
// Write out some prior state
|
|
||||||
tf, err := ioutil.TempFile("", "tf")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
statePath := tf.Name()
|
|
||||||
defer os.Remove(tf.Name())
|
|
||||||
|
|
||||||
originalState := &terraform.State{
|
originalState := &terraform.State{
|
||||||
Resources: map[string]*terraform.ResourceState{
|
Resources: map[string]*terraform.ResourceState{
|
||||||
"test_instance.foo": &terraform.ResourceState{
|
"test_instance.foo": &terraform.ResourceState{
|
||||||
|
@ -89,11 +95,7 @@ func TestApply_state(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err = terraform.WriteState(originalState, tf)
|
statePath := testStateFile(t, originalState)
|
||||||
tf.Close()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
p := testProvider()
|
p := testProvider()
|
||||||
ui := new(cli.MockUi)
|
ui := new(cli.MockUi)
|
||||||
|
@ -104,7 +106,7 @@ func TestApply_state(t *testing.T) {
|
||||||
|
|
||||||
// Run the apply command pointing to our existing state
|
// Run the apply command pointing to our existing state
|
||||||
args := []string{
|
args := []string{
|
||||||
"-state", statePath,
|
statePath,
|
||||||
testFixturePath("apply"),
|
testFixturePath("apply"),
|
||||||
}
|
}
|
||||||
if code := c.Run(args); code != 0 {
|
if code := c.Run(args); code != 0 {
|
||||||
|
@ -150,7 +152,7 @@ func TestApply_stateNoExist(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"-state=idontexist.tfstate",
|
"idontexist.tfstate",
|
||||||
testFixturePath("apply"),
|
testFixturePath("apply"),
|
||||||
}
|
}
|
||||||
if code := c.Run(args); code != 1 {
|
if code := c.Run(args); code != 1 {
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
)
|
)
|
||||||
|
@ -23,6 +26,38 @@ func testTFConfig(p terraform.ResourceProvider) *terraform.Config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testPlanFile(t *testing.T, plan *terraform.Plan) string {
|
||||||
|
path := testTempFile(t)
|
||||||
|
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if err := terraform.WritePlan(plan, f); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStateFile(t *testing.T, s *terraform.State) string {
|
||||||
|
path := testTempFile(t)
|
||||||
|
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if err := terraform.WriteState(s, f); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
func testProvider() *terraform.MockResourceProvider {
|
func testProvider() *terraform.MockResourceProvider {
|
||||||
p := new(terraform.MockResourceProvider)
|
p := new(terraform.MockResourceProvider)
|
||||||
p.RefreshFn = func(
|
p.RefreshFn = func(
|
||||||
|
@ -37,3 +72,21 @@ func testProvider() *terraform.MockResourceProvider {
|
||||||
|
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testTempFile(t *testing.T) string {
|
||||||
|
tf, err := ioutil.TempFile("", "tf")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := tf.Name()
|
||||||
|
|
||||||
|
if err := tf.Close(); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
if err := os.Remove(result); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
|
@ -33,6 +33,10 @@ func (p *Plan) String() string {
|
||||||
|
|
||||||
func (p *Plan) init() {
|
func (p *Plan) init() {
|
||||||
p.once.Do(func() {
|
p.once.Do(func() {
|
||||||
|
if p.Config == nil {
|
||||||
|
p.Config = new(config.Config)
|
||||||
|
}
|
||||||
|
|
||||||
if p.Diff == nil {
|
if p.Diff == nil {
|
||||||
p.Diff = new(Diff)
|
p.Diff = new(Diff)
|
||||||
p.Diff.init()
|
p.Diff.init()
|
||||||
|
|
|
@ -42,6 +42,10 @@ func New(c *Config) (*Terraform, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terraform) Apply(p *Plan) (*State, error) {
|
func (t *Terraform) Apply(p *Plan) (*State, error) {
|
||||||
|
// Make sure we're working with a plan that doesn't have null pointers
|
||||||
|
// everywhere, and is instead just empty otherwise.
|
||||||
|
p.init()
|
||||||
|
|
||||||
g, err := t.Graph(p.Config, p.State)
|
g, err := t.Graph(p.Config, p.State)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
Loading…
Reference in New Issue