config: parsing of "locals" blocks in configuration

This commit is contained in:
Martin Atkins 2017-07-01 09:12:31 -07:00
parent e2272f71a0
commit f6797d6cb0
6 changed files with 172 additions and 4 deletions

View File

@ -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{})

View File

@ -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 {

View File

@ -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) {

View File

@ -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

View File

@ -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"

View File

@ -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}"