Skip to content

Commit fe79e5d

Browse files
committed
command/apply: can take a plan as an argument
1 parent 36ccb34 commit fe79e5d

5 files changed

Lines changed: 177 additions & 96 deletions

File tree

‎command/apply.go‎

Lines changed: 87 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -20,100 +20,75 @@ type ApplyCommand struct {
2020

2121
func (c *ApplyCommand) Run(args []string) int {
2222
var init bool
23-
var statePath, stateOutPath string
23+
var stateOutPath string
2424

2525
cmdFlags := flag.NewFlagSet("apply", flag.ContinueOnError)
2626
cmdFlags.BoolVar(&init, "init", false, "init")
27-
cmdFlags.StringVar(&statePath, "state", "terraform.tfstate", "path")
28-
cmdFlags.StringVar(&stateOutPath, "state-out", "", "path")
27+
cmdFlags.StringVar(&stateOutPath, "out", "", "path")
2928
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
3029
if err := cmdFlags.Parse(args); err != nil {
3130
return 1
3231
}
3332

3433
args = cmdFlags.Args()
35-
if len(args) != 1 {
36-
c.Ui.Error(
37-
"The apply command expects only one argument with the path\n" +
38-
"to a Terraform configuration.\n")
34+
if len(args) != 2 {
35+
c.Ui.Error("The apply command expects two arguments.\n")
3936
cmdFlags.Usage()
4037
return 1
4138
}
4239

43-
if statePath == "" {
44-
c.Ui.Error("-state cannot be blank")
45-
return 1
46-
}
40+
statePath := args[0]
41+
configPath := args[1]
42+
4743
if stateOutPath == "" {
4844
stateOutPath = statePath
4945
}
50-
if !init {
51-
if _, err := os.Stat(statePath); err != nil {
52-
c.Ui.Error(fmt.Sprintf(
53-
"There was an error reading the state file. The path\n"+
54-
"and error are shown below. If you're trying to build a\n"+
55-
"brand new infrastructure, explicitly pass the '-init'\n"+
56-
"flag to Terraform to tell it it is okay to build new\n"+
57-
"state.\n\n"+
58-
"Path: %s\n"+
59-
"Error: %s",
60-
statePath,
61-
err))
62-
return 1
63-
}
64-
}
65-
66-
// Load up the state
67-
var state *terraform.State
68-
if !init {
69-
f, err := os.Open(statePath)
70-
if err != nil {
71-
c.Ui.Error(fmt.Sprintf("Error loading state: %s", err))
72-
return 1
73-
}
74-
75-
state, err = terraform.ReadState(f)
76-
f.Close()
77-
if err != nil {
78-
c.Ui.Error(fmt.Sprintf("Error loading state: %s", err))
79-
return 1
80-
}
81-
}
82-
83-
b, err := config.Load(args[0])
84-
if err != nil {
85-
c.Ui.Error(fmt.Sprintf("Error loading blueprint: %s", err))
86-
return 1
87-
}
8846

47+
// Initialize Terraform right away
8948
c.TFConfig.Hooks = append(c.TFConfig.Hooks, &UiHook{Ui: c.Ui})
9049
tf, err := terraform.New(c.TFConfig)
9150
if err != nil {
9251
c.Ui.Error(fmt.Sprintf("Error initializing Terraform: %s", err))
9352
return 1
9453
}
9554

96-
plan, err := tf.Plan(b, state, nil)
97-
if err != nil {
98-
c.Ui.Error(fmt.Sprintf("Error running plan: %s", err))
99-
return 1
55+
// Attempt to read a plan from the path given. This is how we test that
56+
// it is a plan or not (kind of jank, but if it quacks like a duck...)
57+
var plan *terraform.Plan
58+
f, err := os.Open(configPath)
59+
if err == nil {
60+
plan, err = terraform.ReadPlan(f)
61+
f.Close()
62+
if err != nil {
63+
// Make sure the plan is nil so that we try to load as
64+
// configuration.
65+
plan = nil
66+
}
67+
}
68+
69+
if plan == nil {
70+
// No plan was given, so we're loading from configuration. Generate
71+
// the plan given the configuration.
72+
plan, err = c.configToPlan(tf, init, statePath, configPath)
73+
if err != nil {
74+
c.Ui.Error(err.Error())
75+
return 1
76+
}
10077
}
10178

102-
state, err = tf.Apply(plan)
79+
state, err := tf.Apply(plan)
10380
if err != nil {
10481
c.Ui.Error(fmt.Sprintf("Error applying plan: %s", err))
10582
return 1
10683
}
10784

10885
// Write state out to the file
109-
f, err := os.Create(stateOutPath)
110-
if err != nil {
111-
c.Ui.Error(fmt.Sprintf("Failed to save state: %s", err))
112-
return 1
86+
f, err = os.Create(stateOutPath)
87+
if err == nil {
88+
err = terraform.WriteState(state, f)
89+
f.Close()
11390
}
114-
defer f.Close()
115-
116-
if err := terraform.WriteState(state, f); err != nil {
91+
if err != nil {
11792
c.Ui.Error(fmt.Sprintf("Failed to save state: %s", err))
11893
return 1
11994
}
@@ -125,7 +100,7 @@ func (c *ApplyCommand) Run(args []string) int {
125100

126101
func (c *ApplyCommand) Help() string {
127102
helpText := `
128-
Usage: terraform apply [terraform.tf]
103+
Usage: terraform apply [options] STATE PATH
129104
130105
Builds or changes infrastructure according to the Terraform configuration
131106
file.
@@ -135,17 +110,60 @@ Options:
135110
-init If specified, it is okay to build brand new
136111
infrastructure (with no state file specified).
137112
138-
-state=terraform.tfstate Path to the state file to build off of. This file
139-
will also be written to with updated state unless
140-
-state-out is specified.
141-
142-
-state-out=file.tfstate Path to save the new state. If not specified, the
143-
-state value will be used.
113+
-out=file.tfstate Path to save the new state. If not specified, the
114+
state path argument will be used.
144115
145116
`
146117
return strings.TrimSpace(helpText)
147118
}
148119

149120
func (c *ApplyCommand) Synopsis() string {
150-
return "Builds or changes infrastructure according to Terrafiles"
121+
return "Builds or changes infrastructure"
122+
}
123+
124+
func (c *ApplyCommand) configToPlan(
125+
tf *terraform.Terraform,
126+
init bool,
127+
statePath string,
128+
configPath string) (*terraform.Plan, error) {
129+
if !init {
130+
if _, err := os.Stat(statePath); err != nil {
131+
return nil, fmt.Errorf(
132+
"There was an error reading the state file. The path\n"+
133+
"and error are shown below. If you're trying to build a\n"+
134+
"brand new infrastructure, explicitly pass the '-init'\n"+
135+
"flag to Terraform to tell it it is okay to build new\n"+
136+
"state.\n\n"+
137+
"Path: %s\n"+
138+
"Error: %s",
139+
statePath,
140+
err)
141+
}
142+
}
143+
144+
// Load up the state
145+
var state *terraform.State
146+
if !init {
147+
f, err := os.Open(statePath)
148+
if err == nil {
149+
state, err = terraform.ReadState(f)
150+
f.Close()
151+
}
152+
153+
if err != nil {
154+
return nil, fmt.Errorf("Error loading state: %s", err)
155+
}
156+
}
157+
158+
config, err := config.Load(configPath)
159+
if err != nil {
160+
return nil, fmt.Errorf("Error loading config: %s", err)
161+
}
162+
163+
plan, err := tf.Plan(config, state, nil)
164+
if err != nil {
165+
return nil, fmt.Errorf("Error running plan: %s", err)
166+
}
167+
168+
return plan, nil
151169
}

‎command/apply_test.go‎

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package command
22

33
import (
4-
"io/ioutil"
54
"os"
65
"reflect"
76
"testing"
@@ -11,13 +10,7 @@ import (
1110
)
1211

1312
func TestApply(t *testing.T) {
14-
tf, err := ioutil.TempFile("", "tf")
15-
if err != nil {
16-
t.Fatalf("err: %s", err)
17-
}
18-
statePath := tf.Name()
19-
tf.Close()
20-
os.Remove(tf.Name())
13+
statePath := testTempFile(t)
2114

2215
p := testProvider()
2316
ui := new(cli.MockUi)
@@ -28,7 +21,7 @@ func TestApply(t *testing.T) {
2821

2922
args := []string{
3023
"-init",
31-
"-state", statePath,
24+
statePath,
3225
testFixturePath("apply"),
3326
}
3427
if code := c.Run(args); code != 0 {
@@ -54,7 +47,10 @@ func TestApply(t *testing.T) {
5447
}
5548
}
5649

57-
func TestApply_noState(t *testing.T) {
50+
func TestApply_plan(t *testing.T) {
51+
planPath := testPlanFile(t, new(terraform.Plan))
52+
statePath := testTempFile(t)
53+
5854
p := testProvider()
5955
ui := new(cli.MockUi)
6056
c := &ApplyCommand{
@@ -63,23 +59,33 @@ func TestApply_noState(t *testing.T) {
6359
}
6460

6561
args := []string{
66-
"-state=",
67-
testFixturePath("apply"),
62+
statePath,
63+
planPath,
6864
}
69-
if code := c.Run(args); code != 1 {
70-
t.Fatalf("bad: \n%s", ui.OutputWriter.String())
65+
if code := c.Run(args); code != 0 {
66+
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
7167
}
72-
}
7368

74-
func TestApply_state(t *testing.T) {
75-
// Write out some prior state
76-
tf, err := ioutil.TempFile("", "tf")
69+
if _, err := os.Stat(statePath); err != nil {
70+
t.Fatalf("err: %s", err)
71+
}
72+
73+
f, err := os.Open(statePath)
7774
if err != nil {
7875
t.Fatalf("err: %s", err)
7976
}
80-
statePath := tf.Name()
81-
defer os.Remove(tf.Name())
77+
defer f.Close()
8278

79+
state, err := terraform.ReadState(f)
80+
if err != nil {
81+
t.Fatalf("err: %s", err)
82+
}
83+
if state == nil {
84+
t.Fatal("state should not be nil")
85+
}
86+
}
87+
88+
func TestApply_state(t *testing.T) {
8389
originalState := &terraform.State{
8490
Resources: map[string]*terraform.ResourceState{
8591
"test_instance.foo": &terraform.ResourceState{
@@ -89,11 +95,7 @@ func TestApply_state(t *testing.T) {
8995
},
9096
}
9197

92-
err = terraform.WriteState(originalState, tf)
93-
tf.Close()
94-
if err != nil {
95-
t.Fatalf("err: %s", err)
96-
}
98+
statePath := testStateFile(t, originalState)
9799

98100
p := testProvider()
99101
ui := new(cli.MockUi)
@@ -104,7 +106,7 @@ func TestApply_state(t *testing.T) {
104106

105107
// Run the apply command pointing to our existing state
106108
args := []string{
107-
"-state", statePath,
109+
statePath,
108110
testFixturePath("apply"),
109111
}
110112
if code := c.Run(args); code != 0 {
@@ -150,7 +152,7 @@ func TestApply_stateNoExist(t *testing.T) {
150152
}
151153

152154
args := []string{
153-
"-state=idontexist.tfstate",
155+
"idontexist.tfstate",
154156
testFixturePath("apply"),
155157
}
156158
if code := c.Run(args); code != 1 {

0 commit comments

Comments
 (0)