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

Local Values #15449

Merged
merged 5 commits into from
Aug 21, 2017
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
32 changes: 31 additions & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type Config struct {
ProviderConfigs []*ProviderConfig
Resources []*Resource
Variables []*Variable
Locals []*Local
Outputs []*Output

// The fields below can be filled in by loaders for validation
Expand Down Expand Up @@ -147,14 +148,20 @@ func (p *Provisioner) Copy() *Provisioner {
}
}

// Variable is a variable defined within the configuration.
// Variable is a module argument defined within the configuration.
type Variable struct {
Name string
DeclaredType string `mapstructure:"type"`
Default interface{}
Description string
}

// Local is a local value defined within the configuration.
type Local struct {
Name string
RawConfig *RawConfig
}

// Output is an output defined within the configuration. An output is
// resulting data that is highlighted by Terraform when finished. An
// output marked Sensitive will be output in a masked form following
Expand Down Expand Up @@ -680,6 +687,29 @@ func (c *Config) Validate() error {
}
}

// Check that all locals are valid
{
found := make(map[string]struct{})
for _, l := range c.Locals {
if _, ok := found[l.Name]; ok {
errs = append(errs, fmt.Errorf(
"%s: duplicate local. local value names must be unique",
l.Name,
))
continue
}
found[l.Name] = struct{}{}

for _, v := range l.RawConfig.Variables {
if _, ok := v.(*CountVariable); ok {
errs = append(errs, fmt.Errorf(
"local %s: count variables are only valid within resources", l.Name,
))
}
}
}
}

