terraform/config/interpolate.go

297 lines
6.4 KiB
Go

package config
import (
"fmt"
"regexp"
"strconv"
"strings"
)
// We really need to replace this with a real parser.
var funcRegexp *regexp.Regexp = regexp.MustCompile(
`(?i)([a-z0-9_]+)\(\s*(?:([.a-z0-9_]+)\s*,\s*)*([.a-z0-9_]+)\s*\)`)
// Interpolation is something that can be contained in a "${}" in a
// configuration value.
//
// Interpolations might be simple variable references, or it might be
// function calls, or even nested function calls.
type Interpolation interface {
FullString() string
Interpolate(map[string]string) (string, error)
Variables() map[string]InterpolatedVariable
}
// InterpolationFunc is the function signature for implementing
// callable functions in Terraform configurations.
type InterpolationFunc func(map[string]string, ...string) (string, error)
// An InterpolatedVariable is a variable reference within an interpolation.
//
// Implementations of this interface represents various sources where
// variables can come from: user variables, resources, etc.
type InterpolatedVariable interface {
FullKey() string
}
// FunctionInterpolation is an Interpolation that executes a function
// with some variable number of arguments to generate a value.
type FunctionInterpolation struct {
Func InterpolationFunc
Args []InterpolatedVariable
key string
}
// VariableInterpolation implements Interpolation for simple variable
// interpolation. Ex: "${var.foo}" or "${aws_instance.foo.bar}"
type VariableInterpolation struct {
Variable InterpolatedVariable
key string
}
// A ResourceVariable is a variable that is referencing the field
// of a resource, such as "${aws_instance.foo.ami}"
type ResourceVariable struct {
Type string // Resource type, i.e. "aws_instance"
Name string // Resource name
Field string // Resource field
Multi bool // True if multi-variable: aws_instance.foo.*.id
Index int // Index for multi-variable: aws_instance.foo.1.id == 1
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}"
type UserVariable struct {
Name string
key string
}
// A UserMapVariable is a variable that is referencing a user
// variable that is a map. This looks like "${var.amis.us-east-1}"
type UserMapVariable struct {
Name string
Elem string
key string
}
// NewInterpolation takes some string and returns the valid
// Interpolation associated with it, or error if a valid
// interpolation could not be found or the interpolation itself
// is invalid.
func NewInterpolation(v string) (Interpolation, error) {
match := funcRegexp.FindStringSubmatch(v)
if match != nil {
fn, ok := Funcs[match[1]]
if !ok {
return nil, fmt.Errorf(
"%s: Unknown function '%s'",
v, match[1])
}
args := make([]InterpolatedVariable, 0, len(match)-2)
for i := 2; i < len(match); i++ {
// This can be empty if we have a single argument
// due to the format of the regexp.
if match[i] == "" {
continue
}
v, err := NewInterpolatedVariable(match[i])
if err != nil {
return nil, err
}
args = append(args, v)
}
return &FunctionInterpolation{
Func: fn,
Args: args,
key: v,
}, nil
}
if idx := strings.Index(v, "."); idx >= 0 {
v, err := NewInterpolatedVariable(v)
if err != nil {
return nil, err
}
return &VariableInterpolation{
Variable: v,
key: v.FullKey(),
}, nil
}
return nil, fmt.Errorf(
"Interpolation '%s' is not a valid interpolation. " +
"Please check your syntax and try again.")
}
func NewInterpolatedVariable(v string) (InterpolatedVariable, error) {
if !strings.HasPrefix(v, "var.") {
return NewResourceVariable(v)
}
varKey := v[len("var."):]
if strings.Index(varKey, ".") == -1 {
return NewUserVariable(v)
} else {
return NewUserMapVariable(v)
}
}
func (i *FunctionInterpolation) FullString() string {
return i.key
}
func (i *FunctionInterpolation) Interpolate(
vs map[string]string) (string, error) {
args := make([]string, len(i.Args))
for idx, a := range i.Args {
k := a.FullKey()
v, ok := vs[k]
if !ok {
return "", fmt.Errorf(
"%s: variable argument value unknown: %s",
i.FullString(),
k)
}
args[idx] = v
}
return i.Func(vs, args...)
}
func (i *FunctionInterpolation) Variables() map[string]InterpolatedVariable {
result := make(map[string]InterpolatedVariable)
for _, a := range i.Args {
k := a.FullKey()
if _, ok := result[k]; ok {
continue
}
result[k] = a
}
return result
}
func (i *VariableInterpolation) FullString() string {
return i.key
}
func (i *VariableInterpolation) Interpolate(
vs map[string]string) (string, error) {
v, ok := vs[i.key]
if !ok {
return "", fmt.Errorf(
"%s: value for variable '%s' not found", v)
}
return v, nil
}
func (i *VariableInterpolation) Variables() map[string]InterpolatedVariable {
return map[string]InterpolatedVariable{i.key: i.Variable}
}
func NewResourceVariable(key string) (*ResourceVariable, error) {
parts := strings.SplitN(key, ".", 3)
if len(parts) < 3 {
return nil, fmt.Errorf(
"%s: resource variables must be three parts: type.name.attr",
key)
}
field := parts[2]
multi := false
var index int
if idx := strings.Index(field, "."); idx != -1 {
indexStr := field[:idx]
multi = indexStr == "*"
index = -1
if !multi {
indexInt, err := strconv.ParseInt(indexStr, 0, 0)
if err == nil {
multi = true
index = int(indexInt)
}
}
if multi {
field = field[idx+1:]
}
}
return &ResourceVariable{
Type: parts[0],
Name: parts[1],
Field: field,
Multi: multi,
Index: index,
key: key,
}, nil
}
func (v *ResourceVariable) ResourceId() string {
return fmt.Sprintf("%s.%s", v.Type, v.Name)
}
func (v *ResourceVariable) FullKey() string {
return v.key
}
func NewUserVariable(key string) (*UserVariable, error) {
name := key[len("var."):]
return &UserVariable{
key: key,
Name: name,
}, nil
}
func (v *UserVariable) FullKey() string {
return v.key
}
func (v *UserVariable) GoString() string {
return fmt.Sprintf("*%#v", *v)
}
func NewUserMapVariable(key string) (*UserMapVariable, error) {
name := key[len("var."):]
idx := strings.Index(name, ".")
if idx == -1 {
return nil, fmt.Errorf("not a user map variable: %s", key)
}
elem := name[idx+1:]
name = name[:idx]
return &UserMapVariable{
Name: name,
Elem: elem,
key: key,
}, nil
}
func (v *UserMapVariable) FullKey() string {
return v.key
}
func (v *UserMapVariable) GoString() string {
return fmt.Sprintf("%#v", *v)
}