Skip to content

Commit

Permalink
Adding ignore_changes lifecycle meta property
Browse files Browse the repository at this point in the history
  • Loading branch information
robzienert committed Oct 14, 2015
1 parent bfc107f commit a1939e7
Show file tree
Hide file tree
Showing 10 changed files with 193 additions and 2 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ website/node_modules
*.bak
*~
.*.swp
.idea
5 changes: 3 additions & 2 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,9 @@ type Resource struct {
// ResourceLifecycle is used to store the lifecycle tuning parameters
// to allow customized behavior
type ResourceLifecycle struct {
CreateBeforeDestroy bool `mapstructure:"create_before_destroy"`
PreventDestroy bool `mapstructure:"prevent_destroy"`
CreateBeforeDestroy bool `mapstructure:"create_before_destroy"`
PreventDestroy bool `mapstructure:"prevent_destroy"`
IgnoreChanges []string `mapstructure:"ignore_changes"`
}

// Provisioner is a configured provisioner step on a resource.
Expand Down
57 changes: 57 additions & 0 deletions config/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,54 @@ func TestLoadFile_createBeforeDestroy(t *testing.T) {
}
}

func TestLoadFile_ignoreChanges(t *testing.T) {
c, err := LoadFile(filepath.Join(fixtureDir, "ignore-changes.tf"))
if err != nil {
t.Fatalf("err: %s", err)
}

if c == nil {
t.Fatal("config should not be nil")
}

actual := resourcesStr(c.Resources)
print(actual)
if actual != strings.TrimSpace(ignoreChangesResourcesStr) {
t.Fatalf("bad:\n%s", actual)
}

// Check for the flag value
r := c.Resources[0]
if r.Name != "web" && r.Type != "aws_instance" {
t.Fatalf("Bad: %#v", r)
}

// Should populate ignore changes
if len(r.Lifecycle.IgnoreChanges) == 0 {
t.Fatalf("Bad: %#v", r)
}

r = c.Resources[1]
if r.Name != "bar" && r.Type != "aws_instance" {
t.Fatalf("Bad: %#v", r)
}

// Should not populate ignore changes
if len(r.Lifecycle.IgnoreChanges) > 0 {
t.Fatalf("Bad: %#v", r)
}

r = c.Resources[2]
if r.Name != "baz" && r.Type != "aws_instance" {
t.Fatalf("Bad: %#v", r)
}

// Should not populate ignore changes
if len(r.Lifecycle.IgnoreChanges) > 0 {
t.Fatalf("Bad: %#v", r)
}
}

