From f6797d6cb0558b1c1eb05add6857e25d46383a9d Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Sat, 1 Jul 2017 09:12:31 -0700 Subject: [PATCH] config: parsing of "locals" blocks in configuration --- config/config.go | 32 ++++++++++++++- config/config_string.go | 36 +++++++++++++++++ config/loader_hcl.go | 63 ++++++++++++++++++++++++++++++ config/loader_test.go | 26 ++++++++++-- config/test-fixtures/basic.tf | 11 ++++++ config/test-fixtures/basic.tf.json | 8 ++++ 6 files changed, 172 insertions(+), 4 deletions(-) diff --git a/config/config.go b/config/config.go index 3f756dcf4..9549d0a50 100644 --- a/config/config.go +++ b/config/config.go @@ -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 @@ -147,7 +148,7 @@ 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"` @@ -155,6 +156,12 @@ type Variable struct { 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 @@ -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{}) diff --git a/config/config_string.go b/config/config_string.go index 0b3abbcd5..b57a09fb5 100644 --- a/config/config_string.go +++ b/config/config_string.go @@ -148,6 +148,42 @@ func outputsStr(os []*Output) string { return strings.TrimSpace(result) } +func localsStr(ls []*Local) string { + 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 { diff --git a/config/loader_hcl.go b/config/loader_hcl.go index bcd4d43a4..311083744 100644 --- a/config/loader_hcl.go +++ b/config/loader_hcl.go @@ -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{}{}, @@ -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 @@ -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) { diff --git a/config/loader_test.go b/config/loader_test.go index a3aeb7321..30a3f2444 100644 --- a/config/loader_test.go +++ b/config/loader_test.go @@ -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) @@ -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) @@ -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) @@ -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 diff --git a/config/test-fixtures/basic.tf b/config/test-fixtures/basic.tf index aa5a5c6ed..f64d6a8d5 100644 --- a/config/test-fixtures/basic.tf +++ b/config/test-fixtures/basic.tf @@ -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" diff --git a/config/test-fixtures/basic.tf.json b/config/test-fixtures/basic.tf.json index be86d5de5..6541beae6 100644 --- a/config/test-fixtures/basic.tf.json +++ b/config/test-fixtures/basic.tf.json @@ -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}"