// Check that all outputs are valid
{
found := make(map[string]struct{})
Expand Down
36 changes: 36 additions & 0 deletions config/config_string.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,42 @@ func outputsStr(os []*Output) string {
return strings.TrimSpace(result)
}

func localsStr(ls []*Local) string {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be in a test file, or is it intended to be used elsewhere eventually?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm honestly not sure why these are here vs. in a test-only file, but I was following the precedent set by the others above.

ns := make([]string, 0, len(ls))
m := make(map[string]*Local)
for _, l := range ls {
ns = append(ns, l.Name)
m[l.Name] = l
}
sort.Strings(ns)

result := ""
for _, n := range ns {
l := m[n]

result += fmt.Sprintf("%s\n", n)

if len(l.RawConfig.Variables) > 0 {
result += fmt.Sprintf(" vars\n")
for _, rawV := range l.RawConfig.Variables {
kind := "unknown"
str := rawV.FullKey()

switch rawV.(type) {
case *ResourceVariable:
kind = "resource"
case *UserVariable:
kind = "user"
}

result += fmt.Sprintf(" %s: %s\n", kind, str)
}
}
}

return strings.TrimSpace(result)
}

// This helper turns a provider configs field into a deterministic
// string value for comparison in tests.
func providerConfigsStr(pcs []*ProviderConfig) string {
Expand Down
27 changes: 27 additions & 0 deletions config/interpolate.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ type UserVariable struct {
key string
}

// A LocalVariable is a variable that references a local value defined within
// the current module, via a "locals" block. This looks like "${local.foo}".
type LocalVariable struct {
Name string
}

func NewInterpolatedVariable(v string) (InterpolatedVariable, error) {
if strings.HasPrefix(v, "count.") {
return NewCountVariable(v)
Expand All @@ -112,6 +118,8 @@ func NewInterpolatedVariable(v string) (InterpolatedVariable, error) {
return NewTerraformVariable(v)
} else if strings.HasPrefix(v, "var.") {
return NewUserVariable(v)
} else if strings.HasPrefix(v, "local.") {
return NewLocalVariable(v)
} else if strings.HasPrefix(v, "module.") {
return NewModuleVariable(v)
} else if !strings.ContainsRune(v, '.') {
Expand Down Expand Up @@ -331,6 +339,25 @@ func (v *UserVariable) GoString() string {
return fmt.Sprintf("*%#v", *v)
}

func NewLocalVariable(key string) (*LocalVariable, error) {
name := key[len("local."):]
if idx := strings.Index(name, "."); idx > -1 {
return nil, fmt.Errorf("Can't use dot (.) attribute access in local.%s; use square bracket indexing", name)
}

return &LocalVariable{
Name: name,
}, nil
}

func (v *LocalVariable) FullKey() string {
return fmt.Sprintf("local.%s", v.Name)
}

func (v *LocalVariable) GoString() string {
return fmt.Sprintf("*%#v", *v)
}

// DetectVariables takes an AST root and returns all the interpolated
// variables that are detected in the AST tree.
func DetectVariables(root ast.Node) ([]InterpolatedVariable, error) {
Expand Down
41 changes: 29 additions & 12 deletions config/interpolate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import (
)

func TestNewInterpolatedVariable(t *testing.T) {
cases := []struct {
Input string
Result InterpolatedVariable
Error bool
tests := []struct {
Input string
Want InterpolatedVariable
Error bool
}{
{
"var.foo",
Expand All @@ -22,6 +22,18 @@ func TestNewInterpolatedVariable(t *testing.T) {
},
false,
},
{
"local.foo",
&LocalVariable{
Name: "foo",
},
false,
},
{
"local.foo.nope",
nil,
true,
},
{
"module.foo.bar",
&ModuleVariable{
Expand Down Expand Up @@ -73,14 +85,19 @@ func TestNewInterpolatedVariable(t *testing.T) {
},
}

for i, tc := range cases {
actual, err := NewInterpolatedVariable(tc.Input)
if err != nil != tc.Error {
t.Fatalf("%d. Error: %s", i, err)
}
if !reflect.DeepEqual(actual, tc.Result) {
t.Fatalf("%d bad: %#v", i, actual)
}
for i, test := range tests {
t.Run(test.Input, func(t *testing.T) {
got, err := NewInterpolatedVariable(test.Input)
if err != nil != test.Error {
t.Errorf("%d. Error: %s", i, err)
}
if !test.Error && !reflect.DeepEqual(got, test.Want) {
t.Errorf(
"wrong result\ninput: %s\ngot: %#v\nwant: %#v",
test.Input, got, test.Want,
)
}
})
}
}

Expand Down
63 changes: 63 additions & 0 deletions config/loader_hcl.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func (t *hclConfigurable) Config() (*Config, error) {
validKeys := map[string]struct{}{
"atlas": struct{}{},
"data": struct{}{},
"locals": struct{}{},
"module": struct{}{},
"output": struct{}{},
"provider": struct{}{},
Expand Down Expand Up @@ -72,6 +73,15 @@ func (t *hclConfigurable) Config() (*Config, error) {
}
}

// Build local values
if locals := list.Filter("locals"); len(locals.Items) > 0 {
var err error
config.Locals, err = loadLocalsHcl(locals)
if err != nil {
return nil, err
}
}

// Get Atlas configuration
if atlas := list.Filter("atlas"); len(atlas.Items) > 0 {
var err error
Expand Down Expand Up @@ -408,6 +418,59 @@ func loadModulesHcl(list *ast.ObjectList) ([]*Module, error) {
return result, nil
}

// loadLocalsHcl recurses into the given HCL object turns it into
// a list of locals.
func loadLocalsHcl(list *ast.ObjectList) ([]*Local, error) {

result := make([]*Local, 0, len(list.Items))

for _, block := range list.Items {
if len(block.Keys) > 0 {
return nil, fmt.Errorf(
"locals block at %s should not have label %q",
block.Pos(), block.Keys[0].Token.Value(),
)
}

blockObj, ok := block.Val.(*ast.ObjectType)
if !ok {
return nil, fmt.Errorf("locals value at %s should be a block", block.Val.Pos())
}

// blockObj now contains directly our local decls
for _, item := range blockObj.List.Items {
if len(item.Keys) != 1 {
return nil, fmt.Errorf("local declaration at %s may not be a block", item.Val.Pos())
}

// By the time we get here there can only be one item left, but
// we'll decode into a map anyway because it's a convenient way
// to extract both the key and the value robustly.
kv := map[string]interface{}{}
hcl.DecodeObject(&kv, item)
for k, v := range kv {
rawConfig, err := NewRawConfig(map[string]interface{}{
"value": v,
})

if err != nil {
return nil, fmt.Errorf(
"error parsing local value %q at %s: %s",
k, item.Val.Pos(), err,
)
}

result = append(result, &Local{
Name: k,
RawConfig: rawConfig,
})
}
}
}

return result, nil
}

// LoadOutputsHcl recurses into the given HCL object and turns
// it into a mapping of outputs.
func loadOutputsHcl(list *ast.ObjectList) ([]*Output, error) {
Expand Down
26 changes: 23 additions & 3 deletions config/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,17 +180,17 @@ func TestLoadFileBasic(t *testing.T) {
}

if c.Dir != "" {
t.Fatalf("bad: %#v", c.Dir)
t.Fatalf("wrong dir %#v; want %#v", c.Dir, "")
}

expectedTF := &Terraform{RequiredVersion: "foo"}
if !reflect.DeepEqual(c.Terraform, expectedTF) {
t.Fatalf("bad: %#v", c.Terraform)
t.Fatalf("wrong terraform block %#v; want %#v", c.Terraform, expectedTF)
}

expectedAtlas := &AtlasConfig{Name: "mitchellh/foo"}
if !reflect.DeepEqual(c.Atlas, expectedAtlas) {
t.Fatalf("bad: %#v", c.Atlas)
t.Fatalf("wrong atlas config %#v; want %#v", c.Atlas, expectedAtlas)
}

actual := variablesStr(c.Variables)
Expand All @@ -208,6 +208,10 @@ func TestLoadFileBasic(t *testing.T) {
t.Fatalf("bad:\n%s", actual)
}

if actual, want := localsStr(c.Locals), strings.TrimSpace(basicLocalsStr); actual != want {
t.Fatalf("wrong locals:\n%s\nwant:\n%s", actual, want)
}

actual = outputsStr(c.Outputs)
if actual != strings.TrimSpace(basicOutputsStr) {
t.Fatalf("bad:\n%s", actual)
Expand Down Expand Up @@ -288,6 +292,10 @@ func TestLoadFileBasic_json(t *testing.T) {
t.Fatalf("bad:\n%s", actual)
}

if actual, want := localsStr(c.Locals), strings.TrimSpace(basicLocalsStr); actual != want {
t.Fatalf("wrong locals:\n%s\nwant:\n%s", actual, want)
}

actual = outputsStr(c.Outputs)
if actual != strings.TrimSpace(basicOutputsStr) {
t.Fatalf("bad:\n%s", actual)
Expand Down Expand Up @@ -1055,6 +1063,18 @@ web_ip
resource: aws_instance.web.private_ip
`

const basicLocalsStr = `
literal
literal_list
literal_map
security_group_ids
vars
resource: aws_security_group.firewall.*.id
web_ip
vars
resource: aws_instance.web.private_ip
`

const basicProvidersStr = `
aws
access_key
Expand Down
11 changes: 11 additions & 0 deletions config/test-fixtures/basic.tf
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,17 @@ resource aws_instance "web" {
}
}

locals {
security_group_ids = "${aws_security_group.firewall.*.id}"
web_ip = "${aws_instance.web.private_ip}"
}

locals {
literal = 2
literal_list = ["foo"]
literal_map = {"foo" = "bar"}
}

resource "aws_instance" "db" {
security_groups = "${aws_security_group.firewall.*.id}"
VPC = "foo"
Expand Down
8 changes: 8 additions & 0 deletions config/test-fixtures/basic.tf.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@
}
},

"locals": {
"security_group_ids": "${aws_security_group.firewall.*.id}",
"web_ip": "${aws_instance.web.private_ip}",
"literal": 2,
"literal_list": ["foo"],
"literal_map": {"foo": "bar"}
},

"output": {
"web_ip": {
"value": "${aws_instance.web.private_ip}"
Expand Down
Loading