func TestLoad_preventDestroyString(t *testing.T) {
c, err := LoadFile(filepath.Join(fixtureDir, "prevent-destroy-string.tf"))
if err != nil {
Expand Down Expand Up @@ -676,3 +724,12 @@ aws_instance[bar] (x1)
aws_instance[web] (x1)
ami
`

const ignoreChangesResourcesStr = `
aws_instance[bar] (x1)
ami
aws_instance[baz] (x1)
ami
aws_instance[web] (x1)
ami
`
17 changes: 17 additions & 0 deletions config/test-fixtures/ignore-changes.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
resource "aws_instance" "web" {
ami = "foo"
lifecycle {
ignore_changes = ["ami"]
}
}

resource "aws_instance" "bar" {
ami = "foo"
lifecycle {
ignore_changes = []
}
}

resource "aws_instance" "baz" {
ami = "foo"
}
46 changes: 46 additions & 0 deletions terraform/context_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1672,3 +1672,49 @@ func TestContext2Plan_varListErr(t *testing.T) {
t.Fatal("should error")
}
}

func TestContext2Plan_ignoreChanges(t *testing.T) {
m := testModule(t, "plan-ignore-changes")
p := testProvider("aws")
p.DiffFn = testDiffFn
s := &State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.foo": &ResourceState{
Primary: &InstanceState{
ID: "bar",
Attributes: map[string]string{"ami": "ami-abcd1234"},
},
},
},
},
},
}
ctx := testContext2(t, &ContextOpts{
Module: m,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
Variables: map[string]string{
"foo": "ami-1234abcd",
},
State: s,
})

plan, err := ctx.Plan()
if err != nil {
t.Fatalf("err: %s", err)
}

if len(plan.Diff.RootModule().Resources) < 1 {
t.Fatalf("bad: %#v", plan.Diff.RootModule().Resources)
}

actual := strings.TrimSpace(plan.String())
expected := strings.TrimSpace(testTerraformPlanIgnoreChangesStr)
if actual != expected {
t.Fatalf("bad:\n%s\n\nexpected\n\n%s", actual, expected)
}
}
32 changes: 32 additions & 0 deletions terraform/eval_ignore_changes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package terraform
import (
"github.com/hashicorp/terraform/config"
"strings"
)

// EvalIgnoreChanges is an EvalNode implementation that removes diff
// attributes if their name matches names provided by the resource's
// IgnoreChanges lifecycle.
type EvalIgnoreChanges struct {
Resource *config.Resource
Diff **InstanceDiff
}

func (n *EvalIgnoreChanges) Eval(ctx EvalContext) (interface{}, error) {
if n.Diff == nil || *n.Diff == nil || n.Resource == nil || n.Resource.Id() == "" {
return nil, nil
}

diff := *n.Diff
ignoreChanges := n.Resource.Lifecycle.IgnoreChanges

for _, ignoredName := range ignoreChanges {
for name := range diff.Attributes {
if strings.HasPrefix(name, ignoredName) {
delete(diff.Attributes, name)
}
}
}

return nil, nil
}
13 changes: 13 additions & 0 deletions terraform/terraform_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1286,3 +1286,16 @@ STATE:
<no state>
`

const testTerraformPlanIgnoreChangesStr = `
DIFF:
UPDATE: aws_instance.foo
type: "" => "aws_instance"
STATE:
aws_instance.foo:
ID = bar
ami = ami-abcd1234
`
9 changes: 9 additions & 0 deletions terraform/test-fixtures/plan-ignore-changes/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
variable "foo" {}

resource "aws_instance" "foo" {
ami = "${var.foo}"

lifecycle {
ignore_changes = ["ami"]
}
}
4 changes: 4 additions & 0 deletions terraform/transform_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,10 @@ func (n *graphNodeExpandedResource) EvalTree() EvalNode {
Resource: n.Resource,
Diff: &diff,
},
&EvalIgnoreChanges{
Resource: n.Resource,
Diff: &diff,
},
&EvalWriteState{
Name: n.stateId(),
ResourceType: n.Resource.Type,
Expand Down
11 changes: 11 additions & 0 deletions website/source/docs/configuration/resources.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,20 @@ The `lifecycle` block allows the following keys to be set:
destruction of a given resource. When this is set to `true`, any plan
that includes a destroy of this resource will return an error message.

* `ignore_changes` (list of strings) - Customizes how diffs are evaluated for
resources, allowing individual attributes to be ignored through changes.
As an example, this can be used to ignore dynamic changes to the
resource from external resources. Other meta-parameters cannot be ignored.

~> **NOTE on create\_before\_destroy and dependencies:** Resources that utilize
the `create_before_destroy` key can only depend on other resources that also
include `create_before_destroy`. Referencing a resource that does not include
`create_before_destroy` will result in a dependency graph cycle.

~> **NOTE on ignore\_changes:** Ignored attribute names can be matched by their
name, not state ID. For example, if an `aws_route_table` has two routes defined
and the `ignore_changes` list contains "route", both routes will be ignored.

-------------

Within a resource, you can optionally have a **connection block**.
Expand Down Expand Up @@ -191,6 +200,8 @@ where `LIFECYCLE` is:
```
lifecycle {
[create_before_destroy = true|false]
[prevent_destroy = true|false]
[ignore_changes = [ATTRIBUTE NAME, ...]]
}
```

Expand Down

0 comments on commit a1939e7

Please sign in to comment.