diff --git a/config/config.go b/config/config.go index 4d6ea1a85..fae135dec 100644 --- a/config/config.go +++ b/config/config.go @@ -199,6 +199,23 @@ func (c *Config) Validate() error { } } + // Check that all count variables are valid. + for source, vs := range vars { + for _, v := range vs { + cv, ok := v.(*CountVariable) + if !ok { + continue + } + + if cv.Type == CountValueInvalid { + errs = append(errs, fmt.Errorf( + "%s: invalid count variable: %s", + source, + cv.FullKey())) + } + } + } + // Check that all references to modules are valid modules := make(map[string]*Module) dupped := make(map[string]struct{}) @@ -258,6 +275,11 @@ func (c *Config) Validate() error { // Verify count variables for _, v := range r.RawCount.Variables { switch v.(type) { + case *CountVariable: + errs = append(errs, fmt.Errorf( + "%s: resource count can't reference count variable: %s", + n, + v.FullKey())) case *ModuleVariable: errs = append(errs, fmt.Errorf( "%s: resource count can't reference module variable: %s", diff --git a/config/config_test.go b/config/config_test.go index 76c12d4a0..e4892a277 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -60,6 +60,13 @@ func TestConfigValidate_countInt(t *testing.T) { } } +func TestConfigValidate_countCountVar(t *testing.T) { + c := testConfig(t, "validate-count-count-var") + if err := c.Validate(); err == nil { + t.Fatal("should not be valid") + } +} + func TestConfigValidate_countModuleVar(t *testing.T) { c := testConfig(t, "validate-count-module-var") if err := c.Validate(); err == nil { @@ -88,6 +95,20 @@ func TestConfigValidate_countUserVar(t *testing.T) { } } +func TestConfigValidate_countVar(t *testing.T) { + c := testConfig(t, "validate-count-var") + if err := c.Validate(); err != nil { + t.Fatal("err: %s", err) + } +} + +func TestConfigValidate_countVarInvalid(t *testing.T) { + c := testConfig(t, "validate-count-var-invalid") + if err := c.Validate(); err == nil { + t.Fatal("should not be valid") + } +} + func TestConfigValidate_dupModule(t *testing.T) { c := testConfig(t, "validate-dup-module") if err := c.Validate(); err == nil { diff --git a/config/interpolate.go b/config/interpolate.go index b6d84d425..4b0b30341 100644 --- a/config/interpolate.go +++ b/config/interpolate.go @@ -52,6 +52,21 @@ type VariableInterpolation struct { Variable InterpolatedVariable } +// CountVariable is a variable for referencing information about +// the count. +type CountVariable struct { + Type CountValueType + key string +} + +// CountValueType is the type of the count variable that is referenced. +type CountValueType byte + +const ( + CountValueInvalid CountValueType = iota + CountValueIndex +) + // A ModuleVariable is a variable that is referencing the output // of a module, such as "${module.foo.bar}" type ModuleVariable struct { @@ -84,7 +99,9 @@ type UserVariable struct { } func NewInterpolatedVariable(v string) (InterpolatedVariable, error) { - if strings.HasPrefix(v, "var.") { + if strings.HasPrefix(v, "count.") { + return NewCountVariable(v) + } else if strings.HasPrefix(v, "var.") { return NewUserVariable(v) } else if strings.HasPrefix(v, "module.") { return NewModuleVariable(v) @@ -152,6 +169,24 @@ func (i *VariableInterpolation) Variables() map[string]InterpolatedVariable { return map[string]InterpolatedVariable{i.Variable.FullKey(): i.Variable} } +func NewCountVariable(key string) (*CountVariable, error) { + var fieldType CountValueType + parts := strings.SplitN(key, ".", 2) + switch parts[1] { + case "index": + fieldType = CountValueIndex + } + + return &CountVariable{ + Type: fieldType, + key: key, + }, nil +} + +func (c *CountVariable) FullKey() string { + return c.key +} + func NewModuleVariable(key string) (*ModuleVariable, error) { parts := strings.SplitN(key, ".", 3) if len(parts) < 3 { diff --git a/config/interpolate_test.go b/config/interpolate_test.go index 61d188d22..3c4bf7f9a 100644 --- a/config/interpolate_test.go +++ b/config/interpolate_test.go @@ -29,6 +29,22 @@ func TestNewInterpolatedVariable(t *testing.T) { }, false, }, + { + "count.index", + &CountVariable{ + Type: CountValueIndex, + key: "count.index", + }, + false, + }, + { + "count.nope", + &CountVariable{ + Type: CountValueInvalid, + key: "count.nope", + }, + false, + }, } for i, tc := range cases { diff --git a/config/test-fixtures/validate-count-count-var/main.tf b/config/test-fixtures/validate-count-count-var/main.tf new file mode 100644 index 000000000..f33618784 --- /dev/null +++ b/config/test-fixtures/validate-count-count-var/main.tf @@ -0,0 +1,3 @@ +resource "aws_instance" "web" { + count = "${count.index}" +} diff --git a/config/test-fixtures/validate-count-var-invalid/main.tf b/config/test-fixtures/validate-count-var-invalid/main.tf new file mode 100644 index 000000000..a7f3fdb53 --- /dev/null +++ b/config/test-fixtures/validate-count-var-invalid/main.tf @@ -0,0 +1,3 @@ +resource "aws_instance" "foo" { + foo = "${count.foo}" +} diff --git a/config/test-fixtures/validate-count-var/main.tf b/config/test-fixtures/validate-count-var/main.tf new file mode 100644 index 000000000..c189c7d1a --- /dev/null +++ b/config/test-fixtures/validate-count-var/main.tf @@ -0,0 +1,3 @@ +resource "aws_instance" "foo" { + foo = "${count.index}" +} diff --git a/terraform/context.go b/terraform/context.go index 58cf0542b..95102cc80 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -498,7 +498,7 @@ func (c *walkContext) Walk() error { outputs := make(map[string]string) for _, o := range conf.Outputs { - if err := c.computeVars(o.RawConfig); err != nil { + if err := c.computeVars(o.RawConfig, nil); err != nil { return err } vraw := o.RawConfig.Config()["value"] @@ -619,7 +619,7 @@ func (c *walkContext) applyWalkFn() depgraph.WalkFunc { if !diff.Destroy { // Since we need the configuration, interpolate the variables - if err := r.Config.interpolate(c); err != nil { + if err := r.Config.interpolate(c, r); err != nil { return err } @@ -780,7 +780,7 @@ func (c *walkContext) planWalkFn() depgraph.WalkFunc { diff = &InstanceDiff{Destroy: true} } else { // Make sure the configuration is interpolated - if err := r.Config.interpolate(c); err != nil { + if err := r.Config.interpolate(c, r); err != nil { return err } @@ -993,7 +993,7 @@ func (c *walkContext) validateWalkFn() depgraph.WalkFunc { if rn.ExpandMode > ResourceExpandNone { // Interpolate the count and verify it is non-negative rc := NewResourceConfig(rn.Config.RawCount) - rc.interpolate(c) + rc.interpolate(c, rn.Resource) count, err := rn.Config.Count() if err == nil { if count < 0 { @@ -1063,7 +1063,7 @@ func (c *walkContext) validateWalkFn() depgraph.WalkFunc { for k, p := range sharedProvider.Providers { // Merge the configurations to get what we use to configure with rc := sharedProvider.MergeConfig(false, cs[k]) - rc.interpolate(c) + rc.interpolate(c, nil) log.Printf("[INFO] Validating provider: %s", k) ws, es := p.Validate(rc) @@ -1125,7 +1125,7 @@ func (c *walkContext) genericWalkFn(cb genericWalkFunc) depgraph.WalkFunc { wc.Variables = make(map[string]string) rc := NewResourceConfig(m.Config.RawConfig) - rc.interpolate(c) + rc.interpolate(c, nil) for k, v := range rc.Config { wc.Variables[k] = v.(string) } @@ -1151,7 +1151,7 @@ func (c *walkContext) genericWalkFn(cb genericWalkFunc) depgraph.WalkFunc { for k, p := range sharedProvider.Providers { // Merge the configurations to get what we use to configure with rc := sharedProvider.MergeConfig(false, cs[k]) - rc.interpolate(c) + rc.interpolate(c, nil) log.Printf("[INFO] Configuring provider: %s", k) err := p.Configure(rc) @@ -1211,7 +1211,7 @@ func (c *walkContext) genericWalkResource( rn *GraphNodeResource, fn depgraph.WalkFunc) error { // Interpolate the count rc := NewResourceConfig(rn.Config.RawCount) - rc.interpolate(c) + rc.interpolate(c, rn.Resource) // Expand the node to the actual resources ns, err := rn.Expand() @@ -1260,13 +1260,13 @@ func (c *walkContext) applyProvisioners(r *Resource, is *InstanceState) error { for _, prov := range r.Provisioners { // Interpolate since we may have variables that depend on the // local resource. - if err := prov.Config.interpolate(c); err != nil { + if err := prov.Config.interpolate(c, r); err != nil { return err } // Interpolate the conn info, since it may contain variables connInfo := NewResourceConfig(prov.ConnInfo) - if err := connInfo.interpolate(c); err != nil { + if err := connInfo.interpolate(c, r); err != nil { return err } @@ -1396,7 +1396,8 @@ func (c *walkContext) persistState(r *Resource) { // computeVars takes the State and given RawConfig and processes all // the variables. This dynamically discovers the attributes instead of // using a static map[string]string that the genericWalkFn uses. -func (c *walkContext) computeVars(raw *config.RawConfig) error { +func (c *walkContext) computeVars( + raw *config.RawConfig, r *Resource) error { // If there isn't a raw configuration, don't do anything if raw == nil { return nil @@ -1411,6 +1412,11 @@ func (c *walkContext) computeVars(raw *config.RawConfig) error { // Next, the actual computed variables for n, rawV := range raw.Variables { switch v := rawV.(type) { + case *config.CountVariable: + switch v.Type { + case config.CountValueIndex: + vs[n] = strconv.FormatInt(int64(r.CountIndex), 10) + } case *config.ModuleVariable: value, err := c.computeModuleVariable(v) if err != nil { diff --git a/terraform/context_test.go b/terraform/context_test.go index 29c927838..dded2d987 100644 --- a/terraform/context_test.go +++ b/terraform/context_test.go @@ -2464,6 +2464,52 @@ func TestContextPlan_countComputed(t *testing.T) { } } +func TestContextPlan_countIndex(t *testing.T) { + m := testModule(t, "plan-count-index") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanCountIndexStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + +func TestContextPlan_countIndexZero(t *testing.T) { + m := testModule(t, "plan-count-index-zero") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanCountIndexZeroStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + func TestContextPlan_countVar(t *testing.T) { m := testModule(t, "plan-count-var") p := testProvider("aws") diff --git a/terraform/graph.go b/terraform/graph.go index 49cf68edb..19b026715 100644 --- a/terraform/graph.go +++ b/terraform/graph.go @@ -1704,6 +1704,7 @@ func (n *GraphNodeResource) expand(g *depgraph.Graph, count int) { // Copy the base resource so we can fill it in resource := n.copyResource(name) + resource.CountIndex = i resource.State = state.Primary resource.Flags = flags diff --git a/terraform/resource.go b/terraform/resource.go index f76cfcba9..b43d296ac 100644 --- a/terraform/resource.go +++ b/terraform/resource.go @@ -34,6 +34,7 @@ type Resource struct { Provider ResourceProvider State *InstanceState Provisioners []*ResourceProvisionerConfig + CountIndex int Flags ResourceFlag TaintedIndex int } @@ -92,7 +93,7 @@ type ResourceConfig struct { // NewResourceConfig creates a new ResourceConfig from a config.RawConfig. func NewResourceConfig(c *config.RawConfig) *ResourceConfig { result := &ResourceConfig{raw: c} - result.interpolate(nil) + result.interpolate(nil, nil) return result } @@ -190,13 +191,14 @@ func (c *ResourceConfig) get( return current, true } -func (c *ResourceConfig) interpolate(ctx *walkContext) error { +func (c *ResourceConfig) interpolate( + ctx *walkContext, r *Resource) error { if c == nil { return nil } if ctx != nil { - if err := ctx.computeVars(c.raw); err != nil { + if err := ctx.computeVars(c.raw, r); err != nil { return err } } diff --git a/terraform/resource_test.go b/terraform/resource_test.go index ff85ab1b4..cc260ec3f 100644 --- a/terraform/resource_test.go +++ b/terraform/resource_test.go @@ -102,7 +102,10 @@ func TestResourceConfigGet(t *testing.T) { rc := NewResourceConfig(rawC) if tc.Vars != nil { ctx := NewContext(&ContextOpts{Variables: tc.Vars}) - if err := rc.interpolate(ctx.walkContext(walkInvalid, rootModulePath)); err != nil { + err := rc.interpolate( + ctx.walkContext(walkInvalid, rootModulePath), + nil) + if err != nil { t.Fatalf("err: %s", err) } } diff --git a/terraform/terraform_test.go b/terraform/terraform_test.go index 8041b139e..c0ac52537 100644 --- a/terraform/terraform_test.go +++ b/terraform/terraform_test.go @@ -477,6 +477,33 @@ STATE: ` +const testTerraformPlanCountIndexStr = ` +DIFF: + +CREATE: aws_instance.foo.0 + foo: "" => "0" + type: "" => "aws_instance" +CREATE: aws_instance.foo.1 + foo: "" => "1" + type: "" => "aws_instance" + +STATE: + + +` + +const testTerraformPlanCountIndexZeroStr = ` +DIFF: + +CREATE: aws_instance.foo + foo: "" => "0" + type: "" => "aws_instance" + +STATE: + + +` + const testTerraformPlanCountOneIndexStr = ` DIFF: diff --git a/terraform/test-fixtures/plan-count-index-zero/main.tf b/terraform/test-fixtures/plan-count-index-zero/main.tf new file mode 100644 index 000000000..c189c7d1a --- /dev/null +++ b/terraform/test-fixtures/plan-count-index-zero/main.tf @@ -0,0 +1,3 @@ +resource "aws_instance" "foo" { + foo = "${count.index}" +} diff --git a/terraform/test-fixtures/plan-count-index/main.tf b/terraform/test-fixtures/plan-count-index/main.tf new file mode 100644 index 000000000..9a0d1ebbc --- /dev/null +++ b/terraform/test-fixtures/plan-count-index/main.tf @@ -0,0 +1,4 @@ +resource "aws_instance" "foo" { + count = 2 + foo = "${count.index}" +} diff --git a/website/source/docs/configuration/interpolation.html.md b/website/source/docs/configuration/interpolation.html.md index 4db6c3b9f..5e43bbd2b 100644 --- a/website/source/docs/configuration/interpolation.html.md +++ b/website/source/docs/configuration/interpolation.html.md @@ -14,7 +14,9 @@ into strings. These interpolations are wrapped in `${}`, such as The interpolation syntax is powerful and allows you to reference variables, attributes of resources, call functions, etc. -To reference variables, use the `var.` prefix followed by the +## Available Variables + +**To reference user variables**, use the `var.` prefix followed by the variable name. For example, `${var.foo}` will interpolate the `foo` variable value. If the variable is a mapping, then you can reference static keys in the map with the syntax @@ -22,18 +24,28 @@ 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 other resources, the syntax is +**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" resource named "web". -Finally, Terraform ships with built-in functions. Functions -are called with the syntax `name(arg, arg2, ...)`. For example, -to read a file: `${file("path.txt")}`. The built-in functions -are documented below. +**To reference outputs from a module**, the syntax is +`MODULE.NAME.OUTPUT`. For example `${module.foo.bar}` will +interpolate the "bar" output from the "foo" +[module](/docs/modules/index.html). + +**To reference count information**, the syntax is `count.FIELD`. +For example, `${count.index}` will interpolate the current index +in a multi-count resource. For more information on count, see the +resource configuration page. ## Built-in Functions +Terraform ships with built-in functions. Functions are called with +the syntax `name(arg, arg2, ...)`. For example, +to read a file: `${file("path.txt")}`. The built-in functions +are documented below. + The supported built-in functions are: * `concat(args...)` - Concatenates the values of multiple arguments into diff --git a/website/source/docs/configuration/resources.html.md b/website/source/docs/configuration/resources.html.md index 1edad4340..03e3eedef 100644 --- a/website/source/docs/configuration/resources.html.md +++ b/website/source/docs/configuration/resources.html.md @@ -42,7 +42,9 @@ resource type in the There are **meta-parameters** available to all resources: * `count` (int) - The number of identical resources to create. - This doesn't apply to all resources. + This doesn't apply to all resources. You can use the `${count.index}` + [interpolation](/docs/configuration/interpolation.html) to reference + the current count index in your resource. * `depends_on` (list of strings) - Explicit dependencies that this resource has. These dependencies will be created before this