diff --git a/config/config.go b/config/config.go index 99295ab8f8..8dd9810ebf 100644 --- a/config/config.go +++ b/config/config.go @@ -477,6 +477,22 @@ func (c *Config) Validate() error { } } + // Validate the self variable + for source, rc := range c.rawConfigs() { + // Ignore provisioners. This is a pretty brittle way to do this, + // but better than also repeating all the resources. + if strings.Contains(source, "provision") { + continue + } + + for _, v := range rc.Variables { + if _, ok := v.(*SelfVariable); ok { + errs = append(errs, fmt.Errorf( + "%s: cannot contain self-reference %s", source, v.FullKey())) + } + } + } + if len(errs) > 0 { return &multierror.Error{Errors: errs} } diff --git a/config/config_test.go b/config/config_test.go index 821539ccf8..0503d2e667 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -214,6 +214,20 @@ func TestConfigValidate_provSplatSelf(t *testing.T) { } } +func TestConfigValidate_resourceProvVarSelf(t *testing.T) { + c := testConfig(t, "validate-resource-prov-self") + if err := c.Validate(); err != nil { + t.Fatalf("should be valid: %s", err) + } +} + +func TestConfigValidate_resourceVarSelf(t *testing.T) { + c := testConfig(t, "validate-resource-self") + if err := c.Validate(); err == nil { + t.Fatal("should not be valid") + } +} + func TestConfigValidate_unknownThing(t *testing.T) { c := testConfig(t, "validate-unknownthing") if err := c.Validate(); err == nil { diff --git a/config/interpolate.go b/config/interpolate.go index b13cdb405c..af0a84da49 100644 --- a/config/interpolate.go +++ b/config/interpolate.go @@ -68,6 +68,14 @@ type ResourceVariable struct { key string } +// SelfVariable is a variable that is referencing the same resource +// it is running on: "${self.address}" +type SelfVariable struct { + Field string + + key string +} + // A UserVariable is a variable that is referencing a user variable // that is inputted from outside the configuration. This looks like // "${var.foo}" @@ -83,6 +91,8 @@ func NewInterpolatedVariable(v string) (InterpolatedVariable, error) { return NewCountVariable(v) } else if strings.HasPrefix(v, "path.") { return NewPathVariable(v) + } else if strings.HasPrefix(v, "self.") { + return NewSelfVariable(v) } else if strings.HasPrefix(v, "var.") { return NewUserVariable(v) } else if strings.HasPrefix(v, "module.") { @@ -199,6 +209,24 @@ func (v *ResourceVariable) FullKey() string { return v.key } +func NewSelfVariable(key string) (*SelfVariable, error) { + field := key[len("self."):] + + return &SelfVariable{ + Field: field, + + key: key, + }, nil +} + +func (v *SelfVariable) FullKey() string { + return v.key +} + +func (v *SelfVariable) GoString() string { + return fmt.Sprintf("*%#v", *v) +} + func NewUserVariable(key string) (*UserVariable, error) { name := key[len("var."):] elem := "" diff --git a/config/interpolate_test.go b/config/interpolate_test.go index 46945ee810..69a6ca2293 100644 --- a/config/interpolate_test.go +++ b/config/interpolate_test.go @@ -54,6 +54,14 @@ func TestNewInterpolatedVariable(t *testing.T) { }, false, }, + { + "self.address", + &SelfVariable{ + Field: "address", + key: "self.address", + }, + false, + }, } for i, tc := range cases { diff --git a/config/test-fixtures/validate-resource-prov-self/main.tf b/config/test-fixtures/validate-resource-prov-self/main.tf new file mode 100644 index 0000000000..4a55ac24ec --- /dev/null +++ b/config/test-fixtures/validate-resource-prov-self/main.tf @@ -0,0 +1,11 @@ +resource "aws_instance" "foo" { + foo = "bar" + + connection { + host = "${self.foo}" + } + + provisioner "shell" { + value = "${self.foo}" + } +} diff --git a/config/test-fixtures/validate-resource-self/main.tf b/config/test-fixtures/validate-resource-self/main.tf new file mode 100644 index 0000000000..20049d55bb --- /dev/null +++ b/config/test-fixtures/validate-resource-self/main.tf @@ -0,0 +1,3 @@ +resource "aws_instance" "foo" { + foo = "${self.bar}" +} diff --git a/terraform/context_test.go b/terraform/context_test.go index 0e076cd79d..e4359685f7 100644 --- a/terraform/context_test.go +++ b/terraform/context_test.go @@ -3766,6 +3766,112 @@ func TestContext2Apply_provisionerResourceRef(t *testing.T) { } } +func TestContext2Apply_provisionerSelfRef(t *testing.T) { + m := testModule(t, "apply-provisioner-self-ref") + p := testProvider("aws") + pr := testProvisioner() + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + pr.ApplyFn = func(rs *InstanceState, c *ResourceConfig) error { + val, ok := c.Config["command"] + if !ok || val != "bar" { + t.Fatalf("bad value for command: %v %#v", val, c) + } + + return nil + } + + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + Provisioners: map[string]ResourceProvisionerFactory{ + "shell": testProvisionerFuncFixed(pr), + }, + }) + + if _, err := ctx.Plan(nil); err != nil { + t.Fatalf("err: %s", err) + } + + state, err := ctx.Apply() + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(state.String()) + expected := strings.TrimSpace(testTerraformApplyProvisionerSelfRefStr) + if actual != expected { + t.Fatalf("bad: \n%s", actual) + } + + // Verify apply was invoked + if !pr.ApplyCalled { + t.Fatalf("provisioner not invoked") + } +} + +func TestContext2Apply_provisionerMultiSelfRef(t *testing.T) { + var lock sync.Mutex + commands := make([]string, 0, 5) + + m := testModule(t, "apply-provisioner-multi-self-ref") + p := testProvider("aws") + pr := testProvisioner() + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + pr.ApplyFn = func(rs *InstanceState, c *ResourceConfig) error { + lock.Lock() + defer lock.Unlock() + + val, ok := c.Config["command"] + if !ok { + t.Fatalf("bad value for command: %v %#v", val, c) + } + + commands = append(commands, val.(string)) + return nil + } + + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + Provisioners: map[string]ResourceProvisionerFactory{ + "shell": testProvisionerFuncFixed(pr), + }, + }) + + if _, err := ctx.Plan(nil); err != nil { + t.Fatalf("err: %s", err) + } + + state, err := ctx.Apply() + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(state.String()) + expected := strings.TrimSpace(testTerraformApplyProvisionerMultiSelfRefStr) + if actual != expected { + t.Fatalf("bad: \n%s", actual) + } + + // Verify apply was invoked + if !pr.ApplyCalled { + t.Fatalf("provisioner not invoked") + } + + // Verify our result + sort.Strings(commands) + expectedCommands := []string{"number 0", "number 1", "number 2"} + if !reflect.DeepEqual(commands, expectedCommands) { + t.Fatalf("bad: %#v", commands) + } +} + // Provisioner should NOT run on a diff, only create func TestContext2Apply_Provisioner_Diff(t *testing.T) { m := testModule(t, "apply-provisioner-diff") diff --git a/terraform/interpolate.go b/terraform/interpolate.go index 2d5798aa0b..cf88ad8250 100644 --- a/terraform/interpolate.go +++ b/terraform/interpolate.go @@ -62,6 +62,8 @@ func (i *Interpolater) Values( err = i.valuePathVar(scope, n, v, result) case *config.ResourceVariable: err = i.valueResourceVar(scope, n, v, result) + case *config.SelfVariable: + err = i.valueSelfVar(scope, n, v, result) case *config.UserVariable: err = i.valueUserVar(scope, n, v, result) default: @@ -217,6 +219,24 @@ func (i *Interpolater) valueResourceVar( return nil } +func (i *Interpolater) valueSelfVar( + scope *InterpolationScope, + n string, + v *config.SelfVariable, + result map[string]ast.Variable) error { + rv, err := config.NewResourceVariable(fmt.Sprintf( + "%s.%s.%d.%s", + scope.Resource.Type, + scope.Resource.Name, + scope.Resource.CountIndex, + v.Field)) + if err != nil { + return err + } + + return i.valueResourceVar(scope, n, rv, result) +} + func (i *Interpolater) valueUserVar( scope *InterpolationScope, n string, diff --git a/terraform/resource.go b/terraform/resource.go index 7775ab789f..9dacbc7c5a 100644 --- a/terraform/resource.go +++ b/terraform/resource.go @@ -26,6 +26,13 @@ type ResourceProvisionerConfig struct { // its current state, and potentially a desired diff from the state it // wants to reach. type Resource struct { + // These are all used by the new EvalNode stuff. + Name string + Type string + CountIndex int + + // These aren't really used anymore anywhere, but we keep them around + // since we haven't done a proper cleanup yet. Id string Info *InstanceInfo Config *ResourceConfig @@ -34,7 +41,6 @@ type Resource struct { Provider ResourceProvider State *InstanceState Provisioners []*ResourceProvisionerConfig - CountIndex int Flags ResourceFlag TaintedIndex int } diff --git a/terraform/terraform_test.go b/terraform/terraform_test.go index 75f65c36e1..4372ff77bb 100644 --- a/terraform/terraform_test.go +++ b/terraform/terraform_test.go @@ -359,6 +359,28 @@ aws_instance.bar: type = aws_instance ` +const testTerraformApplyProvisionerSelfRefStr = ` +aws_instance.foo: + ID = foo + foo = bar + type = aws_instance +` + +const testTerraformApplyProvisionerMultiSelfRefStr = ` +aws_instance.foo.0: + ID = foo + foo = number 0 + type = aws_instance +aws_instance.foo.1: + ID = foo + foo = number 1 + type = aws_instance +aws_instance.foo.2: + ID = foo + foo = number 2 + type = aws_instance +` + const testTerraformApplyProvisionerDiffStr = ` aws_instance.bar: ID = foo diff --git a/terraform/test-fixtures/apply-provisioner-multi-self-ref/main.tf b/terraform/test-fixtures/apply-provisioner-multi-self-ref/main.tf new file mode 100644 index 0000000000..72a1e79200 --- /dev/null +++ b/terraform/test-fixtures/apply-provisioner-multi-self-ref/main.tf @@ -0,0 +1,8 @@ +resource "aws_instance" "foo" { + count = 3 + foo = "number ${count.index}" + + provisioner "shell" { + command = "${self.foo}" + } +} diff --git a/terraform/test-fixtures/apply-provisioner-self-ref/main.tf b/terraform/test-fixtures/apply-provisioner-self-ref/main.tf new file mode 100644 index 0000000000..5f401f7c07 --- /dev/null +++ b/terraform/test-fixtures/apply-provisioner-self-ref/main.tf @@ -0,0 +1,7 @@ +resource "aws_instance" "foo" { + foo = "bar" + + provisioner "shell" { + command = "${self.foo}" + } +} diff --git a/terraform/transform_resource.go b/terraform/transform_resource.go index 8c9c16a158..0ce4f4cd78 100644 --- a/terraform/transform_resource.go +++ b/terraform/transform_resource.go @@ -109,7 +109,11 @@ func (n *graphNodeExpandedResource) EvalTree() EvalNode { if index < 0 { index = 0 } - resource := &Resource{CountIndex: index} + resource := &Resource{ + Name: n.Resource.Name, + Type: n.Resource.Type, + CountIndex: index, + } seq := &EvalSequence{Nodes: make([]EvalNode, 0, 5)} diff --git a/website/source/docs/configuration/interpolation.html.md b/website/source/docs/configuration/interpolation.html.md index dbce792b5a..3953a0c3a4 100644 --- a/website/source/docs/configuration/interpolation.html.md +++ b/website/source/docs/configuration/interpolation.html.md @@ -26,6 +26,11 @@ can reference static keys in the map with the syntax get the value of the `us-east-1` key within the `amis` variable that is a mapping. +**To reference attributes of your own resource**, the syntax is +`self.ATTRIBUTE`. For example `${self.private_ip_address}` will +interpolate that resource's private IP address. Note that this is +only allowed/valid within provisioners. + **To reference attributes of other resources**, the syntax is `TYPE.NAME.ATTRIBUTE`. For example, `${aws_instance.web.id}` will interpolate the ID attribute from the "aws\_instance" @@ -72,8 +77,8 @@ The supported built-in functions are: only possible with splat variables from resources with a count greater than one. Example: `join(",", aws_instance.foo.*.id)` - * `split(delim, string)` - Splits the string previously created by `join` - back into a list. This is useful for pushing lists through module + * `split(delim, string)` - Splits the string previously created by `join` + back into a list. This is useful for pushing lists through module outputs since they currently only support string values. Example: `split(",", module.amod.server_ids)`