Merge pull request #355 from hashicorp/f-count-interp

Variable to interpolate count index
This commit is contained in:
Mitchell Hashimoto 2014-10-02 22:12:56 -07:00
commit d28a662b6c
17 changed files with 232 additions and 23 deletions

View File

@ -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 // Check that all references to modules are valid
modules := make(map[string]*Module) modules := make(map[string]*Module)
dupped := make(map[string]struct{}) dupped := make(map[string]struct{})
@ -258,6 +275,11 @@ func (c *Config) Validate() error {
// Verify count variables // Verify count variables
for _, v := range r.RawCount.Variables { for _, v := range r.RawCount.Variables {
switch v.(type) { switch v.(type) {
case *CountVariable:
errs = append(errs, fmt.Errorf(
"%s: resource count can't reference count variable: %s",
n,
v.FullKey()))
case *ModuleVariable: case *ModuleVariable:
errs = append(errs, fmt.Errorf( errs = append(errs, fmt.Errorf(
"%s: resource count can't reference module variable: %s", "%s: resource count can't reference module variable: %s",

View File

@ -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) { func TestConfigValidate_countModuleVar(t *testing.T) {
c := testConfig(t, "validate-count-module-var") c := testConfig(t, "validate-count-module-var")
if err := c.Validate(); err == nil { 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) { func TestConfigValidate_dupModule(t *testing.T) {
c := testConfig(t, "validate-dup-module") c := testConfig(t, "validate-dup-module")
if err := c.Validate(); err == nil { if err := c.Validate(); err == nil {

View File

@ -52,6 +52,21 @@ type VariableInterpolation struct {
Variable InterpolatedVariable 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 // A ModuleVariable is a variable that is referencing the output
// of a module, such as "${module.foo.bar}" // of a module, such as "${module.foo.bar}"
type ModuleVariable struct { type ModuleVariable struct {
@ -84,7 +99,9 @@ type UserVariable struct {
} }
func NewInterpolatedVariable(v string) (InterpolatedVariable, error) { 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) return NewUserVariable(v)
} else if strings.HasPrefix(v, "module.") { } else if strings.HasPrefix(v, "module.") {
return NewModuleVariable(v) return NewModuleVariable(v)
@ -152,6 +169,24 @@ func (i *VariableInterpolation) Variables() map[string]InterpolatedVariable {
return map[string]InterpolatedVariable{i.Variable.FullKey(): i.Variable} 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) { func NewModuleVariable(key string) (*ModuleVariable, error) {
parts := strings.SplitN(key, ".", 3) parts := strings.SplitN(key, ".", 3)
if len(parts) < 3 { if len(parts) < 3 {

View File

@ -29,6 +29,22 @@ func TestNewInterpolatedVariable(t *testing.T) {
}, },
false, false,
}, },
{
"count.index",
&CountVariable{
Type: CountValueIndex,
key: "count.index",
},
false,
},
{
"count.nope",
&CountVariable{
Type: CountValueInvalid,
key: "count.nope",
},
false,
},
} }
for i, tc := range cases { for i, tc := range cases {

View File

@ -0,0 +1,3 @@
resource "aws_instance" "web" {
count = "${count.index}"
}

View File

@ -0,0 +1,3 @@
resource "aws_instance" "foo" {
foo = "${count.foo}"
}

View File

@ -0,0 +1,3 @@
resource "aws_instance" "foo" {
foo = "${count.index}"
}

View File

@ -498,7 +498,7 @@ func (c *walkContext) Walk() error {
outputs := make(map[string]string) outputs := make(map[string]string)
for _, o := range conf.Outputs { for _, o := range conf.Outputs {
if err := c.computeVars(o.RawConfig); err != nil { if err := c.computeVars(o.RawConfig, nil); err != nil {
return err return err
} }
vraw := o.RawConfig.Config()["value"] vraw := o.RawConfig.Config()["value"]
@ -619,7 +619,7 @@ func (c *walkContext) applyWalkFn() depgraph.WalkFunc {
if !diff.Destroy { if !diff.Destroy {
// Since we need the configuration, interpolate the variables // 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 return err
} }
@ -780,7 +780,7 @@ func (c *walkContext) planWalkFn() depgraph.WalkFunc {
diff = &InstanceDiff{Destroy: true} diff = &InstanceDiff{Destroy: true}
} else { } else {
// Make sure the configuration is interpolated // 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 return err
} }
@ -993,7 +993,7 @@ func (c *walkContext) validateWalkFn() depgraph.WalkFunc {
if rn.ExpandMode > ResourceExpandNone { if rn.ExpandMode > ResourceExpandNone {
// Interpolate the count and verify it is non-negative // Interpolate the count and verify it is non-negative
rc := NewResourceConfig(rn.Config.RawCount) rc := NewResourceConfig(rn.Config.RawCount)
rc.interpolate(c) rc.interpolate(c, rn.Resource)
count, err := rn.Config.Count() count, err := rn.Config.Count()
if err == nil { if err == nil {
if count < 0 { if count < 0 {
@ -1063,7 +1063,7 @@ func (c *walkContext) validateWalkFn() depgraph.WalkFunc {
for k, p := range sharedProvider.Providers { for k, p := range sharedProvider.Providers {
// Merge the configurations to get what we use to configure with // Merge the configurations to get what we use to configure with
rc := sharedProvider.MergeConfig(false, cs[k]) rc := sharedProvider.MergeConfig(false, cs[k])
rc.interpolate(c) rc.interpolate(c, nil)
log.Printf("[INFO] Validating provider: %s", k) log.Printf("[INFO] Validating provider: %s", k)
ws, es := p.Validate(rc) ws, es := p.Validate(rc)
@ -1125,7 +1125,7 @@ func (c *walkContext) genericWalkFn(cb genericWalkFunc) depgraph.WalkFunc {
wc.Variables = make(map[string]string) wc.Variables = make(map[string]string)
rc := NewResourceConfig(m.Config.RawConfig) rc := NewResourceConfig(m.Config.RawConfig)
rc.interpolate(c) rc.interpolate(c, nil)
for k, v := range rc.Config { for k, v := range rc.Config {
wc.Variables[k] = v.(string) wc.Variables[k] = v.(string)
} }
@ -1151,7 +1151,7 @@ func (c *walkContext) genericWalkFn(cb genericWalkFunc) depgraph.WalkFunc {
for k, p := range sharedProvider.Providers { for k, p := range sharedProvider.Providers {
// Merge the configurations to get what we use to configure with // Merge the configurations to get what we use to configure with
rc := sharedProvider.MergeConfig(false, cs[k]) rc := sharedProvider.MergeConfig(false, cs[k])
rc.interpolate(c) rc.interpolate(c, nil)
log.Printf("[INFO] Configuring provider: %s", k) log.Printf("[INFO] Configuring provider: %s", k)
err := p.Configure(rc) err := p.Configure(rc)
@ -1211,7 +1211,7 @@ func (c *walkContext) genericWalkResource(
rn *GraphNodeResource, fn depgraph.WalkFunc) error { rn *GraphNodeResource, fn depgraph.WalkFunc) error {
// Interpolate the count // Interpolate the count
rc := NewResourceConfig(rn.Config.RawCount) rc := NewResourceConfig(rn.Config.RawCount)
rc.interpolate(c) rc.interpolate(c, rn.Resource)
// Expand the node to the actual resources // Expand the node to the actual resources
ns, err := rn.Expand() ns, err := rn.Expand()
@ -1260,13 +1260,13 @@ func (c *walkContext) applyProvisioners(r *Resource, is *InstanceState) error {
for _, prov := range r.Provisioners { for _, prov := range r.Provisioners {
// Interpolate since we may have variables that depend on the // Interpolate since we may have variables that depend on the
// local resource. // local resource.
if err := prov.Config.interpolate(c); err != nil { if err := prov.Config.interpolate(c, r); err != nil {
return err return err
} }
// Interpolate the conn info, since it may contain variables // Interpolate the conn info, since it may contain variables
connInfo := NewResourceConfig(prov.ConnInfo) connInfo := NewResourceConfig(prov.ConnInfo)
if err := connInfo.interpolate(c); err != nil { if err := connInfo.interpolate(c, r); err != nil {
return err return err
} }
@ -1396,7 +1396,8 @@ func (c *walkContext) persistState(r *Resource) {
// computeVars takes the State and given RawConfig and processes all // computeVars takes the State and given RawConfig and processes all
// the variables. This dynamically discovers the attributes instead of // the variables. This dynamically discovers the attributes instead of
// using a static map[string]string that the genericWalkFn uses. // 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 there isn't a raw configuration, don't do anything
if raw == nil { if raw == nil {
return nil return nil
@ -1411,6 +1412,11 @@ func (c *walkContext) computeVars(raw *config.RawConfig) error {
// Next, the actual computed variables // Next, the actual computed variables
for n, rawV := range raw.Variables { for n, rawV := range raw.Variables {
switch v := rawV.(type) { switch v := rawV.(type) {
case *config.CountVariable:
switch v.Type {
case config.CountValueIndex:
vs[n] = strconv.FormatInt(int64(r.CountIndex), 10)
}
case *config.ModuleVariable: case *config.ModuleVariable:
value, err := c.computeModuleVariable(v) value, err := c.computeModuleVariable(v)
if err != nil { if err != nil {

View File

@ -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) { func TestContextPlan_countVar(t *testing.T) {
m := testModule(t, "plan-count-var") m := testModule(t, "plan-count-var")
p := testProvider("aws") p := testProvider("aws")

View File

@ -1704,6 +1704,7 @@ func (n *GraphNodeResource) expand(g *depgraph.Graph, count int) {
// Copy the base resource so we can fill it in // Copy the base resource so we can fill it in
resource := n.copyResource(name) resource := n.copyResource(name)
resource.CountIndex = i
resource.State = state.Primary resource.State = state.Primary
resource.Flags = flags resource.Flags = flags

View File

@ -34,6 +34,7 @@ type Resource struct {
Provider ResourceProvider Provider ResourceProvider
State *InstanceState State *InstanceState
Provisioners []*ResourceProvisionerConfig Provisioners []*ResourceProvisionerConfig
CountIndex int
Flags ResourceFlag Flags ResourceFlag
TaintedIndex int TaintedIndex int
} }
@ -92,7 +93,7 @@ type ResourceConfig struct {
// NewResourceConfig creates a new ResourceConfig from a config.RawConfig. // NewResourceConfig creates a new ResourceConfig from a config.RawConfig.
func NewResourceConfig(c *config.RawConfig) *ResourceConfig { func NewResourceConfig(c *config.RawConfig) *ResourceConfig {
result := &ResourceConfig{raw: c} result := &ResourceConfig{raw: c}
result.interpolate(nil) result.interpolate(nil, nil)
return result return result
} }
@ -190,13 +191,14 @@ func (c *ResourceConfig) get(
return current, true return current, true
} }
func (c *ResourceConfig) interpolate(ctx *walkContext) error { func (c *ResourceConfig) interpolate(
ctx *walkContext, r *Resource) error {
if c == nil { if c == nil {
return nil return nil
} }
if ctx != nil { if ctx != nil {
if err := ctx.computeVars(c.raw); err != nil { if err := ctx.computeVars(c.raw, r); err != nil {
return err return err
} }
} }

View File

@ -102,7 +102,10 @@ func TestResourceConfigGet(t *testing.T) {
rc := NewResourceConfig(rawC) rc := NewResourceConfig(rawC)
if tc.Vars != nil { if tc.Vars != nil {
ctx := NewContext(&ContextOpts{Variables: tc.Vars}) 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) t.Fatalf("err: %s", err)
} }
} }

View File

@ -477,6 +477,33 @@ STATE:
<no state> <no 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:
<no state>
`
const testTerraformPlanCountIndexZeroStr = `
DIFF:
CREATE: aws_instance.foo
foo: "" => "0"
type: "" => "aws_instance"
STATE:
<no state>
`
const testTerraformPlanCountOneIndexStr = ` const testTerraformPlanCountOneIndexStr = `
DIFF: DIFF:

View File

@ -0,0 +1,3 @@
resource "aws_instance" "foo" {
foo = "${count.index}"
}

View File

@ -0,0 +1,4 @@
resource "aws_instance" "foo" {
count = 2
foo = "${count.index}"
}

View File

@ -14,7 +14,9 @@ into strings. These interpolations are wrapped in `${}`, such as
The interpolation syntax is powerful and allows you to reference The interpolation syntax is powerful and allows you to reference
variables, attributes of resources, call functions, etc. 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 variable name. For example, `${var.foo}` will interpolate the
`foo` variable value. If the variable is a mapping, then you `foo` variable value. If the variable is a mapping, then you
can reference static keys in the map with the syntax 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 get the value of the `us-east-1` key within the `amis` variable
that is a mapping. 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}` `TYPE.NAME.ATTRIBUTE`. For example, `${aws_instance.web.id}`
will interpolate the ID attribute from the "aws\_instance" will interpolate the ID attribute from the "aws\_instance"
resource named "web". resource named "web".
Finally, Terraform ships with built-in functions. Functions **To reference outputs from a module**, the syntax is
are called with the syntax `name(arg, arg2, ...)`. For example, `MODULE.NAME.OUTPUT`. For example `${module.foo.bar}` will
to read a file: `${file("path.txt")}`. The built-in functions interpolate the "bar" output from the "foo"
are documented below. [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 ## 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: The supported built-in functions are:
* `concat(args...)` - Concatenates the values of multiple arguments into * `concat(args...)` - Concatenates the values of multiple arguments into

View File

@ -42,7 +42,9 @@ resource type in the
There are **meta-parameters** available to all resources: There are **meta-parameters** available to all resources:
* `count` (int) - The number of identical resources to create. * `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 * `depends_on` (list of strings) - Explicit dependencies that this
resource has. These dependencies will be created before this resource has. These dependencies will be created before this