diff --git a/.gitignore b/.gitignore index 314611940..66ea31701 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ website/node_modules *.bak *~ .*.swp +.idea diff --git a/config/config.go b/config/config.go index c088414da..d31777f6e 100644 --- a/config/config.go +++ b/config/config.go @@ -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. diff --git a/config/loader_test.go b/config/loader_test.go index d239bd0b9..eaf4f10aa 100644 --- a/config/loader_test.go +++ b/config/loader_test.go @@ -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 { @@ -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 +` diff --git a/config/test-fixtures/ignore-changes.tf b/config/test-fixtures/ignore-changes.tf new file mode 100644 index 000000000..765a05798 --- /dev/null +++ b/config/test-fixtures/ignore-changes.tf @@ -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" +} diff --git a/terraform/context_plan_test.go b/terraform/context_plan_test.go index 50f2bb471..db6f24577 100644 --- a/terraform/context_plan_test.go +++ b/terraform/context_plan_test.go @@ -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) + } +} diff --git a/terraform/eval_ignore_changes.go b/terraform/eval_ignore_changes.go new file mode 100644 index 000000000..1a44089a9 --- /dev/null +++ b/terraform/eval_ignore_changes.go @@ -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 +} diff --git a/terraform/terraform_test.go b/terraform/terraform_test.go index c84e9803c..02d4de2a2 100644 --- a/terraform/terraform_test.go +++ b/terraform/terraform_test.go @@ -1286,3 +1286,16 @@ STATE: ` + +const testTerraformPlanIgnoreChangesStr = ` +DIFF: + +UPDATE: aws_instance.foo + type: "" => "aws_instance" + +STATE: + +aws_instance.foo: + ID = bar + ami = ami-abcd1234 +` diff --git a/terraform/test-fixtures/plan-ignore-changes/main.tf b/terraform/test-fixtures/plan-ignore-changes/main.tf new file mode 100644 index 000000000..056256a1d --- /dev/null +++ b/terraform/test-fixtures/plan-ignore-changes/main.tf @@ -0,0 +1,9 @@ +variable "foo" {} + +resource "aws_instance" "foo" { + ami = "${var.foo}" + + lifecycle { + ignore_changes = ["ami"] + } +} diff --git a/terraform/transform_resource.go b/terraform/transform_resource.go index a52b3ba72..81ff158d9 100644 --- a/terraform/transform_resource.go +++ b/terraform/transform_resource.go @@ -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, diff --git a/website/source/docs/configuration/resources.html.md b/website/source/docs/configuration/resources.html.md index f099c5f25..d5e087fec 100644 --- a/website/source/docs/configuration/resources.html.md +++ b/website/source/docs/configuration/resources.html.md @@ -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**. @@ -191,6 +200,8 @@ where `LIFECYCLE` is: ``` lifecycle { [create_before_destroy = true|false] + [prevent_destroy = true|false] + [ignore_changes = [ATTRIBUTE NAME, ...]] } ```