Merge pull request #31 from hashicorp/f-taint

Tainted Resources
This commit is contained in:
Armon Dadgar 2014-07-22 15:09:53 -04:00
commit 2b68015034
16 changed files with 201 additions and 59 deletions

View File

@ -14,31 +14,31 @@ import (
type ResourceProvisioner struct{} type ResourceProvisioner struct{}
func (p *ResourceProvisioner) Apply(s *terraform.ResourceState, func (p *ResourceProvisioner) Apply(s *terraform.ResourceState,
c *terraform.ResourceConfig) (*terraform.ResourceState, error) { c *terraform.ResourceConfig) error {
// Ensure the connection type is SSH // Ensure the connection type is SSH
if err := helper.VerifySSH(s); err != nil { if err := helper.VerifySSH(s); err != nil {
return s, err return err
} }
// Get the SSH configuration // Get the SSH configuration
conf, err := helper.ParseSSHConfig(s) conf, err := helper.ParseSSHConfig(s)
if err != nil { if err != nil {
return s, err return err
} }
// Get the source and destination // Get the source and destination
sRaw := c.Config["source"] sRaw := c.Config["source"]
src, ok := sRaw.(string) src, ok := sRaw.(string)
if !ok { if !ok {
return s, fmt.Errorf("Unsupported 'source' type! Must be string.") return fmt.Errorf("Unsupported 'source' type! Must be string.")
} }
dRaw := c.Config["destination"] dRaw := c.Config["destination"]
dst, ok := dRaw.(string) dst, ok := dRaw.(string)
if !ok { if !ok {
return s, fmt.Errorf("Unsupported 'destination' type! Must be string.") return fmt.Errorf("Unsupported 'destination' type! Must be string.")
} }
return s, p.copyFiles(conf, src, dst) return p.copyFiles(conf, src, dst)
} }
func (p *ResourceProvisioner) Validate(c *terraform.ResourceConfig) (ws []string, es []error) { func (p *ResourceProvisioner) Validate(c *terraform.ResourceConfig) (ws []string, es []error) {

View File

@ -21,16 +21,16 @@ type ResourceProvisioner struct{}
func (p *ResourceProvisioner) Apply( func (p *ResourceProvisioner) Apply(
s *terraform.ResourceState, s *terraform.ResourceState,
c *terraform.ResourceConfig) (*terraform.ResourceState, error) { c *terraform.ResourceConfig) error {
// Get the command // Get the command
commandRaw, ok := c.Config["command"] commandRaw, ok := c.Config["command"]
if !ok { if !ok {
return s, fmt.Errorf("local-exec provisioner missing 'command'") return fmt.Errorf("local-exec provisioner missing 'command'")
} }
command, ok := commandRaw.(string) command, ok := commandRaw.(string)
if !ok { if !ok {
return s, fmt.Errorf("local-exec provisioner command must be a string") return fmt.Errorf("local-exec provisioner command must be a string")
} }
// Execute the command using a shell // Execute the command using a shell
@ -51,10 +51,10 @@ func (p *ResourceProvisioner) Apply(
// Run the command to completion // Run the command to completion
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
return s, fmt.Errorf("Error running command '%s': %v. Output: %s", return fmt.Errorf("Error running command '%s': %v. Output: %s",
command, err, output.Bytes()) command, err, output.Bytes())
} }
return s, nil return nil
} }
func (p *ResourceProvisioner) Validate(c *terraform.ResourceConfig) ([]string, []error) { func (p *ResourceProvisioner) Validate(c *terraform.ResourceConfig) ([]string, []error) {

View File

@ -21,8 +21,7 @@ func TestResourceProvider_Apply(t *testing.T) {
}) })
p := new(ResourceProvisioner) p := new(ResourceProvisioner)
_, err := p.Apply(nil, c) if err := p.Apply(nil, c); err != nil {
if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }

View File

@ -23,22 +23,22 @@ const (
type ResourceProvisioner struct{} type ResourceProvisioner struct{}
func (p *ResourceProvisioner) Apply(s *terraform.ResourceState, func (p *ResourceProvisioner) Apply(s *terraform.ResourceState,
c *terraform.ResourceConfig) (*terraform.ResourceState, error) { c *terraform.ResourceConfig) error {
// Ensure the connection type is SSH // Ensure the connection type is SSH
if err := helper.VerifySSH(s); err != nil { if err := helper.VerifySSH(s); err != nil {
return s, err return err
} }
// Get the SSH configuration // Get the SSH configuration
conf, err := helper.ParseSSHConfig(s) conf, err := helper.ParseSSHConfig(s)
if err != nil { if err != nil {
return s, err return err
} }
// Collect the scripts // Collect the scripts
scripts, err := p.collectScripts(c) scripts, err := p.collectScripts(c)
if err != nil { if err != nil {
return s, err return err
} }
for _, s := range scripts { for _, s := range scripts {
defer s.Close() defer s.Close()
@ -46,9 +46,9 @@ func (p *ResourceProvisioner) Apply(s *terraform.ResourceState,
// Copy and execute each script // Copy and execute each script
if err := p.runScripts(conf, scripts); err != nil { if err := p.runScripts(conf, scripts); err != nil {
return s, err return err
} }
return s, nil return nil
} }
func (p *ResourceProvisioner) Validate(c *terraform.ResourceConfig) (ws []string, es []error) { func (p *ResourceProvisioner) Validate(c *terraform.ResourceConfig) (ws []string, es []error) {

View File

@ -37,7 +37,7 @@ func (p *ResourceProvisioner) Validate(c *terraform.ResourceConfig) ([]string, [
func (p *ResourceProvisioner) Apply( func (p *ResourceProvisioner) Apply(
s *terraform.ResourceState, s *terraform.ResourceState,
c *terraform.ResourceConfig) (*terraform.ResourceState, error) { c *terraform.ResourceConfig) error {
var resp ResourceProvisionerApplyResponse var resp ResourceProvisionerApplyResponse
args := &ResourceProvisionerApplyArgs{ args := &ResourceProvisionerApplyArgs{
State: s, State: s,
@ -46,13 +46,13 @@ func (p *ResourceProvisioner) Apply(
err := p.Client.Call(p.Name+".Apply", args, &resp) err := p.Client.Call(p.Name+".Apply", args, &resp)
if err != nil { if err != nil {
return nil, err return err
} }
if resp.Error != nil { if resp.Error != nil {
err = resp.Error err = resp.Error
} }
return resp.State, err return err
} }
type ResourceProvisionerValidateArgs struct { type ResourceProvisionerValidateArgs struct {
@ -70,7 +70,6 @@ type ResourceProvisionerApplyArgs struct {
} }
type ResourceProvisionerApplyResponse struct { type ResourceProvisionerApplyResponse struct {
State *terraform.ResourceState
Error *BasicError Error *BasicError
} }
@ -83,9 +82,8 @@ type ResourceProvisionerServer struct {
func (s *ResourceProvisionerServer) Apply( func (s *ResourceProvisionerServer) Apply(
args *ResourceProvisionerApplyArgs, args *ResourceProvisionerApplyArgs,
result *ResourceProvisionerApplyResponse) error { result *ResourceProvisionerApplyResponse) error {
state, err := s.Provisioner.Apply(args.State, args.Config) err := s.Provisioner.Apply(args.State, args.Config)
*result = ResourceProvisionerApplyResponse{ *result = ResourceProvisionerApplyResponse{
State: state,
Error: NewBasicError(err), Error: NewBasicError(err),
} }
return nil return nil

View File

@ -21,14 +21,10 @@ func TestResourceProvisioner_apply(t *testing.T) {
} }
provisioner := &ResourceProvisioner{Client: client, Name: name} provisioner := &ResourceProvisioner{Client: client, Name: name}
p.ApplyReturn = &terraform.ResourceState{
ID: "bob",
}
// Apply // Apply
state := &terraform.ResourceState{} state := &terraform.ResourceState{}
conf := &terraform.ResourceConfig{} conf := &terraform.ResourceConfig{}
newState, err := provisioner.Apply(state, conf) err = provisioner.Apply(state, conf)
if !p.ApplyCalled { if !p.ApplyCalled {
t.Fatal("apply should be called") t.Fatal("apply should be called")
} }
@ -38,9 +34,6 @@ func TestResourceProvisioner_apply(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("bad: %#v", err) t.Fatalf("bad: %#v", err)
} }
if !reflect.DeepEqual(p.ApplyReturn, newState) {
t.Fatalf("bad: %#v", newState)
}
} }
func TestResourceProvisioner_validate(t *testing.T) { func TestResourceProvisioner_validate(t *testing.T) {

View File

@ -554,10 +554,11 @@ func (c *Context) applyWalkFn() depgraph.WalkFunc {
// //
// Additionally, we need to be careful to not run this if there // Additionally, we need to be careful to not run this if there
// was an error during the provider apply. // was an error during the provider apply.
tainted := false
if applyerr == nil && r.State.ID == "" && len(r.Provisioners) > 0 { if applyerr == nil && r.State.ID == "" && len(r.Provisioners) > 0 {
rs, err = c.applyProvisioners(r, rs) if err := c.applyProvisioners(r, rs); err != nil {
if err != nil {
errs = append(errs, err) errs = append(errs, err)
tainted = true
} }
} }
@ -567,6 +568,10 @@ func (c *Context) applyWalkFn() depgraph.WalkFunc {
delete(c.state.Resources, r.Id) delete(c.state.Resources, r.Id)
} else { } else {
c.state.Resources[r.Id] = rs c.state.Resources[r.Id] = rs
if tainted {
c.state.Tainted[r.Id] = struct{}{}
}
} }
c.sl.Unlock() c.sl.Unlock()
@ -591,9 +596,7 @@ func (c *Context) applyWalkFn() depgraph.WalkFunc {
// applyProvisioners is used to run any provisioners a resource has // applyProvisioners is used to run any provisioners a resource has
// defined after the resource creation has already completed. // defined after the resource creation has already completed.
func (c *Context) applyProvisioners(r *Resource, rs *ResourceState) (*ResourceState, error) { func (c *Context) applyProvisioners(r *Resource, rs *ResourceState) error {
var err error
// Store the original connection info, restore later // Store the original connection info, restore later
origConnInfo := rs.ConnInfo origConnInfo := rs.ConnInfo
defer func() { defer func() {
@ -604,13 +607,13 @@ func (c *Context) applyProvisioners(r *Resource, rs *ResourceState) (*ResourceSt
// Interpolate since we may have variables that depend on the // Interpolate since we may have variables that depend on the
// local resource. // local resource.
if err := prov.Config.interpolate(c); err != nil { if err := prov.Config.interpolate(c); err != nil {
return rs, err return err
} }
// Interpolate the conn info, since it may contain variables // Interpolate the conn info, since it may contain variables
connInfo := NewResourceConfig(prov.ConnInfo) connInfo := NewResourceConfig(prov.ConnInfo)
if err := connInfo.interpolate(c); err != nil { if err := connInfo.interpolate(c); err != nil {
return rs, err return err
} }
// Merge the connection information // Merge the connection information
@ -643,12 +646,12 @@ func (c *Context) applyProvisioners(r *Resource, rs *ResourceState) (*ResourceSt
rs.ConnInfo = overlay rs.ConnInfo = overlay
// Invoke the Provisioner // Invoke the Provisioner
rs, err = prov.Provisioner.Apply(rs, prov.Config) if err := prov.Provisioner.Apply(rs, prov.Config); err != nil {
if err != nil { return err
return rs, err
} }
} }
return rs, nil
return nil
} }
func (c *Context) planWalkFn(result *Plan) depgraph.WalkFunc { func (c *Context) planWalkFn(result *Plan) depgraph.WalkFunc {
@ -678,7 +681,13 @@ func (c *Context) planWalkFn(result *Plan) depgraph.WalkFunc {
// Get a diff from the newest state // Get a diff from the newest state
log.Printf("[DEBUG] %s: Executing diff", r.Id) log.Printf("[DEBUG] %s: Executing diff", r.Id)
var err error var err error
diff, err = r.Provider.Diff(r.State, r.Config) state := r.State
if r.Tainted {
// If we're tainted, we pretend to create a new thing.
state = new(ResourceState)
state.Type = r.State.Type
}
diff, err = r.Provider.Diff(state, r.Config)
if err != nil { if err != nil {
return err return err
} }
@ -688,6 +697,11 @@ func (c *Context) planWalkFn(result *Plan) depgraph.WalkFunc {
diff = new(ResourceDiff) diff = new(ResourceDiff)
} }
if r.Tainted {
// Tainted resources must also be destroyed
diff.Destroy = true
}
if diff.RequiresNew() && r.State.ID != "" { if diff.RequiresNew() && r.State.ID != "" {
// This will also require a destroy // This will also require a destroy
diff.Destroy = true diff.Destroy = true

View File

@ -414,12 +414,13 @@ func TestContextApply_Provisioner_compute(t *testing.T) {
pr := testProvisioner() pr := testProvisioner()
p.ApplyFn = testApplyFn p.ApplyFn = testApplyFn
p.DiffFn = testDiffFn p.DiffFn = testDiffFn
pr.ApplyFn = func(rs *ResourceState, c *ResourceConfig) (*ResourceState, error) { pr.ApplyFn = func(rs *ResourceState, c *ResourceConfig) error {
val, ok := c.Config["foo"] val, ok := c.Config["foo"]
if !ok || val != "computed_dynamical" { if !ok || val != "computed_dynamical" {
t.Fatalf("bad value for foo: %v %#v", val, c) t.Fatalf("bad value for foo: %v %#v", val, c)
} }
return rs, nil
return nil
} }
ctx := testContext(t, &ContextOpts{ ctx := testContext(t, &ContextOpts{
Config: c, Config: c,
@ -455,6 +456,46 @@ func TestContextApply_Provisioner_compute(t *testing.T) {
} }
} }
func TestContextApply_provisionerFail(t *testing.T) {
c := testConfig(t, "apply-provisioner-fail")
p := testProvider("aws")
pr := testProvisioner()
p.ApplyFn = testApplyFn
p.DiffFn = testDiffFn
pr.ApplyFn = func(*ResourceState, *ResourceConfig) error {
return fmt.Errorf("EXPLOSION")
}
ctx := testContext(t, &ContextOpts{
Config: c,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
Provisioners: map[string]ResourceProvisionerFactory{
"shell": testProvisionerFuncFixed(pr),
},
Variables: map[string]string{
"value": "1",
},
})
if _, err := ctx.Plan(nil); err != nil {
t.Fatalf("err: %s", err)
}
state, err := ctx.Apply()
if err == nil {
t.Fatal("should error")
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyProvisionerFailStr)
if actual != expected {
t.Fatalf("bad: \n%s", actual)
}
}
func TestContextApply_outputDiffVars(t *testing.T) { func TestContextApply_outputDiffVars(t *testing.T) {
c := testConfig(t, "apply-good") c := testConfig(t, "apply-good")
p := testProvider("aws") p := testProvider("aws")
@ -527,7 +568,7 @@ func TestContextApply_Provisioner_ConnInfo(t *testing.T) {
} }
p.DiffFn = testDiffFn p.DiffFn = testDiffFn
pr.ApplyFn = func(rs *ResourceState, c *ResourceConfig) (*ResourceState, error) { pr.ApplyFn = func(rs *ResourceState, c *ResourceConfig) error {
conn := rs.ConnInfo conn := rs.ConnInfo
if conn["type"] != "telnet" { if conn["type"] != "telnet" {
t.Fatalf("Bad: %#v", conn) t.Fatalf("Bad: %#v", conn)
@ -544,7 +585,8 @@ func TestContextApply_Provisioner_ConnInfo(t *testing.T) {
if conn["pass"] != "test" { if conn["pass"] != "test" {
t.Fatalf("Bad: %#v", conn) t.Fatalf("Bad: %#v", conn)
} }
return rs, nil
return nil
} }
ctx := testContext(t, &ContextOpts{ ctx := testContext(t, &ContextOpts{
@ -1415,6 +1457,44 @@ func TestContextPlan_state(t *testing.T) {
} }
} }
func TestContextPlan_taint(t *testing.T) {
c := testConfig(t, "plan-taint")
p := testProvider("aws")
p.DiffFn = testDiffFn
s := &State{
Resources: map[string]*ResourceState{
"aws_instance.foo": &ResourceState{
ID: "bar",
Type: "aws_instance",
Attributes: map[string]string{"num": "2"},
},
"aws_instance.bar": &ResourceState{
ID: "baz",
Type: "aws_instance",
},
},
Tainted: map[string]struct{}{"aws_instance.bar": struct{}{}},
}
ctx := testContext(t, &ContextOpts{
Config: c,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
State: s,
})
plan, err := ctx.Plan(nil)
if err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(plan.String())
expected := strings.TrimSpace(testTerraformPlanTaintStr)
if actual != expected {
t.Fatalf("bad:\n%s", actual)
}
}
func TestContextRefresh(t *testing.T) { func TestContextRefresh(t *testing.T) {
p := testProvider("aws") p := testProvider("aws")
c := testConfig(t, "refresh-basic") c := testConfig(t, "refresh-basic")

View File

@ -178,6 +178,12 @@ func graphAddConfigResources(
index = i index = i
} }
// Determine if this resource is tainted
tainted := false
if s != nil && s.Tainted != nil {
_, tainted = s.Tainted[r.Id()]
}
var state *ResourceState var state *ResourceState
if s != nil { if s != nil {
state = s.Resources[name] state = s.Resources[name]
@ -209,9 +215,10 @@ func graphAddConfigResources(
Type: r.Type, Type: r.Type,
Config: r, Config: r,
Resource: &Resource{ Resource: &Resource{
Id: name, Id: name,
State: state, State: state,
Config: NewResourceConfig(r.RawConfig), Config: NewResourceConfig(r.RawConfig),
Tainted: tainted,
}, },
}, },
} }

View File

@ -31,6 +31,7 @@ type Resource struct {
Provider ResourceProvider Provider ResourceProvider
State *ResourceState State *ResourceState
Provisioners []*ResourceProvisionerConfig Provisioners []*ResourceProvisionerConfig
Tainted bool
} }
// Vars returns the mapping of variables that should be replaced in // Vars returns the mapping of variables that should be replaced in

View File

@ -20,7 +20,7 @@ type ResourceProvisioner interface {
// resource state along with an error. Instead of a diff, the ResourceConfig // resource state along with an error. Instead of a diff, the ResourceConfig
// is provided since provisioners only run after a resource has been // is provided since provisioners only run after a resource has been
// newly created. // newly created.
Apply(*ResourceState, *ResourceConfig) (*ResourceState, error) Apply(*ResourceState, *ResourceConfig) error
} }
// ResourceProvisionerFactory is a function type that creates a new instance // ResourceProvisionerFactory is a function type that creates a new instance

View File

@ -9,8 +9,7 @@ type MockResourceProvisioner struct {
ApplyCalled bool ApplyCalled bool
ApplyState *ResourceState ApplyState *ResourceState
ApplyConfig *ResourceConfig ApplyConfig *ResourceConfig
ApplyFn func(*ResourceState, *ResourceConfig) (*ResourceState, error) ApplyFn func(*ResourceState, *ResourceConfig) error
ApplyReturn *ResourceState
ApplyReturnError error ApplyReturnError error
ValidateCalled bool ValidateCalled bool
@ -29,12 +28,12 @@ func (p *MockResourceProvisioner) Validate(c *ResourceConfig) ([]string, []error
return p.ValidateReturnWarns, p.ValidateReturnErrors return p.ValidateReturnWarns, p.ValidateReturnErrors
} }
func (p *MockResourceProvisioner) Apply(state *ResourceState, c *ResourceConfig) (*ResourceState, error) { func (p *MockResourceProvisioner) Apply(state *ResourceState, c *ResourceConfig) error {
p.ApplyCalled = true p.ApplyCalled = true
p.ApplyState = state p.ApplyState = state
p.ApplyConfig = c p.ApplyConfig = c
if p.ApplyFn != nil { if p.ApplyFn != nil {
return p.ApplyFn(state, c) return p.ApplyFn(state, c)
} }
return p.ApplyReturn, p.ApplyReturnError return p.ApplyReturnError
} }

View File

@ -18,13 +18,20 @@ import (
type State struct { type State struct {
Outputs map[string]string Outputs map[string]string
Resources map[string]*ResourceState Resources map[string]*ResourceState
Tainted map[string]struct{}
once sync.Once once sync.Once
} }
func (s *State) init() { func (s *State) init() {
s.once.Do(func() { s.once.Do(func() {
s.Resources = make(map[string]*ResourceState) if s.Resources == nil {
s.Resources = make(map[string]*ResourceState)
}
if s.Tainted == nil {
s.Tainted = make(map[string]struct{})
}
}) })
} }
@ -97,7 +104,12 @@ func (s *State) String() string {
id = "<not created>" id = "<not created>"
} }
buf.WriteString(fmt.Sprintf("%s:\n", k)) taintStr := ""
if _, ok := s.Tainted[k]; ok {
taintStr = " (tainted)"
}
buf.WriteString(fmt.Sprintf("%s:%s\n", k, taintStr))
buf.WriteString(fmt.Sprintf(" ID = %s\n", id)) buf.WriteString(fmt.Sprintf(" ID = %s\n", id))
attrKeys := make([]string, 0, len(rs.Attributes)) attrKeys := make([]string, 0, len(rs.Attributes))

View File

@ -130,6 +130,15 @@ aws_instance.foo:
type = aws_instance type = aws_instance
` `
const testTerraformApplyProvisionerFailStr = `
aws_instance.bar: (tainted)
ID = foo
aws_instance.foo:
ID = foo
num = 2
type = aws_instance
`
const testTerraformApplyDestroyStr = ` const testTerraformApplyDestroyStr = `
<no state> <no state>
` `
@ -402,3 +411,19 @@ STATE:
aws_instance.foo: aws_instance.foo:
ID = bar ID = bar
` `
const testTerraformPlanTaintStr = `
DIFF:
DESTROY: aws_instance.bar
foo: "" => "2"
type: "" => "aws_instance"
STATE:
aws_instance.bar: (tainted)
ID = baz
aws_instance.foo:
ID = bar
num = 2
`

View File

@ -0,0 +1,7 @@
resource "aws_instance" "foo" {
num = "2"
}
resource "aws_instance" "bar" {
provisioner "shell" {}
}

View File

@ -0,0 +1,7 @@
resource "aws_instance" "foo" {
num = "2"
}
resource "aws_instance" "bar" {
foo = "${aws_instance.foo.num}"
}