Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding ignore_changes lifecycle flag #2525

Merged
merged 1 commit into from
Oct 14, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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