From 429711b938400fad42f89d0df76a53ebeb76b41c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 23 Feb 2015 19:09:48 -0800 Subject: [PATCH 01/11] terraform: PostStateUpdate hook and EvalUpdateStateHook --- terraform/eval_context.go | 152 ------------------------------ terraform/eval_context_mock.go | 166 +++++++++++++++++++++++++++++++++ terraform/eval_state.go | 23 ++++- terraform/eval_state_test.go | 27 ++++++ terraform/hook.go | 7 ++ terraform/hook_mock.go | 11 +++ terraform/hook_stop.go | 4 + 7 files changed, 237 insertions(+), 153 deletions(-) create mode 100644 terraform/eval_context_mock.go create mode 100644 terraform/eval_state_test.go diff --git a/terraform/eval_context.go b/terraform/eval_context.go index e1d4eef516..120cf71e77 100644 --- a/terraform/eval_context.go +++ b/terraform/eval_context.go @@ -70,155 +70,3 @@ type EvalContext interface { // be used to modify that state. State() (*State, *sync.RWMutex) } - -// MockEvalContext is a mock version of EvalContext that can be used -// for tests. -type MockEvalContext struct { - HookCalled bool - HookError error - - InputCalled bool - InputInput UIInput - - InitProviderCalled bool - InitProviderName string - InitProviderProvider ResourceProvider - InitProviderError error - - ProviderCalled bool - ProviderName string - ProviderProvider ResourceProvider - - ProviderInputCalled bool - ProviderInputName string - ProviderInputConfig map[string]interface{} - - SetProviderInputCalled bool - SetProviderInputName string - SetProviderInputConfig map[string]interface{} - - ConfigureProviderCalled bool - ConfigureProviderName string - ConfigureProviderConfig *ResourceConfig - ConfigureProviderError error - - ParentProviderConfigCalled bool - ParentProviderConfigName string - ParentProviderConfigConfig *ResourceConfig - - InitProvisionerCalled bool - InitProvisionerName string - InitProvisionerProvisioner ResourceProvisioner - InitProvisionerError error - - ProvisionerCalled bool - ProvisionerName string - ProvisionerProvisioner ResourceProvisioner - - InterpolateCalled bool - InterpolateConfig *config.RawConfig - InterpolateResource *Resource - InterpolateConfigResult *ResourceConfig - InterpolateError error - - PathCalled bool - PathPath []string - - SetVariablesCalled bool - SetVariablesVariables map[string]string - - DiffCalled bool - DiffDiff *Diff - DiffLock *sync.RWMutex - - StateCalled bool - StateState *State - StateLock *sync.RWMutex -} - -func (c *MockEvalContext) Hook(fn func(Hook) (HookAction, error)) error { - c.HookCalled = true - return c.HookError -} - -func (c *MockEvalContext) Input() UIInput { - c.InputCalled = true - return c.InputInput -} - -func (c *MockEvalContext) InitProvider(n string) (ResourceProvider, error) { - c.InitProviderCalled = true - c.InitProviderName = n - return c.InitProviderProvider, c.InitProviderError -} - -func (c *MockEvalContext) Provider(n string) ResourceProvider { - c.ProviderCalled = true - c.ProviderName = n - return c.ProviderProvider -} - -func (c *MockEvalContext) ConfigureProvider(n string, cfg *ResourceConfig) error { - c.ConfigureProviderCalled = true - c.ConfigureProviderName = n - c.ConfigureProviderConfig = cfg - return c.ConfigureProviderError -} - -func (c *MockEvalContext) ParentProviderConfig(n string) *ResourceConfig { - c.ParentProviderConfigCalled = true - c.ParentProviderConfigName = n - return c.ParentProviderConfigConfig -} - -func (c *MockEvalContext) ProviderInput(n string) map[string]interface{} { - c.ProviderInputCalled = true - c.ProviderInputName = n - return c.ProviderInputConfig -} - -func (c *MockEvalContext) SetProviderInput(n string, cfg map[string]interface{}) { - c.SetProviderInputCalled = true - c.SetProviderInputName = n - c.SetProviderInputConfig = cfg -} - -func (c *MockEvalContext) InitProvisioner(n string) (ResourceProvisioner, error) { - c.InitProvisionerCalled = true - c.InitProvisionerName = n - return c.InitProvisionerProvisioner, c.InitProvisionerError -} - -func (c *MockEvalContext) Provisioner(n string) ResourceProvisioner { - c.ProvisionerCalled = true - c.ProvisionerName = n - return c.ProvisionerProvisioner -} - -func (c *MockEvalContext) Interpolate( - config *config.RawConfig, resource *Resource) (*ResourceConfig, error) { - c.InterpolateCalled = true - c.InterpolateConfig = config - c.InterpolateResource = resource - return c.InterpolateConfigResult, c.InterpolateError -} - -func (c *MockEvalContext) Path() []string { - c.PathCalled = true - return c.PathPath -} - -func (c *MockEvalContext) SetVariables(vs map[string]string) { - c.SetVariablesCalled = true - c.SetVariablesVariables = vs -} - -func (c *MockEvalContext) Diff() (*Diff, *sync.RWMutex) { - c.DiffCalled = true - return c.DiffDiff, c.DiffLock -} - -func (c *MockEvalContext) State() (*State, *sync.RWMutex) { - c.StateCalled = true - return c.StateState, c.StateLock -} diff --git a/terraform/eval_context_mock.go b/terraform/eval_context_mock.go new file mode 100644 index 0000000000..3190f680ac --- /dev/null +++ b/terraform/eval_context_mock.go @@ -0,0 +1,166 @@ +package terraform + +import ( + "sync" + + "github.com/hashicorp/terraform/config" +) + +// MockEvalContext is a mock version of EvalContext that can be used +// for tests. +type MockEvalContext struct { + HookCalled bool + HookHook Hook + HookError error + + InputCalled bool + InputInput UIInput + + InitProviderCalled bool + InitProviderName string + InitProviderProvider ResourceProvider + InitProviderError error + + ProviderCalled bool + ProviderName string + ProviderProvider ResourceProvider + + ProviderInputCalled bool + ProviderInputName string + ProviderInputConfig map[string]interface{} + + SetProviderInputCalled bool + SetProviderInputName string + SetProviderInputConfig map[string]interface{} + + ConfigureProviderCalled bool + ConfigureProviderName string + ConfigureProviderConfig *ResourceConfig + ConfigureProviderError error + + ParentProviderConfigCalled bool + ParentProviderConfigName string + ParentProviderConfigConfig *ResourceConfig + + InitProvisionerCalled bool + InitProvisionerName string + InitProvisionerProvisioner ResourceProvisioner + InitProvisionerError error + + ProvisionerCalled bool + ProvisionerName string + ProvisionerProvisioner ResourceProvisioner + + InterpolateCalled bool + InterpolateConfig *config.RawConfig + InterpolateResource *Resource + InterpolateConfigResult *ResourceConfig + InterpolateError error + + PathCalled bool + PathPath []string + + SetVariablesCalled bool + SetVariablesVariables map[string]string + + DiffCalled bool + DiffDiff *Diff + DiffLock *sync.RWMutex + + StateCalled bool + StateState *State + StateLock *sync.RWMutex +} + +func (c *MockEvalContext) Hook(fn func(Hook) (HookAction, error)) error { + c.HookCalled = true + if c.HookHook != nil { + if _, err := fn(c.HookHook); err != nil { + return err + } + } + + return c.HookError +} + +func (c *MockEvalContext) Input() UIInput { + c.InputCalled = true + return c.InputInput +} + +func (c *MockEvalContext) InitProvider(n string) (ResourceProvider, error) { + c.InitProviderCalled = true + c.InitProviderName = n + return c.InitProviderProvider, c.InitProviderError +} + +func (c *MockEvalContext) Provider(n string) ResourceProvider { + c.ProviderCalled = true + c.ProviderName = n + return c.ProviderProvider +} + +func (c *MockEvalContext) ConfigureProvider(n string, cfg *ResourceConfig) error { + c.ConfigureProviderCalled = true + c.ConfigureProviderName = n + c.ConfigureProviderConfig = cfg + return c.ConfigureProviderError +} + +func (c *MockEvalContext) ParentProviderConfig(n string) *ResourceConfig { + c.ParentProviderConfigCalled = true + c.ParentProviderConfigName = n + return c.ParentProviderConfigConfig +} + +func (c *MockEvalContext) ProviderInput(n string) map[string]interface{} { + c.ProviderInputCalled = true + c.ProviderInputName = n + return c.ProviderInputConfig +} + +func (c *MockEvalContext) SetProviderInput(n string, cfg map[string]interface{}) { + c.SetProviderInputCalled = true + c.SetProviderInputName = n + c.SetProviderInputConfig = cfg +} + +func (c *MockEvalContext) InitProvisioner(n string) (ResourceProvisioner, error) { + c.InitProvisionerCalled = true + c.InitProvisionerName = n + return c.InitProvisionerProvisioner, c.InitProvisionerError +} + +func (c *MockEvalContext) Provisioner(n string) ResourceProvisioner { + c.ProvisionerCalled = true + c.ProvisionerName = n + return c.ProvisionerProvisioner +} + +func (c *MockEvalContext) Interpolate( + config *config.RawConfig, resource *Resource) (*ResourceConfig, error) { + c.InterpolateCalled = true + c.InterpolateConfig = config + c.InterpolateResource = resource + return c.InterpolateConfigResult, c.InterpolateError +} + +func (c *MockEvalContext) Path() []string { + c.PathCalled = true + return c.PathPath +} + +func (c *MockEvalContext) SetVariables(vs map[string]string) { + c.SetVariablesCalled = true + c.SetVariablesVariables = vs +} + +func (c *MockEvalContext) Diff() (*Diff, *sync.RWMutex) { + c.DiffCalled = true + return c.DiffDiff, c.DiffLock +} + +func (c *MockEvalContext) State() (*State, *sync.RWMutex) { + c.StateCalled = true + return c.StateState, c.StateLock +} diff --git a/terraform/eval_state.go b/terraform/eval_state.go index bf8fec0801..d584c0363b 100644 --- a/terraform/eval_state.go +++ b/terraform/eval_state.go @@ -58,6 +58,28 @@ func (n *EvalReadState) Eval(ctx EvalContext) (interface{}, error) { return result, nil } +// EvalUpdateStateHook is an EvalNode implementation that calls the +// PostStateUpdate hook with the current state. +type EvalUpdateStateHook struct{} + +func (n *EvalUpdateStateHook) Eval(ctx EvalContext) (interface{}, error) { + state, lock := ctx.State() + + // Get a read lock so it doesn't change while we're calling this + lock.RLock() + defer lock.RUnlock() + + // Call the hook + err := ctx.Hook(func(h Hook) (HookAction, error) { + return h.PostStateUpdate(state) + }) + if err != nil { + return nil, err + } + + return nil, nil +} + // EvalWriteState is an EvalNode implementation that reads the // InstanceState for a specific resource out of the state. type EvalWriteState struct { @@ -111,7 +133,6 @@ func (n *EvalWriteState) Eval(ctx EvalContext) (interface{}, error) { // Set the primary state rs.Primary = *n.State } - println(fmt.Sprintf("%#v", rs)) return nil, nil } diff --git a/terraform/eval_state_test.go b/terraform/eval_state_test.go new file mode 100644 index 0000000000..0e9e988082 --- /dev/null +++ b/terraform/eval_state_test.go @@ -0,0 +1,27 @@ +package terraform + +import ( + "sync" + "testing" +) + +func TestEvalUpdateStateHook(t *testing.T) { + mockHook := new(MockHook) + + ctx := new(MockEvalContext) + ctx.HookHook = mockHook + ctx.StateState = &State{Serial: 42} + ctx.StateLock = new(sync.RWMutex) + + node := &EvalUpdateStateHook{} + if _, err := node.Eval(ctx); err != nil { + t.Fatalf("err: %s", err) + } + + if !mockHook.PostStateUpdateCalled { + t.Fatal("should call PostStateUpdate") + } + if mockHook.PostStateUpdateState.Serial != 42 { + t.Fatalf("bad: %#v", mockHook.PostStateUpdateState) + } +} diff --git a/terraform/hook.go b/terraform/hook.go index e4ad420165..79e69c6353 100644 --- a/terraform/hook.go +++ b/terraform/hook.go @@ -49,6 +49,9 @@ type Hook interface { // resource state is refreshed, respectively. PreRefresh(*InstanceInfo, *InstanceState) (HookAction, error) PostRefresh(*InstanceInfo, *InstanceState) (HookAction, error) + + // PostStateUpdate is called after the state is updated. + PostStateUpdate(*State) (HookAction, error) } // NilHook is a Hook implementation that does nothing. It exists only to @@ -100,6 +103,10 @@ func (*NilHook) PostRefresh(*InstanceInfo, *InstanceState) (HookAction, error) { return HookActionContinue, nil } +func (*NilHook) PostStateUpdate(*State) (HookAction, error) { + return HookActionContinue, nil +} + // handleHook turns hook actions into panics. This lets you use the // panic/recover mechanism in Go as a flow control mechanism for hook // actions. diff --git a/terraform/hook_mock.go b/terraform/hook_mock.go index b2b6a6e6f5..d30ab8f068 100644 --- a/terraform/hook_mock.go +++ b/terraform/hook_mock.go @@ -69,6 +69,11 @@ type MockHook struct { PreRefreshState *InstanceState PreRefreshReturn HookAction PreRefreshError error + + PostStateUpdateCalled bool + PostStateUpdateState *State + PostStateUpdateReturn HookAction + PostStateUpdateError error } func (h *MockHook) PreApply(n *InstanceInfo, s *InstanceState, d *InstanceDiff) (HookAction, error) { @@ -152,3 +157,9 @@ func (h *MockHook) PostRefresh(n *InstanceInfo, s *InstanceState) (HookAction, e h.PostRefreshState = s return h.PostRefreshReturn, h.PostRefreshError } + +func (h *MockHook) PostStateUpdate(s *State) (HookAction, error) { + h.PostStateUpdateCalled = true + h.PostStateUpdateState = s + return h.PostStateUpdateReturn, h.PostStateUpdateError +} diff --git a/terraform/hook_stop.go b/terraform/hook_stop.go index 0dc1ad7b40..34713221ed 100644 --- a/terraform/hook_stop.go +++ b/terraform/hook_stop.go @@ -53,6 +53,10 @@ func (h *stopHook) PostRefresh(*InstanceInfo, *InstanceState) (HookAction, error return h.hook() } +func (h *stopHook) PostStateUpdate(*State) (HookAction, error) { + return h.hook() +} + func (h *stopHook) hook() (HookAction, error) { if h.Stopped() { return HookActionHalt, nil From 821536b1e998115a27fec5bc216e8d612a80df45 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 23 Feb 2015 19:13:25 -0800 Subject: [PATCH 02/11] terraform: call the EvalUpdateStateHook strategically --- terraform/transform_orphan.go | 1 + terraform/transform_resource.go | 1 + terraform/transform_tainted.go | 1 + 3 files changed, 3 insertions(+) diff --git a/terraform/transform_orphan.go b/terraform/transform_orphan.go index 780d8430b2..e2a9c7dcd4 100644 --- a/terraform/transform_orphan.go +++ b/terraform/transform_orphan.go @@ -262,6 +262,7 @@ func (n *graphNodeOrphanResource) EvalTree() EvalNode { Dependencies: n.DependentOn(), State: &state, }, + &EvalUpdateStateHook{}, }, }, }) diff --git a/terraform/transform_resource.go b/terraform/transform_resource.go index 0ce4f4cd78..19c9c418a0 100644 --- a/terraform/transform_resource.go +++ b/terraform/transform_resource.go @@ -400,6 +400,7 @@ func (n *graphNodeExpandedResource) EvalTree() EvalNode { State: &state, Error: &err, }, + &EvalUpdateStateHook{}, }, }, }) diff --git a/terraform/transform_tainted.go b/terraform/transform_tainted.go index 50223a9e85..1e1b2cb314 100644 --- a/terraform/transform_tainted.go +++ b/terraform/transform_tainted.go @@ -163,6 +163,7 @@ func (n *graphNodeTaintedResource) EvalTree() EvalNode { Tainted: &tainted, TaintedIndex: n.Index, }, + &EvalUpdateStateHook{}, }, }, }) From c2bf60060359b0570a16f30686a4bea63083bf11 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 23 Feb 2015 21:26:33 -0800 Subject: [PATCH 03/11] state: only change serial if changed --- state/cache.go | 2 ++ state/inmem.go | 1 + state/local.go | 10 ++++++++-- state/testing.go | 44 +++++++++++++++++++++++++++++++++++++---- terraform/state.go | 16 ++++++++++++--- terraform/state_test.go | 13 ------------ 6 files changed, 64 insertions(+), 22 deletions(-) diff --git a/state/cache.go b/state/cache.go index 3322ab5acb..0f0f306a08 100644 --- a/state/cache.go +++ b/state/cache.go @@ -104,6 +104,8 @@ func (s *CacheState) RefreshState() error { s.refreshResult = CacheRefreshNoop return err } + + cached = durable } s.state = cached diff --git a/state/inmem.go b/state/inmem.go index 82385a6dfd..68c2ad0c3e 100644 --- a/state/inmem.go +++ b/state/inmem.go @@ -18,6 +18,7 @@ func (s *InmemState) RefreshState() error { } func (s *InmemState) WriteState(state *terraform.State) error { + state.IncrementSerialMaybe(s.state) s.state = state return nil } diff --git a/state/local.go b/state/local.go index 1840cae1a8..30c3093aa9 100644 --- a/state/local.go +++ b/state/local.go @@ -15,13 +15,15 @@ type LocalState struct { Path string PathOut string - state *terraform.State - written bool + state *terraform.State + readState *terraform.State + written bool } // SetState will force a specific state in-memory for this local state. func (s *LocalState) SetState(state *terraform.State) { s.state = state + s.readState = state } // StateReader impl. @@ -61,6 +63,9 @@ func (s *LocalState) WriteState(state *terraform.State) error { } defer f.Close() + s.state.IncrementSerialMaybe(s.readState) + s.readState = s.state + if err := terraform.WriteState(s.state, f); err != nil { return err } @@ -105,5 +110,6 @@ func (s *LocalState) RefreshState() error { } s.state = state + s.readState = state return nil } diff --git a/state/testing.go b/state/testing.go index 3ada81e40c..7efd782d81 100644 --- a/state/testing.go +++ b/state/testing.go @@ -28,9 +28,7 @@ func TestState(t *testing.T, s interface{}) { current := TestStateInitial() // Check that the initial state is correct - state := reader.State() - current.Serial = state.Serial - if !reflect.DeepEqual(state, current) { + if state := reader.State(); !reflect.DeepEqual(state, current) { t.Fatalf("not initial: %#v\n\n%#v", state, current) } @@ -67,11 +65,49 @@ func TestState(t *testing.T, s interface{}) { // Just set the serials the same... Then compare. actual := reader.State() - actual.Serial = current.Serial if !reflect.DeepEqual(actual, current) { t.Fatalf("bad: %#v\n\n%#v", actual, current) } } + + // If we can write and persist then verify that the serial + // is only implemented on change. + writer, writeOk := s.(StateWriter) + persister, persistOk := s.(StatePersister) + if writeOk && persistOk { + // Same serial + serial := current.Serial + if err := writer.WriteState(current); err != nil { + t.Fatalf("err: %s", err) + } + if err := persister.PersistState(); err != nil { + t.Fatalf("err: %s", err) + } + + if reader.State().Serial != serial { + t.Fatalf("bad: expected %d, got %d", serial, reader.State().Serial) + } + + // Change the serial + currentCopy := *current + current = ¤tCopy + current.Modules = []*terraform.ModuleState{ + &terraform.ModuleState{ + Path: []string{"root", "somewhere"}, + Outputs: map[string]string{"serialCheck": "true"}, + }, + } + if err := writer.WriteState(current); err != nil { + t.Fatalf("err: %s", err) + } + if err := persister.PersistState(); err != nil { + t.Fatalf("err: %s", err) + } + + if reader.State().Serial <= serial { + t.Fatalf("bad: expected %d, got %d", serial, reader.State().Serial) + } + } } // TestStateInitial is the initial state that a State should have diff --git a/terraform/state.go b/terraform/state.go index 3ef3f3afe2..68b5c11c6f 100644 --- a/terraform/state.go +++ b/terraform/state.go @@ -161,6 +161,11 @@ func (s *State) RootModule() *ModuleState { // Equal tests if one state is equal to another. func (s *State) Equal(other *State) bool { + // If one is nil, we do a direct check + if s == nil || other == nil { + return s == other + } + // If the versions are different, they're certainly not equal if s.Version != other.Version { return false @@ -183,6 +188,14 @@ func (s *State) Equal(other *State) bool { return true } +// IncrementSerialMaybe increments the serial number of this state +// if it different from the other state. +func (s *State) IncrementSerialMaybe(other *State) { + if !s.Equal(other) { + s.Serial++ + } +} + func (s *State) init() { if s.Version == 0 { s.Version = StateVersion @@ -951,9 +964,6 @@ func WriteState(d *State, dst io.Writer) error { // Ensure the version is set d.Version = StateVersion - // Always increment the serial number - d.Serial++ - // Encode the data in a human-friendly way data, err := json.MarshalIndent(d, "", " ") if err != nil { diff --git a/terraform/state_test.go b/terraform/state_test.go index fc969e9737..aaa049f4b5 100644 --- a/terraform/state_test.go +++ b/terraform/state_test.go @@ -537,15 +537,6 @@ func TestReadWriteState(t *testing.T) { t.Fatalf("bad version number: %d", state.Version) } - // Verify the serial number is incremented - if state.Serial != 10 { - t.Fatalf("bad serial: %d", state.Serial) - } - - // Remove the changes or the checksum will fail - state.Version = 0 - state.Serial = 9 - // Checksum after the write chksumAfter := checksumStruct(t, state) if chksumAfter != chksum { @@ -557,10 +548,6 @@ func TestReadWriteState(t *testing.T) { t.Fatalf("err: %s", err) } - // Verify the changes came through - state.Version = StateVersion - state.Serial = 10 - // ReadState should not restore sensitive information! mod := state.RootModule() mod.Resources["foo"].Primary.Ephemeral = EphemeralState{} From ed6128aa6ecdc6122641db745c1117bb6c9abfc8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 23 Feb 2015 21:30:59 -0800 Subject: [PATCH 04/11] state/remote: increment serial properly --- state/remote/state.go | 5 ++++- state/remote/state_test.go | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/state/remote/state.go b/state/remote/state.go index 5137744ec4..c0abca40e7 100644 --- a/state/remote/state.go +++ b/state/remote/state.go @@ -13,7 +13,7 @@ import ( type State struct { Client Client - state *terraform.State + state, readState *terraform.State } // StateReader impl. @@ -43,11 +43,14 @@ func (s *State) RefreshState() error { } s.state = state + s.readState = state return nil } // StatePersister impl. func (s *State) PersistState() error { + s.state.IncrementSerialMaybe(s.readState) + var buf bytes.Buffer if err := terraform.WriteState(s.state, &buf); err != nil { return err diff --git a/state/remote/state_test.go b/state/remote/state_test.go index 08b51439b1..4878916675 100644 --- a/state/remote/state_test.go +++ b/state/remote/state_test.go @@ -7,8 +7,11 @@ import ( ) func TestState(t *testing.T) { - s := &State{Client: new(InmemClient)} - s.WriteState(state.TestStateInitial()) + s := &State{ + Client: new(InmemClient), + state: state.TestStateInitial(), + readState: state.TestStateInitial(), + } if err := s.PersistState(); err != nil { t.Fatalf("err: %s", err) } From f3af221866785450257edc7c117f3069adf4eb20 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 23 Feb 2015 21:32:27 -0800 Subject: [PATCH 05/11] terraform: make DeepCopy public --- terraform/context.go | 6 +++--- terraform/state.go | 38 ++++++++++++++++++++------------------ 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/terraform/context.go b/terraform/context.go index 97abe573c7..e2db6e6f93 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -224,7 +224,7 @@ func (c *Context) Apply() (*State, error) { defer c.releaseRun(v) // Copy our own state - c.state = c.state.deepcopy() + c.state = c.state.DeepCopy() // Do the walk _, err := c.walk(walkApply) @@ -264,7 +264,7 @@ func (c *Context) Plan(opts *PlanOpts) (*Plan, error) { c.state = &State{} c.state.init() } else { - c.state = old.deepcopy() + c.state = old.DeepCopy() } defer func() { c.state = old @@ -299,7 +299,7 @@ func (c *Context) Refresh() (*State, error) { defer c.releaseRun(v) // Copy our own state - c.state = c.state.deepcopy() + c.state = c.state.DeepCopy() // Do the walk if _, err := c.walk(walkRefresh); err != nil { diff --git a/terraform/state.go b/terraform/state.go index 68b5c11c6f..f94d9bedca 100644 --- a/terraform/state.go +++ b/terraform/state.go @@ -188,6 +188,26 @@ func (s *State) Equal(other *State) bool { return true } +// DeepCopy performs a deep copy of the state structure and returns +// a new structure. +func (s *State) DeepCopy() *State { + if s == nil { + return nil + } + n := &State{ + Version: s.Version, + Serial: s.Serial, + Modules: make([]*ModuleState, 0, len(s.Modules)), + } + for _, mod := range s.Modules { + n.Modules = append(n.Modules, mod.deepcopy()) + } + if s.Remote != nil { + n.Remote = s.Remote.deepcopy() + } + return n +} + // IncrementSerialMaybe increments the serial number of this state // if it different from the other state. func (s *State) IncrementSerialMaybe(other *State) { @@ -209,24 +229,6 @@ func (s *State) init() { } } -func (s *State) deepcopy() *State { - if s == nil { - return nil - } - n := &State{ - Version: s.Version, - Serial: s.Serial, - Modules: make([]*ModuleState, 0, len(s.Modules)), - } - for _, mod := range s.Modules { - n.Modules = append(n.Modules, mod.deepcopy()) - } - if s.Remote != nil { - n.Remote = s.Remote.deepcopy() - } - return n -} - // prune is used to remove any resources that are no longer required func (s *State) prune() { if s == nil { From cc8e6b6331cc1626db029d3dfba15c8f102d28fe Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 23 Feb 2015 21:36:35 -0800 Subject: [PATCH 06/11] state: deep copies are required --- state/cache.go | 2 +- state/inmem.go | 2 +- state/local.go | 2 +- state/remote/state.go | 2 +- state/testing.go | 12 +++++++++--- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/state/cache.go b/state/cache.go index 0f0f306a08..a20eb4a067 100644 --- a/state/cache.go +++ b/state/cache.go @@ -19,7 +19,7 @@ type CacheState struct { // StateReader impl. func (s *CacheState) State() *terraform.State { - return s.state + return s.state.DeepCopy() } // WriteState will write and persist the state to the cache. diff --git a/state/inmem.go b/state/inmem.go index 68c2ad0c3e..ff8daab8fa 100644 --- a/state/inmem.go +++ b/state/inmem.go @@ -10,7 +10,7 @@ type InmemState struct { } func (s *InmemState) State() *terraform.State { - return s.state + return s.state.DeepCopy() } func (s *InmemState) RefreshState() error { diff --git a/state/local.go b/state/local.go index 30c3093aa9..02afb1ed7d 100644 --- a/state/local.go +++ b/state/local.go @@ -28,7 +28,7 @@ func (s *LocalState) SetState(state *terraform.State) { // StateReader impl. func (s *LocalState) State() *terraform.State { - return s.state + return s.state.DeepCopy() } // WriteState for LocalState always persists the state as well. diff --git a/state/remote/state.go b/state/remote/state.go index c0abca40e7..e679b5d73e 100644 --- a/state/remote/state.go +++ b/state/remote/state.go @@ -18,7 +18,7 @@ type State struct { // StateReader impl. func (s *State) State() *terraform.State { - return s.state + return s.state.DeepCopy() } // StateWriter impl. diff --git a/state/testing.go b/state/testing.go index 7efd782d81..6a4a88ad0c 100644 --- a/state/testing.go +++ b/state/testing.go @@ -28,7 +28,7 @@ func TestState(t *testing.T, s interface{}) { current := TestStateInitial() // Check that the initial state is correct - if state := reader.State(); !reflect.DeepEqual(state, current) { + if state := reader.State(); !current.Equal(state) { t.Fatalf("not initial: %#v\n\n%#v", state, current) } @@ -45,7 +45,7 @@ func TestState(t *testing.T, s interface{}) { t.Fatalf("err: %s", err) } - if actual := reader.State(); !reflect.DeepEqual(actual, current) { + if actual := reader.State(); !actual.Equal(current) { t.Fatalf("bad: %#v\n\n%#v", actual, current) } } @@ -65,7 +65,7 @@ func TestState(t *testing.T, s interface{}) { // Just set the serials the same... Then compare. actual := reader.State() - if !reflect.DeepEqual(actual, current) { + if !actual.Equal(current) { t.Fatalf("bad: %#v\n\n%#v", actual, current) } } @@ -107,6 +107,12 @@ func TestState(t *testing.T, s interface{}) { if reader.State().Serial <= serial { t.Fatalf("bad: expected %d, got %d", serial, reader.State().Serial) } + + // Check that State() returns a copy + reader.State().Serial++ + if reflect.DeepEqual(reader.State(), current) { + t.Fatal("State() should return a copy") + } } } From 57f7507ebd7161a12559673024cba345d5fc66cc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 23 Feb 2015 21:43:54 -0800 Subject: [PATCH 07/11] terraform: more state tests, fix a bug --- terraform/state.go | 3 +++ terraform/state_test.go | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/terraform/state.go b/terraform/state.go index f94d9bedca..5b695c33cf 100644 --- a/terraform/state.go +++ b/terraform/state.go @@ -172,6 +172,9 @@ func (s *State) Equal(other *State) bool { } // If any of the modules are not equal, then this state isn't equal + if len(s.Modules) != len(other.Modules) { + return false + } for _, m := range s.Modules { // This isn't very optimal currently but works. otherM := other.ModuleByPath(m.Path) diff --git a/terraform/state_test.go b/terraform/state_test.go index aaa049f4b5..ef2aa0b595 100644 --- a/terraform/state_test.go +++ b/terraform/state_test.go @@ -116,6 +116,19 @@ func TestStateEqual(t *testing.T) { Result bool One, Two *State }{ + // Nils + { + false, + nil, + &State{Version: 2}, + }, + + { + true, + nil, + nil, + }, + // Different versions { false, @@ -159,6 +172,9 @@ func TestStateEqual(t *testing.T) { if tc.One.Equal(tc.Two) != tc.Result { t.Fatalf("Bad: %d\n\n%s\n\n%s", i, tc.One.String(), tc.Two.String()) } + if tc.Two.Equal(tc.One) != tc.Result { + t.Fatalf("Bad: %d\n\n%s\n\n%s", i, tc.One.String(), tc.Two.String()) + } } } From 95cf69aa3284248ae301024fdd83e0bcbf927d9f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 23 Feb 2015 21:57:17 -0800 Subject: [PATCH 08/11] command: StateHook for continous state updates --- command/apply.go | 15 ++++++++++++++- command/hook_state.go | 33 +++++++++++++++++++++++++++++++++ command/hook_state_test.go | 29 +++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 command/hook_state.go create mode 100644 command/hook_state_test.go diff --git a/command/apply.go b/command/apply.go index a568b04312..d46b716798 100644 --- a/command/apply.go +++ b/command/apply.go @@ -68,7 +68,8 @@ func (c *ApplyCommand) Run(args []string) int { // Prepare the extra hooks to count resources countHook := new(CountHook) - c.Meta.extraHooks = []terraform.Hook{countHook} + stateHook := new(StateHook) + c.Meta.extraHooks = []terraform.Hook{countHook, stateHook} if !c.Destroy && maybeInit { // Do a detect to determine if we need to do an init + apply. @@ -151,6 +152,18 @@ func (c *ApplyCommand) Run(args []string) int { } } + // Setup the state hook for continous state updates + { + state, err := c.State() + if err != nil { + c.Ui.Error(fmt.Sprintf( + "Error reading state: %s", err)) + return 1 + } + + stateHook.State = state + } + // Start the apply in a goroutine so that we can be interrupted. var state *terraform.State var applyErr error diff --git a/command/hook_state.go b/command/hook_state.go new file mode 100644 index 0000000000..ab5c47a114 --- /dev/null +++ b/command/hook_state.go @@ -0,0 +1,33 @@ +package command + +import ( + "sync" + + "github.com/hashicorp/terraform/state" + "github.com/hashicorp/terraform/terraform" +) + +// StateHook is a hook that continuously updates the state by calling +// WriteState on a state.State. +type StateHook struct { + terraform.NilHook + sync.Mutex + + State state.State +} + +func (h *StateHook) PostStateUpdate( + s *terraform.State) (terraform.HookAction, error) { + h.Lock() + defer h.Unlock() + + if h.State != nil { + // Write the new state + if err := h.State.WriteState(s); err != nil { + return terraform.HookActionHalt, err + } + } + + // Continue forth + return terraform.HookActionContinue, nil +} diff --git a/command/hook_state_test.go b/command/hook_state_test.go new file mode 100644 index 0000000000..0d0fd7927f --- /dev/null +++ b/command/hook_state_test.go @@ -0,0 +1,29 @@ +package command + +import ( + "testing" + + "github.com/hashicorp/terraform/state" + "github.com/hashicorp/terraform/terraform" +) + +func TestStateHook_impl(t *testing.T) { + var _ terraform.Hook = new(StateHook) +} + +func TestStateHook(t *testing.T) { + is := &state.InmemState{} + var hook terraform.Hook = &StateHook{State: is} + + s := state.TestStateInitial() + action, err := hook.PostStateUpdate(s) + if err != nil { + t.Fatalf("err: %s", err) + } + if action != terraform.HookActionContinue { + t.Fatalf("bad: %v", action) + } + if !is.State().Equal(s) { + t.Fatalf("bad state: %#v", is.State()) + } +} From ac167c308217262e5dea14edd4033be009f647eb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 23 Feb 2015 22:10:31 -0800 Subject: [PATCH 09/11] terraform: test post state update is called --- terraform/context_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/terraform/context_test.go b/terraform/context_test.go index e4359685f7..5ee9a8c71b 100644 --- a/terraform/context_test.go +++ b/terraform/context_test.go @@ -4454,6 +4454,9 @@ func TestContext2Apply_hook(t *testing.T) { if !h.PostApplyCalled { t.Fatal("should be called") } + if !h.PostStateUpdateCalled { + t.Fatalf("should call post state update") + } } func TestContext2Apply_idAttr(t *testing.T) { From 2286fc5f20132aecf2a2a9d5b1bd99a76e0b0b01 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 24 Feb 2015 17:36:15 -0800 Subject: [PATCH 10/11] update CHANGELOG --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 857cd7923f..4e2ed6e9b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,18 @@ FEATURES: * **Self-variables** can be used to reference the current resource's attributes within a provisioner. Ex. `${self.private_ip_address}` [GH-1033] + * **Continous state** saving during `terraform apply`. The state file is + continously updated as apply is running, meaning that the state is + less likely to become corrupt in a catastrophic case: terraform panic + or system killing Terraform. IMPROVEMENTS: * **New config function: `split`** - Split a value based on a delimiter. This is useful for faking lists as parameters to modules. + * core: The serial of the state is only updated if there is an actual + change. This will lower the amount of state changing on things + like refresh. BUG FIXES: From 92bf85925b87affb52d02ca4c5a0c48e4157d721 Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Fri, 20 Feb 2015 16:26:43 -0600 Subject: [PATCH 11/11] providers/aws: Convert Launch Configurations to awslabs/aws-sdk-go --- builtin/providers/aws/config.go | 22 +++--- .../aws/resource_aws_autoscaling_group.go | 10 +-- .../resource_aws_autoscaling_group_test.go | 63 ++++++++++------ .../aws/resource_aws_launch_configuration.go | 75 +++++++++++++------ .../resource_aws_launch_configuration_test.go | 29 +++---- 5 files changed, 119 insertions(+), 80 deletions(-) diff --git a/builtin/providers/aws/config.go b/builtin/providers/aws/config.go index 47e1c00fbb..7a1896c146 100644 --- a/builtin/providers/aws/config.go +++ b/builtin/providers/aws/config.go @@ -7,14 +7,13 @@ import ( "unicode" "github.com/hashicorp/terraform/helper/multierror" - "github.com/mitchellh/goamz/autoscaling" "github.com/mitchellh/goamz/aws" "github.com/mitchellh/goamz/ec2" "github.com/mitchellh/goamz/elb" "github.com/mitchellh/goamz/rds" awsGo "github.com/awslabs/aws-sdk-go/aws" - awsAutoScaling "github.com/awslabs/aws-sdk-go/gen/autoscaling" + "github.com/awslabs/aws-sdk-go/gen/autoscaling" "github.com/awslabs/aws-sdk-go/gen/route53" "github.com/awslabs/aws-sdk-go/gen/s3" ) @@ -26,14 +25,13 @@ type Config struct { } type AWSClient struct { - ec2conn *ec2.EC2 - elbconn *elb.ELB - autoscalingconn *autoscaling.AutoScaling - s3conn *s3.S3 - rdsconn *rds.Rds - r53conn *route53.Route53 - region string - awsAutoScalingconn *awsAutoScaling.AutoScaling + ec2conn *ec2.EC2 + elbconn *elb.ELB + autoscalingconn *autoscaling.AutoScaling + s3conn *s3.S3 + rdsconn *rds.Rds + r53conn *route53.Route53 + region string } // Client configures and returns a fully initailized AWSClient @@ -67,7 +65,7 @@ func (c *Config) Client() (interface{}, error) { log.Println("[INFO] Initializing ELB connection") client.elbconn = elb.New(auth, region) log.Println("[INFO] Initializing AutoScaling connection") - client.autoscalingconn = autoscaling.New(auth, region) + client.autoscalingconn = autoscaling.New(creds, c.Region, nil) log.Println("[INFO] Initializing S3 connection") client.s3conn = s3.New(creds, c.Region, nil) log.Println("[INFO] Initializing RDS connection") @@ -78,8 +76,6 @@ func (c *Config) Client() (interface{}, error) { // See http://docs.aws.amazon.com/general/latest/gr/sigv4_changes.html log.Println("[INFO] Initializing Route53 connection") client.r53conn = route53.New(creds, "us-east-1", nil) - log.Println("[INFO] Initializing AWS Go AutoScaling connection") - client.awsAutoScalingconn = awsAutoScaling.New(creds, c.Region, nil) } if len(errs) > 0 { diff --git a/builtin/providers/aws/resource_aws_autoscaling_group.go b/builtin/providers/aws/resource_aws_autoscaling_group.go index 2a285cf189..ebe9ac72d1 100644 --- a/builtin/providers/aws/resource_aws_autoscaling_group.go +++ b/builtin/providers/aws/resource_aws_autoscaling_group.go @@ -123,7 +123,7 @@ func resourceAwsAutoscalingGroup() *schema.Resource { } func resourceAwsAutoscalingGroupCreate(d *schema.ResourceData, meta interface{}) error { - autoscalingconn := meta.(*AWSClient).awsAutoScalingconn + autoscalingconn := meta.(*AWSClient).autoscalingconn var autoScalingGroupOpts autoscaling.CreateAutoScalingGroupType autoScalingGroupOpts.AutoScalingGroupName = aws.String(d.Get("name").(string)) @@ -199,7 +199,7 @@ func resourceAwsAutoscalingGroupRead(d *schema.ResourceData, meta interface{}) e } func resourceAwsAutoscalingGroupUpdate(d *schema.ResourceData, meta interface{}) error { - autoscalingconn := meta.(*AWSClient).awsAutoScalingconn + autoscalingconn := meta.(*AWSClient).autoscalingconn opts := autoscaling.UpdateAutoScalingGroupType{ AutoScalingGroupName: aws.String(d.Id()), @@ -232,7 +232,7 @@ func resourceAwsAutoscalingGroupUpdate(d *schema.ResourceData, meta interface{}) } func resourceAwsAutoscalingGroupDelete(d *schema.ResourceData, meta interface{}) error { - autoscalingconn := meta.(*AWSClient).awsAutoScalingconn + autoscalingconn := meta.(*AWSClient).autoscalingconn // Read the autoscaling group first. If it doesn't exist, we're done. // We need the group in order to check if there are instances attached. @@ -276,7 +276,7 @@ func resourceAwsAutoscalingGroupDelete(d *schema.ResourceData, meta interface{}) func getAwsAutoscalingGroup( d *schema.ResourceData, meta interface{}) (*autoscaling.AutoScalingGroup, error) { - autoscalingconn := meta.(*AWSClient).awsAutoScalingconn + autoscalingconn := meta.(*AWSClient).autoscalingconn describeOpts := autoscaling.AutoScalingGroupNamesType{ AutoScalingGroupNames: []string{d.Id()}, @@ -307,7 +307,7 @@ func getAwsAutoscalingGroup( } func resourceAwsAutoscalingGroupDrain(d *schema.ResourceData, meta interface{}) error { - autoscalingconn := meta.(*AWSClient).awsAutoScalingconn + autoscalingconn := meta.(*AWSClient).autoscalingconn // First, set the capacity to zero so the group will drain log.Printf("[DEBUG] Reducing autoscaling group capacity to zero") diff --git a/builtin/providers/aws/resource_aws_autoscaling_group_test.go b/builtin/providers/aws/resource_aws_autoscaling_group_test.go index 531d518434..1d87f17880 100644 --- a/builtin/providers/aws/resource_aws_autoscaling_group_test.go +++ b/builtin/providers/aws/resource_aws_autoscaling_group_test.go @@ -4,9 +4,10 @@ import ( "fmt" "testing" + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/aws-sdk-go/gen/autoscaling" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" - "github.com/mitchellh/goamz/autoscaling" ) func TestAccAWSAutoScalingGroup_basic(t *testing.T) { @@ -51,8 +52,7 @@ func TestAccAWSAutoScalingGroup_basic(t *testing.T) { testAccCheckAWSLaunchConfigurationExists("aws_launch_configuration.new", &lc), resource.TestCheckResourceAttr( "aws_autoscaling_group.bar", "desired_capacity", "5"), - resource.TestCheckResourceAttrPtr( - "aws_autoscaling_group.bar", "launch_configuration", &lc.Name), + testLaunchConfigurationName("aws_autoscaling_group.bar", &lc), ), }, }, @@ -87,19 +87,19 @@ func testAccCheckAWSAutoScalingGroupDestroy(s *terraform.State) error { // Try to find the Group describeGroups, err := conn.DescribeAutoScalingGroups( - &autoscaling.DescribeAutoScalingGroups{ - Names: []string{rs.Primary.ID}, + &autoscaling.AutoScalingGroupNamesType{ + AutoScalingGroupNames: []string{rs.Primary.ID}, }) if err == nil { if len(describeGroups.AutoScalingGroups) != 0 && - describeGroups.AutoScalingGroups[0].Name == rs.Primary.ID { + *describeGroups.AutoScalingGroups[0].AutoScalingGroupName == rs.Primary.ID { return fmt.Errorf("AutoScaling Group still exists") } } // Verify the error - ec2err, ok := err.(*autoscaling.Error) + ec2err, ok := err.(aws.APIError) if !ok { return err } @@ -117,32 +117,32 @@ func testAccCheckAWSAutoScalingGroupAttributes(group *autoscaling.AutoScalingGro return fmt.Errorf("Bad availability_zones: %s", group.AvailabilityZones[0]) } - if group.Name != "foobar3-terraform-test" { - return fmt.Errorf("Bad name: %s", group.Name) + if *group.AutoScalingGroupName != "foobar3-terraform-test" { + return fmt.Errorf("Bad name: %s", *group.AutoScalingGroupName) } - if group.MaxSize != 5 { - return fmt.Errorf("Bad max_size: %d", group.MaxSize) + if *group.MaxSize != 5 { + return fmt.Errorf("Bad max_size: %d", *group.MaxSize) } - if group.MinSize != 2 { - return fmt.Errorf("Bad max_size: %d", group.MinSize) + if *group.MinSize != 2 { + return fmt.Errorf("Bad max_size: %d", *group.MinSize) } - if group.HealthCheckType != "ELB" { - return fmt.Errorf("Bad health_check_type: %s", group.HealthCheckType) + if *group.HealthCheckType != "ELB" { + return fmt.Errorf("Bad health_check_type: %s", *group.HealthCheckType) } - if group.HealthCheckGracePeriod != 300 { - return fmt.Errorf("Bad health_check_grace_period: %d", group.HealthCheckGracePeriod) + if *group.HealthCheckGracePeriod != 300 { + return fmt.Errorf("Bad health_check_grace_period: %d", *group.HealthCheckGracePeriod) } - if group.DesiredCapacity != 4 { - return fmt.Errorf("Bad desired_capacity: %d", group.DesiredCapacity) + if *group.DesiredCapacity != 4 { + return fmt.Errorf("Bad desired_capacity: %d", *group.DesiredCapacity) } - if group.LaunchConfigurationName == "" { - return fmt.Errorf("Bad launch configuration name: %s", group.LaunchConfigurationName) + if *group.LaunchConfigurationName == "" { + return fmt.Errorf("Bad launch configuration name: %s", *group.LaunchConfigurationName) } return nil @@ -172,8 +172,8 @@ func testAccCheckAWSAutoScalingGroupExists(n string, group *autoscaling.AutoScal conn := testAccProvider.Meta().(*AWSClient).autoscalingconn - describeOpts := autoscaling.DescribeAutoScalingGroups{ - Names: []string{rs.Primary.ID}, + describeOpts := autoscaling.AutoScalingGroupNamesType{ + AutoScalingGroupNames: []string{rs.Primary.ID}, } describeGroups, err := conn.DescribeAutoScalingGroups(&describeOpts) @@ -182,7 +182,7 @@ func testAccCheckAWSAutoScalingGroupExists(n string, group *autoscaling.AutoScal } if len(describeGroups.AutoScalingGroups) != 1 || - describeGroups.AutoScalingGroups[0].Name != rs.Primary.ID { + *describeGroups.AutoScalingGroups[0].AutoScalingGroupName != rs.Primary.ID { return fmt.Errorf("AutoScaling Group not found") } @@ -192,6 +192,21 @@ func testAccCheckAWSAutoScalingGroupExists(n string, group *autoscaling.AutoScal } } +func testLaunchConfigurationName(n string, lc *autoscaling.LaunchConfiguration) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if *lc.LaunchConfigurationName != rs.Primary.Attributes["launch_configuration"] { + return fmt.Errorf("Launch configuration names do not match") + } + + return nil + } +} + const testAccAWSAutoScalingGroupConfig = ` resource "aws_launch_configuration" "foobar" { name = "foobarautoscaling-terraform-test" diff --git a/builtin/providers/aws/resource_aws_launch_configuration.go b/builtin/providers/aws/resource_aws_launch_configuration.go index 1092956f7f..021e9417ce 100644 --- a/builtin/providers/aws/resource_aws_launch_configuration.go +++ b/builtin/providers/aws/resource_aws_launch_configuration.go @@ -2,15 +2,17 @@ package aws import ( "crypto/sha1" + "encoding/base64" "encoding/hex" "fmt" "log" "time" + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/aws-sdk-go/gen/autoscaling" "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" - "github.com/mitchellh/goamz/autoscaling" ) func resourceAwsLaunchConfiguration() *schema.Resource { @@ -94,15 +96,26 @@ func resourceAwsLaunchConfiguration() *schema.Resource { func resourceAwsLaunchConfigurationCreate(d *schema.ResourceData, meta interface{}) error { autoscalingconn := meta.(*AWSClient).autoscalingconn - var createLaunchConfigurationOpts autoscaling.CreateLaunchConfiguration - createLaunchConfigurationOpts.Name = d.Get("name").(string) - createLaunchConfigurationOpts.IamInstanceProfile = d.Get("iam_instance_profile").(string) - createLaunchConfigurationOpts.ImageId = d.Get("image_id").(string) - createLaunchConfigurationOpts.InstanceType = d.Get("instance_type").(string) - createLaunchConfigurationOpts.KeyName = d.Get("key_name").(string) - createLaunchConfigurationOpts.UserData = d.Get("user_data").(string) - createLaunchConfigurationOpts.AssociatePublicIpAddress = d.Get("associate_public_ip_address").(bool) - createLaunchConfigurationOpts.SpotPrice = d.Get("spot_price").(string) + var createLaunchConfigurationOpts autoscaling.CreateLaunchConfigurationType + createLaunchConfigurationOpts.LaunchConfigurationName = aws.String(d.Get("name").(string)) + createLaunchConfigurationOpts.ImageID = aws.String(d.Get("image_id").(string)) + createLaunchConfigurationOpts.InstanceType = aws.String(d.Get("instance_type").(string)) + + if v, ok := d.GetOk("user_data"); ok { + createLaunchConfigurationOpts.UserData = aws.String(base64.StdEncoding.EncodeToString([]byte(v.(string)))) + } + if v, ok := d.GetOk("associate_public_ip_address"); ok { + createLaunchConfigurationOpts.AssociatePublicIPAddress = aws.Boolean(v.(bool)) + } + if v, ok := d.GetOk("iam_instance_profile"); ok { + createLaunchConfigurationOpts.IAMInstanceProfile = aws.String(v.(string)) + } + if v, ok := d.GetOk("key_name"); ok { + createLaunchConfigurationOpts.KeyName = aws.String(v.(string)) + } + if v, ok := d.GetOk("spot_price"); ok { + createLaunchConfigurationOpts.SpotPrice = aws.String(v.(string)) + } if v, ok := d.GetOk("security_groups"); ok { createLaunchConfigurationOpts.SecurityGroups = expandStringList( @@ -110,7 +123,7 @@ func resourceAwsLaunchConfigurationCreate(d *schema.ResourceData, meta interface } log.Printf("[DEBUG] autoscaling create launch configuration: %#v", createLaunchConfigurationOpts) - _, err := autoscalingconn.CreateLaunchConfiguration(&createLaunchConfigurationOpts) + err := autoscalingconn.CreateLaunchConfiguration(&createLaunchConfigurationOpts) if err != nil { return fmt.Errorf("Error creating launch configuration: %s", err) } @@ -128,8 +141,8 @@ func resourceAwsLaunchConfigurationCreate(d *schema.ResourceData, meta interface func resourceAwsLaunchConfigurationRead(d *schema.ResourceData, meta interface{}) error { autoscalingconn := meta.(*AWSClient).autoscalingconn - describeOpts := autoscaling.DescribeLaunchConfigurations{ - Names: []string{d.Id()}, + describeOpts := autoscaling.LaunchConfigurationNamesType{ + LaunchConfigurationNames: []string{d.Id()}, } log.Printf("[DEBUG] launch configuration describe configuration: %#v", describeOpts) @@ -143,7 +156,7 @@ func resourceAwsLaunchConfigurationRead(d *schema.ResourceData, meta interface{} } // Verify AWS returned our launch configuration - if describConfs.LaunchConfigurations[0].Name != d.Id() { + if *describConfs.LaunchConfigurations[0].LaunchConfigurationName != d.Id() { return fmt.Errorf( "Unable to find launch configuration: %#v", describConfs.LaunchConfigurations) @@ -151,14 +164,28 @@ func resourceAwsLaunchConfigurationRead(d *schema.ResourceData, meta interface{} lc := describConfs.LaunchConfigurations[0] - d.Set("key_name", lc.KeyName) - d.Set("iam_instance_profile", lc.IamInstanceProfile) - d.Set("image_id", lc.ImageId) - d.Set("instance_type", lc.InstanceType) - d.Set("name", lc.Name) - d.Set("security_groups", lc.SecurityGroups) - d.Set("spot_price", lc.SpotPrice) + d.Set("key_name", *lc.KeyName) + d.Set("image_id", *lc.ImageID) + d.Set("instance_type", *lc.InstanceType) + d.Set("name", *lc.LaunchConfigurationName) + if lc.IAMInstanceProfile != nil { + d.Set("iam_instance_profile", *lc.IAMInstanceProfile) + } else { + d.Set("iam_instance_profile", nil) + } + + if lc.SpotPrice != nil { + d.Set("spot_price", *lc.SpotPrice) + } else { + d.Set("spot_price", nil) + } + + if lc.SecurityGroups != nil { + d.Set("security_groups", lc.SecurityGroups) + } else { + d.Set("security_groups", nil) + } return nil } @@ -166,10 +193,10 @@ func resourceAwsLaunchConfigurationDelete(d *schema.ResourceData, meta interface autoscalingconn := meta.(*AWSClient).autoscalingconn log.Printf("[DEBUG] Launch Configuration destroy: %v", d.Id()) - _, err := autoscalingconn.DeleteLaunchConfiguration( - &autoscaling.DeleteLaunchConfiguration{Name: d.Id()}) + err := autoscalingconn.DeleteLaunchConfiguration( + &autoscaling.LaunchConfigurationNameType{LaunchConfigurationName: aws.String(d.Id())}) if err != nil { - autoscalingerr, ok := err.(*autoscaling.Error) + autoscalingerr, ok := err.(aws.APIError) if ok && autoscalingerr.Code == "InvalidConfiguration.NotFound" { return nil } diff --git a/builtin/providers/aws/resource_aws_launch_configuration_test.go b/builtin/providers/aws/resource_aws_launch_configuration_test.go index 32036af592..711932f419 100644 --- a/builtin/providers/aws/resource_aws_launch_configuration_test.go +++ b/builtin/providers/aws/resource_aws_launch_configuration_test.go @@ -4,9 +4,10 @@ import ( "fmt" "testing" + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/aws-sdk-go/gen/autoscaling" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" - "github.com/mitchellh/goamz/autoscaling" ) func TestAccAWSLaunchConfiguration(t *testing.T) { @@ -57,19 +58,19 @@ func testAccCheckAWSLaunchConfigurationDestroy(s *terraform.State) error { } describe, err := conn.DescribeLaunchConfigurations( - &autoscaling.DescribeLaunchConfigurations{ - Names: []string{rs.Primary.ID}, + &autoscaling.LaunchConfigurationNamesType{ + LaunchConfigurationNames: []string{rs.Primary.ID}, }) if err == nil { if len(describe.LaunchConfigurations) != 0 && - describe.LaunchConfigurations[0].Name == rs.Primary.ID { + *describe.LaunchConfigurations[0].LaunchConfigurationName == rs.Primary.ID { return fmt.Errorf("Launch Configuration still exists") } } // Verify the error - providerErr, ok := err.(*autoscaling.Error) + providerErr, ok := err.(aws.APIError) if !ok { return err } @@ -83,16 +84,16 @@ func testAccCheckAWSLaunchConfigurationDestroy(s *terraform.State) error { func testAccCheckAWSLaunchConfigurationAttributes(conf *autoscaling.LaunchConfiguration) resource.TestCheckFunc { return func(s *terraform.State) error { - if conf.ImageId != "ami-21f78e11" { - return fmt.Errorf("Bad image_id: %s", conf.ImageId) + if *conf.ImageID != "ami-21f78e11" { + return fmt.Errorf("Bad image_id: %s", *conf.ImageID) } - if conf.Name != "foobar-terraform-test" { - return fmt.Errorf("Bad name: %s", conf.Name) + if *conf.LaunchConfigurationName != "foobar-terraform-test" { + return fmt.Errorf("Bad name: %s", *conf.LaunchConfigurationName) } - if conf.InstanceType != "t1.micro" { - return fmt.Errorf("Bad instance_type: %s", conf.InstanceType) + if *conf.InstanceType != "t1.micro" { + return fmt.Errorf("Bad instance_type: %s", *conf.InstanceType) } return nil @@ -112,8 +113,8 @@ func testAccCheckAWSLaunchConfigurationExists(n string, res *autoscaling.LaunchC conn := testAccProvider.Meta().(*AWSClient).autoscalingconn - describeOpts := autoscaling.DescribeLaunchConfigurations{ - Names: []string{rs.Primary.ID}, + describeOpts := autoscaling.LaunchConfigurationNamesType{ + LaunchConfigurationNames: []string{rs.Primary.ID}, } describe, err := conn.DescribeLaunchConfigurations(&describeOpts) @@ -122,7 +123,7 @@ func testAccCheckAWSLaunchConfigurationExists(n string, res *autoscaling.LaunchC } if len(describe.LaunchConfigurations) != 1 || - describe.LaunchConfigurations[0].Name != rs.Primary.ID { + *describe.LaunchConfigurations[0].LaunchConfigurationName != rs.Primary.ID { return fmt.Errorf("Launch Configuration Group not found") }