From 15c0b25d011f37e7c20aeca9eaf461f78285b8d9 Mon Sep 17 00:00:00 2001 From: Alex Pilon Date: Fri, 22 Feb 2019 18:24:37 -0500 Subject: deps: github.com/hashicorp/terraform@sdk-v0.11-with-go-modules Updated via: go get github.com/hashicorp/terraform@sdk-v0.11-with-go-modules and go mod tidy --- .../hashicorp/terraform/config/append.go | 6 + .../hashicorp/terraform/config/config.go | 396 +++++++++++------ .../hashicorp/terraform/config/config_string.go | 40 ++ .../terraform/config/configschema/decoder_spec.go | 97 +++++ .../hashicorp/terraform/config/configschema/doc.go | 14 + .../terraform/config/configschema/implied_type.go | 21 + .../config/configschema/internal_validate.go | 92 ++++ .../config/configschema/nestingmode_string.go | 16 + .../terraform/config/configschema/schema.go | 107 +++++ .../hashicorp/terraform/config/hcl2_shim_util.go | 134 ++++++ .../terraform/config/hcl2shim/single_attr_body.go | 85 ++++ .../hashicorp/terraform/config/hcl2shim/values.go | 246 +++++++++++ .../hashicorp/terraform/config/import_tree.go | 54 ++- .../hashicorp/terraform/config/interpolate.go | 55 ++- .../terraform/config/interpolate_funcs.go | 429 ++++++++++++++++--- .../hashicorp/terraform/config/interpolate_walk.go | 4 +- .../hashicorp/terraform/config/loader.go | 5 +- .../hashicorp/terraform/config/loader_hcl.go | 126 +++++- .../hashicorp/terraform/config/loader_hcl2.go | 473 +++++++++++++++++++++ .../github.com/hashicorp/terraform/config/merge.go | 11 + .../hashicorp/terraform/config/module/get.go | 20 +- .../hashicorp/terraform/config/module/inode.go | 2 +- .../hashicorp/terraform/config/module/module.go | 6 +- .../hashicorp/terraform/config/module/storage.go | 365 ++++++++++++++++ .../hashicorp/terraform/config/module/testing.go | 6 +- .../hashicorp/terraform/config/module/tree.go | 314 ++++++++++---- .../config/module/validate_provider_alias.go | 2 +- .../hashicorp/terraform/config/module/versions.go | 95 +++++ .../hashicorp/terraform/config/raw_config.go | 125 +++++- .../terraform/config/resource_mode_string.go | 4 +- .../hashicorp/terraform/config/testing.go | 2 + 31 files changed, 3022 insertions(+), 330 deletions(-) create mode 100644 vendor/github.com/hashicorp/terraform/config/configschema/decoder_spec.go create mode 100644 vendor/github.com/hashicorp/terraform/config/configschema/doc.go create mode 100644 vendor/github.com/hashicorp/terraform/config/configschema/implied_type.go create mode 100644 vendor/github.com/hashicorp/terraform/config/configschema/internal_validate.go create mode 100644 vendor/github.com/hashicorp/terraform/config/configschema/nestingmode_string.go create mode 100644 vendor/github.com/hashicorp/terraform/config/configschema/schema.go create mode 100644 vendor/github.com/hashicorp/terraform/config/hcl2_shim_util.go create mode 100644 vendor/github.com/hashicorp/terraform/config/hcl2shim/single_attr_body.go create mode 100644 vendor/github.com/hashicorp/terraform/config/hcl2shim/values.go create mode 100644 vendor/github.com/hashicorp/terraform/config/loader_hcl2.go create mode 100644 vendor/github.com/hashicorp/terraform/config/module/storage.go create mode 100644 vendor/github.com/hashicorp/terraform/config/module/versions.go (limited to 'vendor/github.com/hashicorp/terraform/config') diff --git a/vendor/github.com/hashicorp/terraform/config/append.go b/vendor/github.com/hashicorp/terraform/config/append.go index 5f4e89e..9d80c42 100644 --- a/vendor/github.com/hashicorp/terraform/config/append.go +++ b/vendor/github.com/hashicorp/terraform/config/append.go @@ -82,5 +82,11 @@ func Append(c1, c2 *Config) (*Config, error) { c.Variables = append(c.Variables, c2.Variables...) } + if len(c1.Locals) > 0 || len(c2.Locals) > 0 { + c.Locals = make([]*Local, 0, len(c1.Locals)+len(c2.Locals)) + c.Locals = append(c.Locals, c1.Locals...) + c.Locals = append(c.Locals, c2.Locals...) + } + return c, nil } diff --git a/vendor/github.com/hashicorp/terraform/config/config.go b/vendor/github.com/hashicorp/terraform/config/config.go index 3f756dc..1772fd7 100644 --- a/vendor/github.com/hashicorp/terraform/config/config.go +++ b/vendor/github.com/hashicorp/terraform/config/config.go @@ -8,11 +8,11 @@ import ( "strconv" "strings" - "github.com/hashicorp/go-multierror" - "github.com/hashicorp/hil" + hcl2 "github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/hil/ast" "github.com/hashicorp/terraform/helper/hilmapstructure" "github.com/hashicorp/terraform/plugin/discovery" + "github.com/hashicorp/terraform/tfdiags" "github.com/mitchellh/reflectwalk" ) @@ -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 @@ -55,6 +56,8 @@ type AtlasConfig struct { type Module struct { Name string Source string + Version string + Providers map[string]string RawConfig *RawConfig } @@ -147,7 +150,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 +158,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 @@ -222,7 +231,10 @@ func (r *Resource) Count() (int, error) { v, err := strconv.ParseInt(count, 0, 0) if err != nil { - return 0, err + return 0, fmt.Errorf( + "cannot parse %q as an integer", + count, + ) } return int(v), nil @@ -253,7 +265,9 @@ func (r *Resource) ProviderFullName() string { // the provider name is inferred from the resource type name. func ResourceProviderFullName(resourceType, explicitProvider string) string { if explicitProvider != "" { - return explicitProvider + // check for an explicit provider name, or return the original + parts := strings.SplitAfter(explicitProvider, "provider.") + return parts[len(parts)-1] } idx := strings.IndexRune(resourceType, '_') @@ -268,30 +282,35 @@ func ResourceProviderFullName(resourceType, explicitProvider string) string { } // Validate does some basic semantic checking of the configuration. -func (c *Config) Validate() error { +func (c *Config) Validate() tfdiags.Diagnostics { if c == nil { return nil } - var errs []error + var diags tfdiags.Diagnostics for _, k := range c.unknownKeys { - errs = append(errs, fmt.Errorf( - "Unknown root level key: %s", k)) + diags = diags.Append( + fmt.Errorf("Unknown root level key: %s", k), + ) } // Validate the Terraform config if tf := c.Terraform; tf != nil { - errs = append(errs, c.Terraform.Validate()...) + errs := c.Terraform.Validate() + for _, err := range errs { + diags = diags.Append(err) + } } vars := c.InterpolatedVariables() varMap := make(map[string]*Variable) for _, v := range c.Variables { if _, ok := varMap[v.Name]; ok { - errs = append(errs, fmt.Errorf( + diags = diags.Append(fmt.Errorf( "Variable '%s': duplicate found. Variable names must be unique.", - v.Name)) + v.Name, + )) } varMap[v.Name] = v @@ -299,17 +318,19 @@ func (c *Config) Validate() error { for k, _ := range varMap { if !NameRegexp.MatchString(k) { - errs = append(errs, fmt.Errorf( - "variable %q: variable name must match regular expresion %s", - k, NameRegexp)) + diags = diags.Append(fmt.Errorf( + "variable %q: variable name must match regular expression %s", + k, NameRegexp, + )) } } for _, v := range c.Variables { if v.Type() == VariableTypeUnknown { - errs = append(errs, fmt.Errorf( + diags = diags.Append(fmt.Errorf( "Variable '%s': must be a string or a map", - v.Name)) + v.Name, + )) continue } @@ -330,9 +351,10 @@ func (c *Config) Validate() error { if v.Default != nil { if err := reflectwalk.Walk(v.Default, w); err == nil { if interp { - errs = append(errs, fmt.Errorf( - "Variable '%s': cannot contain interpolations", - v.Name)) + diags = diags.Append(fmt.Errorf( + "variable %q: default may not contain interpolations", + v.Name, + )) } } } @@ -348,10 +370,11 @@ func (c *Config) Validate() error { } if _, ok := varMap[uv.Name]; !ok { - errs = append(errs, fmt.Errorf( - "%s: unknown variable referenced: '%s'. define it with 'variable' blocks", + diags = diags.Append(fmt.Errorf( + "%s: unknown variable referenced: '%s'; define it with a 'variable' block", source, - uv.Name)) + uv.Name, + )) } } } @@ -362,17 +385,19 @@ func (c *Config) Validate() error { switch v := rawV.(type) { case *CountVariable: if v.Type == CountValueInvalid { - errs = append(errs, fmt.Errorf( + diags = diags.Append(fmt.Errorf( "%s: invalid count variable: %s", source, - v.FullKey())) + v.FullKey(), + )) } case *PathVariable: if v.Type == PathValueInvalid { - errs = append(errs, fmt.Errorf( + diags = diags.Append(fmt.Errorf( "%s: invalid path variable: %s", source, - v.FullKey())) + v.FullKey(), + )) } } } @@ -380,27 +405,35 @@ func (c *Config) Validate() error { // Check that providers aren't declared multiple times and that their // version constraints, where present, are syntactically valid. - providerSet := make(map[string]struct{}) + providerSet := make(map[string]bool) for _, p := range c.ProviderConfigs { name := p.FullName() if _, ok := providerSet[name]; ok { - errs = append(errs, fmt.Errorf( - "provider.%s: declared multiple times, you can only declare a provider once", - name)) + diags = diags.Append(fmt.Errorf( + "provider.%s: multiple configurations present; only one configuration is allowed per provider", + name, + )) continue } if p.Version != "" { _, err := discovery.ConstraintStr(p.Version).Parse() if err != nil { - errs = append(errs, fmt.Errorf( - "provider.%s: invalid version constraint %q: %s", - name, p.Version, err, - )) + diags = diags.Append(&hcl2.Diagnostic{ + Severity: hcl2.DiagError, + Summary: "Invalid provider version constraint", + Detail: fmt.Sprintf( + "The value %q given for provider.%s is not a valid version constraint.", + p.Version, name, + ), + // TODO: include a "Subject" source reference in here, + // once the config loader is able to retain source + // location information. + }) } } - providerSet[name] = struct{}{} + providerSet[name] = true } // Check that all references to modules are valid @@ -412,9 +445,10 @@ func (c *Config) Validate() error { if _, ok := dupped[m.Id()]; !ok { dupped[m.Id()] = struct{}{} - errs = append(errs, fmt.Errorf( - "%s: module repeated multiple times", - m.Id())) + diags = diags.Append(fmt.Errorf( + "module %q: module repeated multiple times", + m.Id(), + )) } // Already seen this module, just skip it @@ -428,21 +462,23 @@ func (c *Config) Validate() error { "root": m.Source, }) if err != nil { - errs = append(errs, fmt.Errorf( - "%s: module source error: %s", - m.Id(), err)) + diags = diags.Append(fmt.Errorf( + "module %q: module source error: %s", + m.Id(), err, + )) } else if len(rc.Interpolations) > 0 { - errs = append(errs, fmt.Errorf( - "%s: module source cannot contain interpolations", - m.Id())) + diags = diags.Append(fmt.Errorf( + "module %q: module source cannot contain interpolations", + m.Id(), + )) } // Check that the name matches our regexp if !NameRegexp.Match([]byte(m.Name)) { - errs = append(errs, fmt.Errorf( - "%s: module name can only contain letters, numbers, "+ - "dashes, and underscores", - m.Id())) + diags = diags.Append(fmt.Errorf( + "module %q: module name must be a letter or underscore followed by only letters, numbers, dashes, and underscores", + m.Id(), + )) } // Check that the configuration can all be strings, lists or maps @@ -466,30 +502,47 @@ func (c *Config) Validate() error { continue } - errs = append(errs, fmt.Errorf( - "%s: variable %s must be a string, list or map value", - m.Id(), k)) + diags = diags.Append(fmt.Errorf( + "module %q: argument %s must have a string, list, or map value", + m.Id(), k, + )) } // Check for invalid count variables for _, v := range m.RawConfig.Variables { switch v.(type) { case *CountVariable: - errs = append(errs, fmt.Errorf( - "%s: count variables are only valid within resources", m.Name)) + diags = diags.Append(fmt.Errorf( + "module %q: count variables are only valid within resources", + m.Name, + )) case *SelfVariable: - errs = append(errs, fmt.Errorf( - "%s: self variables are only valid within resources", m.Name)) + diags = diags.Append(fmt.Errorf( + "module %q: self variables are only valid within resources", + m.Name, + )) } } // Update the raw configuration to only contain the string values m.RawConfig, err = NewRawConfig(raw) if err != nil { - errs = append(errs, fmt.Errorf( + diags = diags.Append(fmt.Errorf( "%s: can't initialize configuration: %s", - m.Id(), err)) + m.Id(), err, + )) } + + // check that all named providers actually exist + for _, p := range m.Providers { + if !providerSet[p] { + diags = diags.Append(fmt.Errorf( + "module %q: cannot pass non-existent provider %q", + m.Name, p, + )) + } + } + } dupped = nil @@ -503,10 +556,10 @@ func (c *Config) Validate() error { } if _, ok := modules[mv.Name]; !ok { - errs = append(errs, fmt.Errorf( + diags = diags.Append(fmt.Errorf( "%s: unknown module referenced: %s", - source, - mv.Name)) + source, mv.Name, + )) } } } @@ -519,9 +572,10 @@ func (c *Config) Validate() error { if _, ok := dupped[r.Id()]; !ok { dupped[r.Id()] = struct{}{} - errs = append(errs, fmt.Errorf( + diags = diags.Append(fmt.Errorf( "%s: resource repeated multiple times", - r.Id())) + r.Id(), + )) } } @@ -535,53 +589,42 @@ func (c *Config) Validate() error { for _, v := range r.RawCount.Variables { switch v.(type) { case *CountVariable: - errs = append(errs, fmt.Errorf( + diags = diags.Append(fmt.Errorf( "%s: resource count can't reference count variable: %s", - n, - v.FullKey())) + n, v.FullKey(), + )) case *SimpleVariable: - errs = append(errs, fmt.Errorf( + diags = diags.Append(fmt.Errorf( "%s: resource count can't reference variable: %s", - n, - v.FullKey())) + n, v.FullKey(), + )) // Good case *ModuleVariable: case *ResourceVariable: case *TerraformVariable: case *UserVariable: + case *LocalVariable: default: - errs = append(errs, fmt.Errorf( + diags = diags.Append(fmt.Errorf( "Internal error. Unknown type in count var in %s: %T", - n, v)) + n, v, + )) } } - // Interpolate with a fixed number to verify that its a number. - r.RawCount.interpolate(func(root ast.Node) (interface{}, error) { - // Execute the node but transform the AST so that it returns - // a fixed value of "5" for all interpolations. - result, err := hil.Eval( - hil.FixedValueTransform( - root, &ast.LiteralNode{Value: "5", Typex: ast.TypeString}), - nil) - if err != nil { - return "", err - } - - return result.Value, nil - }) - _, err := strconv.ParseInt(r.RawCount.Value().(string), 0, 0) - if err != nil { - errs = append(errs, fmt.Errorf( - "%s: resource count must be an integer", - n)) + if !r.RawCount.couldBeInteger() { + diags = diags.Append(fmt.Errorf( + "%s: resource count must be an integer", n, + )) } r.RawCount.init() // Validate DependsOn - errs = append(errs, c.validateDependsOn(n, r.DependsOn, resources, modules)...) + for _, err := range c.validateDependsOn(n, r.DependsOn, resources, modules) { + diags = diags.Append(err) + } // Verify provisioners for _, p := range r.Provisioners { @@ -595,9 +638,10 @@ func (c *Config) Validate() error { } if rv.Multi && rv.Index == -1 && rv.Type == r.Type && rv.Name == r.Name { - errs = append(errs, fmt.Errorf( - "%s: connection info cannot contain splat variable "+ - "referencing itself", n)) + diags = diags.Append(fmt.Errorf( + "%s: connection info cannot contain splat variable referencing itself", + n, + )) break } } @@ -609,9 +653,10 @@ func (c *Config) Validate() error { } if rv.Multi && rv.Index == -1 && rv.Type == r.Type && rv.Name == r.Name { - errs = append(errs, fmt.Errorf( - "%s: connection info cannot contain splat variable "+ - "referencing itself", n)) + diags = diags.Append(fmt.Errorf( + "%s: connection info cannot contain splat variable referencing itself", + n, + )) break } } @@ -619,21 +664,24 @@ func (c *Config) Validate() error { // Check for invalid when/onFailure values, though this should be // picked up by the loader we check here just in case. if p.When == ProvisionerWhenInvalid { - errs = append(errs, fmt.Errorf( - "%s: provisioner 'when' value is invalid", n)) + diags = diags.Append(fmt.Errorf( + "%s: provisioner 'when' value is invalid", n, + )) } if p.OnFailure == ProvisionerOnFailureInvalid { - errs = append(errs, fmt.Errorf( - "%s: provisioner 'on_failure' value is invalid", n)) + diags = diags.Append(fmt.Errorf( + "%s: provisioner 'on_failure' value is invalid", n, + )) } } // Verify ignore_changes contains valid entries for _, v := range r.Lifecycle.IgnoreChanges { if strings.Contains(v, "*") && v != "*" { - errs = append(errs, fmt.Errorf( - "%s: ignore_changes does not support using a partial string "+ - "together with a wildcard: %s", n, v)) + diags = diags.Append(fmt.Errorf( + "%s: ignore_changes does not support using a partial string together with a wildcard: %s", + n, v, + )) } } @@ -642,21 +690,24 @@ func (c *Config) Validate() error { "root": r.Lifecycle.IgnoreChanges, }) if err != nil { - errs = append(errs, fmt.Errorf( + diags = diags.Append(fmt.Errorf( "%s: lifecycle ignore_changes error: %s", - n, err)) + n, err, + )) } else if len(rc.Interpolations) > 0 { - errs = append(errs, fmt.Errorf( + diags = diags.Append(fmt.Errorf( "%s: lifecycle ignore_changes cannot contain interpolations", - n)) + n, + )) } // If it is a data source then it can't have provisioners if r.Mode == DataResourceMode { if _, ok := r.RawConfig.Raw["provisioner"]; ok { - errs = append(errs, fmt.Errorf( + diags = diags.Append(fmt.Errorf( "%s: data sources cannot have provisioners", - n)) + n, + )) } } } @@ -670,25 +721,50 @@ func (c *Config) Validate() error { id := rv.ResourceId() if _, ok := resources[id]; !ok { - errs = append(errs, fmt.Errorf( + diags = diags.Append(fmt.Errorf( "%s: unknown resource '%s' referenced in variable %s", source, id, - rv.FullKey())) + rv.FullKey(), + )) continue } } } + // Check that all locals are valid + { + found := make(map[string]struct{}) + for _, l := range c.Locals { + if _, ok := found[l.Name]; ok { + diags = diags.Append(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 { + diags = diags.Append(fmt.Errorf( + "local %s: count variables are only valid within resources", l.Name, + )) + } + } + } + } + // Check that all outputs are valid { found := make(map[string]struct{}) for _, o := range c.Outputs { // Verify the output is new if _, ok := found[o.Name]; ok { - errs = append(errs, fmt.Errorf( - "%s: duplicate output. output names must be unique.", - o.Name)) + diags = diags.Append(fmt.Errorf( + "output %q: an output of this name was already defined", + o.Name, + )) continue } found[o.Name] = struct{}{} @@ -708,9 +784,10 @@ func (c *Config) Validate() error { continue } - errs = append(errs, fmt.Errorf( - "%s: value for 'sensitive' must be boolean", - o.Name)) + diags = diags.Append(fmt.Errorf( + "output %q: value for 'sensitive' must be boolean", + o.Name, + )) continue } if k == "description" { @@ -719,27 +796,78 @@ func (c *Config) Validate() error { continue } - errs = append(errs, fmt.Errorf( - "%s: value for 'description' must be string", - o.Name)) + diags = diags.Append(fmt.Errorf( + "output %q: value for 'description' must be string", + o.Name, + )) continue } invalidKeys = append(invalidKeys, k) } if len(invalidKeys) > 0 { - errs = append(errs, fmt.Errorf( - "%s: output has invalid keys: %s", - o.Name, strings.Join(invalidKeys, ", "))) + diags = diags.Append(fmt.Errorf( + "output %q: invalid keys: %s", + o.Name, strings.Join(invalidKeys, ", "), + )) } if !valueKeyFound { - errs = append(errs, fmt.Errorf( - "%s: output is missing required 'value' key", o.Name)) + diags = diags.Append(fmt.Errorf( + "output %q: missing required 'value' argument", o.Name, + )) } for _, v := range o.RawConfig.Variables { if _, ok := v.(*CountVariable); ok { - errs = append(errs, fmt.Errorf( - "%s: count variables are only valid within resources", o.Name)) + diags = diags.Append(fmt.Errorf( + "output %q: count variables are only valid within resources", + o.Name, + )) + } + } + + // Detect a common mistake of using a "count"ed resource in + // an output value without using the splat or index form. + // Prior to 0.11 this error was silently ignored, but outputs + // now have their errors checked like all other contexts. + // + // TODO: Remove this in 0.12. + for _, v := range o.RawConfig.Variables { + rv, ok := v.(*ResourceVariable) + if !ok { + continue + } + + // If the variable seems to be treating the referenced + // resource as a singleton (no count specified) then + // we'll check to make sure it is indeed a singleton. + // It's a warning if not. + + if rv.Multi || rv.Index != 0 { + // This reference is treating the resource as a + // multi-resource, so the warning doesn't apply. + continue + } + + for _, r := range c.Resources { + if r.Id() != rv.ResourceId() { + continue + } + + // We test specifically for the raw string "1" here + // because we _do_ want to generate this warning if + // the user has provided an expression that happens + // to return 1 right now, to catch situations where + // a count might dynamically be set to something + // other than 1 and thus splat syntax is still needed + // to be safe. + if r.RawCount != nil && r.RawCount.Raw != nil && r.RawCount.Raw["count"] != "1" && rv.Field != "count" { + diags = diags.Append(tfdiags.SimpleWarning(fmt.Sprintf( + "output %q: must use splat syntax to access %s attribute %q, because it has \"count\" set; use %s.*.%s to obtain a list of the attributes across all instances", + o.Name, + r.Id(), rv.Field, + r.Id(), rv.Field, + ))) + } } } } @@ -755,17 +883,15 @@ func (c *Config) Validate() error { for _, v := range rc.Variables { if _, ok := v.(*SelfVariable); ok { - errs = append(errs, fmt.Errorf( - "%s: cannot contain self-reference %s", source, v.FullKey())) + diags = diags.Append(fmt.Errorf( + "%s: cannot contain self-reference %s", + source, v.FullKey(), + )) } } } - if len(errs) > 0 { - return &multierror.Error{Errors: errs} - } - - return nil + return diags } // InterpolatedVariables is a helper that returns a mapping of all the interpolated diff --git a/vendor/github.com/hashicorp/terraform/config/config_string.go b/vendor/github.com/hashicorp/terraform/config/config_string.go index 0b3abbc..a6933c2 100644 --- a/vendor/github.com/hashicorp/terraform/config/config_string.go +++ b/vendor/github.com/hashicorp/terraform/config/config_string.go @@ -143,6 +143,46 @@ func outputsStr(os []*Output) string { result += fmt.Sprintf(" %s: %s\n", kind, str) } } + + if o.Description != "" { + result += fmt.Sprintf(" description\n %s\n", o.Description) + } + } + + 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) diff --git a/vendor/github.com/hashicorp/terraform/config/configschema/decoder_spec.go b/vendor/github.com/hashicorp/terraform/config/configschema/decoder_spec.go new file mode 100644 index 0000000..2b1b0ca --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/config/configschema/decoder_spec.go @@ -0,0 +1,97 @@ +package configschema + +import ( + "github.com/hashicorp/hcl2/hcldec" + "github.com/zclconf/go-cty/cty" +) + +var mapLabelNames = []string{"key"} + +// DecoderSpec returns a hcldec.Spec that can be used to decode a HCL Body +// using the facilities in the hcldec package. +// +// The returned specification is guaranteed to return a value of the same type +// returned by method ImpliedType, but it may contain null or unknown values if +// any of the block attributes are defined as optional and/or computed +// respectively. +func (b *Block) DecoderSpec() hcldec.Spec { + ret := hcldec.ObjectSpec{} + if b == nil { + return ret + } + + for name, attrS := range b.Attributes { + switch { + case attrS.Computed && attrS.Optional: + // In this special case we use an unknown value as a default + // to get the intended behavior that the result is computed + // unless it has been explicitly set in config. + ret[name] = &hcldec.DefaultSpec{ + Primary: &hcldec.AttrSpec{ + Name: name, + Type: attrS.Type, + }, + Default: &hcldec.LiteralSpec{ + Value: cty.UnknownVal(attrS.Type), + }, + } + case attrS.Computed: + ret[name] = &hcldec.LiteralSpec{ + Value: cty.UnknownVal(attrS.Type), + } + default: + ret[name] = &hcldec.AttrSpec{ + Name: name, + Type: attrS.Type, + Required: attrS.Required, + } + } + } + + for name, blockS := range b.BlockTypes { + if _, exists := ret[name]; exists { + // This indicates an invalid schema, since it's not valid to + // define both an attribute and a block type of the same name. + // However, we don't raise this here since it's checked by + // InternalValidate. + continue + } + + childSpec := blockS.Block.DecoderSpec() + + switch blockS.Nesting { + case NestingSingle: + ret[name] = &hcldec.BlockSpec{ + TypeName: name, + Nested: childSpec, + Required: blockS.MinItems == 1 && blockS.MaxItems >= 1, + } + case NestingList: + ret[name] = &hcldec.BlockListSpec{ + TypeName: name, + Nested: childSpec, + MinItems: blockS.MinItems, + MaxItems: blockS.MaxItems, + } + case NestingSet: + ret[name] = &hcldec.BlockSetSpec{ + TypeName: name, + Nested: childSpec, + MinItems: blockS.MinItems, + MaxItems: blockS.MaxItems, + } + case NestingMap: + ret[name] = &hcldec.BlockMapSpec{ + TypeName: name, + Nested: childSpec, + LabelNames: mapLabelNames, + } + default: + // Invalid nesting type is just ignored. It's checked by + // InternalValidate. + continue + } + } + + return ret +} diff --git a/vendor/github.com/hashicorp/terraform/config/configschema/doc.go b/vendor/github.com/hashicorp/terraform/config/configschema/doc.go new file mode 100644 index 0000000..caf8d73 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/config/configschema/doc.go @@ -0,0 +1,14 @@ +// Package configschema contains types for describing the expected structure +// of a configuration block whose shape is not known until runtime. +// +// For example, this is used to describe the expected contents of a resource +// configuration block, which is defined by the corresponding provider plugin +// and thus not compiled into Terraform core. +// +// A configschema primarily describes the shape of configuration, but it is +// also suitable for use with other structures derived from the configuration, +// such as the cached state of a resource or a resource diff. +// +// This package should not be confused with the package helper/schema, which +// is the higher-level helper library used to implement providers themselves. +package configschema diff --git a/vendor/github.com/hashicorp/terraform/config/configschema/implied_type.go b/vendor/github.com/hashicorp/terraform/config/configschema/implied_type.go new file mode 100644 index 0000000..67324eb --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/config/configschema/implied_type.go @@ -0,0 +1,21 @@ +package configschema + +import ( + "github.com/hashicorp/hcl2/hcldec" + "github.com/zclconf/go-cty/cty" +) + +// ImpliedType returns the cty.Type that would result from decoding a +// configuration block using the receiving block schema. +// +// ImpliedType always returns a result, even if the given schema is +// inconsistent. Code that creates configschema.Block objects should be +// tested using the InternalValidate method to detect any inconsistencies +// that would cause this method to fall back on defaults and assumptions. +func (b *Block) ImpliedType() cty.Type { + if b == nil { + return cty.EmptyObject + } + + return hcldec.ImpliedType(b.DecoderSpec()) +} diff --git a/vendor/github.com/hashicorp/terraform/config/configschema/internal_validate.go b/vendor/github.com/hashicorp/terraform/config/configschema/internal_validate.go new file mode 100644 index 0000000..33cbe88 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/config/configschema/internal_validate.go @@ -0,0 +1,92 @@ +package configschema + +import ( + "fmt" + "regexp" + + "github.com/zclconf/go-cty/cty" + + multierror "github.com/hashicorp/go-multierror" +) + +var validName = regexp.MustCompile(`^[a-z0-9_]+$`) + +// InternalValidate returns an error if the receiving block and its child +// schema definitions have any consistencies with the documented rules for +// valid schema. +// +// This is intended to be used within unit tests to detect when a given +// schema is invalid. +func (b *Block) InternalValidate() error { + if b == nil { + return fmt.Errorf("top-level block schema is nil") + } + return b.internalValidate("", nil) + +} + +func (b *Block) internalValidate(prefix string, err error) error { + for name, attrS := range b.Attributes { + if attrS == nil { + err = multierror.Append(err, fmt.Errorf("%s%s: attribute schema is nil", prefix, name)) + continue + } + if !validName.MatchString(name) { + err = multierror.Append(err, fmt.Errorf("%s%s: name may contain only lowercase letters, digits and underscores", prefix, name)) + } + if attrS.Optional == false && attrS.Required == false && attrS.Computed == false { + err = multierror.Append(err, fmt.Errorf("%s%s: must set Optional, Required or Computed", prefix, name)) + } + if attrS.Optional && attrS.Required { + err = multierror.Append(err, fmt.Errorf("%s%s: cannot set both Optional and Required", prefix, name)) + } + if attrS.Computed && attrS.Required { + err = multierror.Append(err, fmt.Errorf("%s%s: cannot set both Computed and Required", prefix, name)) + } + if attrS.Type == cty.NilType { + err = multierror.Append(err, fmt.Errorf("%s%s: Type must be set to something other than cty.NilType", prefix, name)) + } + } + + for name, blockS := range b.BlockTypes { + if blockS == nil { + err = multierror.Append(err, fmt.Errorf("%s%s: block schema is nil", prefix, name)) + continue + } + + if _, isAttr := b.Attributes[name]; isAttr { + err = multierror.Append(err, fmt.Errorf("%s%s: name defined as both attribute and child block type", prefix, name)) + } else if !validName.MatchString(name) { + err = multierror.Append(err, fmt.Errorf("%s%s: name may contain only lowercase letters, digits and underscores", prefix, name)) + } + + if blockS.MinItems < 0 || blockS.MaxItems < 0 { + err = multierror.Append(err, fmt.Errorf("%s%s: MinItems and MaxItems must both be greater than zero", prefix, name)) + } + + switch blockS.Nesting { + case NestingSingle: + switch { + case blockS.MinItems != blockS.MaxItems: + err = multierror.Append(err, fmt.Errorf("%s%s: MinItems and MaxItems must match in NestingSingle mode", prefix, name)) + case blockS.MinItems < 0 || blockS.MinItems > 1: + err = multierror.Append(err, fmt.Errorf("%s%s: MinItems and MaxItems must be set to either 0 or 1 in NestingSingle mode", prefix, name)) + } + case NestingList, NestingSet: + if blockS.MinItems > blockS.MaxItems && blockS.MaxItems != 0 { + err = multierror.Append(err, fmt.Errorf("%s%s: MinItems must be less than or equal to MaxItems in %s mode", prefix, name, blockS.Nesting)) + } + case NestingMap: + if blockS.MinItems != 0 || blockS.MaxItems != 0 { + err = multierror.Append(err, fmt.Errorf("%s%s: MinItems and MaxItems must both be 0 in NestingMap mode", prefix, name)) + } + default: + err = multierror.Append(err, fmt.Errorf("%s%s: invalid nesting mode %s", prefix, name, blockS.Nesting)) + } + + subPrefix := prefix + name + "." + err = blockS.Block.internalValidate(subPrefix, err) + } + + return err +} diff --git a/vendor/github.com/hashicorp/terraform/config/configschema/nestingmode_string.go b/vendor/github.com/hashicorp/terraform/config/configschema/nestingmode_string.go new file mode 100644 index 0000000..6cb9313 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/config/configschema/nestingmode_string.go @@ -0,0 +1,16 @@ +// Code generated by "stringer -type=NestingMode"; DO NOT EDIT. + +package configschema + +import "strconv" + +const _NestingMode_name = "nestingModeInvalidNestingSingleNestingListNestingSetNestingMap" + +var _NestingMode_index = [...]uint8{0, 18, 31, 42, 52, 62} + +func (i NestingMode) String() string { + if i < 0 || i >= NestingMode(len(_NestingMode_index)-1) { + return "NestingMode(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _NestingMode_name[_NestingMode_index[i]:_NestingMode_index[i+1]] +} diff --git a/vendor/github.com/hashicorp/terraform/config/configschema/schema.go b/vendor/github.com/hashicorp/terraform/config/configschema/schema.go new file mode 100644 index 0000000..9a8ee55 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/config/configschema/schema.go @@ -0,0 +1,107 @@ +package configschema + +import ( + "github.com/zclconf/go-cty/cty" +) + +// Block represents a configuration block. +// +// "Block" here is a logical grouping construct, though it happens to map +// directly onto the physical block syntax of Terraform's native configuration +// syntax. It may be a more a matter of convention in other syntaxes, such as +// JSON. +// +// When converted to a value, a Block always becomes an instance of an object +// type derived from its defined attributes and nested blocks +type Block struct { + // Attributes describes any attributes that may appear directly inside + // the block. + Attributes map[string]*Attribute + + // BlockTypes describes any nested block types that may appear directly + // inside the block. + BlockTypes map[string]*NestedBlock +} + +// Attribute represents a configuration attribute, within a block. +type Attribute struct { + // Type is a type specification that the attribute's value must conform to. + Type cty.Type + + // Required, if set to true, specifies that an omitted or null value is + // not permitted. + Required bool + + // Optional, if set to true, specifies that an omitted or null value is + // permitted. This field conflicts with Required. + Optional bool + + // Computed, if set to true, specifies that the value comes from the + // provider rather than from configuration. If combined with Optional, + // then the config may optionally provide an overridden value. + Computed bool + + // Sensitive, if set to true, indicates that an attribute may contain + // sensitive information. + // + // At present nothing is done with this information, but callers are + // encouraged to set it where appropriate so that it may be used in the + // future to help Terraform mask sensitive information. (Terraform + // currently achieves this in a limited sense via other mechanisms.) + Sensitive bool +} + +// NestedBlock represents the embedding of one block within another. +type NestedBlock struct { + // Block is the description of the block that's nested. + Block + + // Nesting provides the nesting mode for the child block, which determines + // how many instances of the block are allowed, how many labels it expects, + // and how the resulting data will be converted into a data structure. + Nesting NestingMode + + // MinItems and MaxItems set, for the NestingList and NestingSet nesting + // modes, lower and upper limits on the number of child blocks allowed + // of the given type. If both are left at zero, no limit is applied. + // + // As a special case, both values can be set to 1 for NestingSingle in + // order to indicate that a particular single block is required. + // + // These fields are ignored for other nesting modes and must both be left + // at zero. + MinItems, MaxItems int +} + +// NestingMode is an enumeration of modes for nesting blocks inside other +// blocks. +type NestingMode int + +//go:generate stringer -type=NestingMode + +const ( + nestingModeInvalid NestingMode = iota + + // NestingSingle indicates that only a single instance of a given + // block type is permitted, with no labels, and its content should be + // provided directly as an object value. + NestingSingle + + // NestingList indicates that multiple blocks of the given type are + // permitted, with no labels, and that their corresponding objects should + // be provided in a list. + NestingList + + // NestingSet indicates that multiple blocks of the given type are + // permitted, with no labels, and that their corresponding objects should + // be provided in a set. + NestingSet + + // NestingMap indicates that multiple blocks of the given type are + // permitted, each with a single label, and that their corresponding + // objects should be provided in a map whose keys are the labels. + // + // It's an error, therefore, to use the same label value on multiple + // blocks. + NestingMap +) diff --git a/vendor/github.com/hashicorp/terraform/config/hcl2_shim_util.go b/vendor/github.com/hashicorp/terraform/config/hcl2_shim_util.go new file mode 100644 index 0000000..207d105 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/config/hcl2_shim_util.go @@ -0,0 +1,134 @@ +package config + +import ( + "fmt" + + "github.com/zclconf/go-cty/cty/function/stdlib" + + "github.com/hashicorp/hil/ast" + "github.com/hashicorp/terraform/config/hcl2shim" + + hcl2 "github.com/hashicorp/hcl2/hcl" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" + "github.com/zclconf/go-cty/cty/function" +) + +// --------------------------------------------------------------------------- +// This file contains some helper functions that are used to shim between +// HCL2 concepts and HCL/HIL concepts, to help us mostly preserve the existing +// public API that was built around HCL/HIL-oriented approaches. +// --------------------------------------------------------------------------- + +func hcl2InterpolationFuncs() map[string]function.Function { + hcl2Funcs := map[string]function.Function{} + + for name, hilFunc := range Funcs() { + hcl2Funcs[name] = hcl2InterpolationFuncShim(hilFunc) + } + + // Some functions in the old world are dealt with inside langEvalConfig + // due to their legacy reliance on direct access to the symbol table. + // Since 0.7 they don't actually need it anymore and just ignore it, + // so we're cheating a bit here and exploiting that detail by passing nil. + hcl2Funcs["lookup"] = hcl2InterpolationFuncShim(interpolationFuncLookup(nil)) + hcl2Funcs["keys"] = hcl2InterpolationFuncShim(interpolationFuncKeys(nil)) + hcl2Funcs["values"] = hcl2InterpolationFuncShim(interpolationFuncValues(nil)) + + // As a bonus, we'll provide the JSON-handling functions from the cty + // function library since its "jsonencode" is more complete (doesn't force + // weird type conversions) and HIL's type system can't represent + // "jsondecode" at all. The result of jsondecode will eventually be forced + // to conform to the HIL type system on exit into the rest of Terraform due + // to our shimming right now, but it should be usable for decoding _within_ + // an expression. + hcl2Funcs["jsonencode"] = stdlib.JSONEncodeFunc + hcl2Funcs["jsondecode"] = stdlib.JSONDecodeFunc + + return hcl2Funcs +} + +func hcl2InterpolationFuncShim(hilFunc ast.Function) function.Function { + spec := &function.Spec{} + + for i, hilArgType := range hilFunc.ArgTypes { + spec.Params = append(spec.Params, function.Parameter{ + Type: hcl2shim.HCL2TypeForHILType(hilArgType), + Name: fmt.Sprintf("arg%d", i+1), // HIL args don't have names, so we'll fudge it + }) + } + + if hilFunc.Variadic { + spec.VarParam = &function.Parameter{ + Type: hcl2shim.HCL2TypeForHILType(hilFunc.VariadicType), + Name: "varargs", // HIL args don't have names, so we'll fudge it + } + } + + spec.Type = func(args []cty.Value) (cty.Type, error) { + return hcl2shim.HCL2TypeForHILType(hilFunc.ReturnType), nil + } + spec.Impl = func(args []cty.Value, retType cty.Type) (cty.Value, error) { + hilArgs := make([]interface{}, len(args)) + for i, arg := range args { + hilV := hcl2shim.HILVariableFromHCL2Value(arg) + + // Although the cty function system does automatic type conversions + // to match the argument types, cty doesn't distinguish int and + // float and so we may need to adjust here to ensure that the + // wrapped function gets exactly the Go type it was expecting. + var wantType ast.Type + if i < len(hilFunc.ArgTypes) { + wantType = hilFunc.ArgTypes[i] + } else { + wantType = hilFunc.VariadicType + } + switch { + case hilV.Type == ast.TypeInt && wantType == ast.TypeFloat: + hilV.Type = wantType + hilV.Value = float64(hilV.Value.(int)) + case hilV.Type == ast.TypeFloat && wantType == ast.TypeInt: + hilV.Type = wantType + hilV.Value = int(hilV.Value.(float64)) + } + + // HIL functions actually expect to have the outermost variable + // "peeled" but any nested values (in lists or maps) will + // still have their ast.Variable wrapping. + hilArgs[i] = hilV.Value + } + + hilResult, err := hilFunc.Callback(hilArgs) + if err != nil { + return cty.DynamicVal, err + } + + // Just as on the way in, we get back a partially-peeled ast.Variable + // which we need to re-wrap in order to convert it back into what + // we're calling a "config value". + rv := hcl2shim.HCL2ValueFromHILVariable(ast.Variable{ + Type: hilFunc.ReturnType, + Value: hilResult, + }) + + return convert.Convert(rv, retType) // if result is unknown we'll force the correct type here + } + return function.New(spec) +} + +func hcl2EvalWithUnknownVars(expr hcl2.Expression) (cty.Value, hcl2.Diagnostics) { + trs := expr.Variables() + vars := map[string]cty.Value{} + val := cty.DynamicVal + + for _, tr := range trs { + name := tr.RootName() + vars[name] = val + } + + ctx := &hcl2.EvalContext{ + Variables: vars, + Functions: hcl2InterpolationFuncs(), + } + return expr.Value(ctx) +} diff --git a/vendor/github.com/hashicorp/terraform/config/hcl2shim/single_attr_body.go b/vendor/github.com/hashicorp/terraform/config/hcl2shim/single_attr_body.go new file mode 100644 index 0000000..19651c8 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/config/hcl2shim/single_attr_body.go @@ -0,0 +1,85 @@ +package hcl2shim + +import ( + "fmt" + + hcl2 "github.com/hashicorp/hcl2/hcl" +) + +// SingleAttrBody is a weird implementation of hcl2.Body that acts as if +// it has a single attribute whose value is the given expression. +// +// This is used to shim Resource.RawCount and Output.RawConfig to behave +// more like they do in the old HCL loader. +type SingleAttrBody struct { + Name string + Expr hcl2.Expression +} + +var _ hcl2.Body = SingleAttrBody{} + +func (b SingleAttrBody) Content(schema *hcl2.BodySchema) (*hcl2.BodyContent, hcl2.Diagnostics) { + content, all, diags := b.content(schema) + if !all { + // This should never happen because this body implementation should only + // be used by code that is aware that it's using a single-attr body. + diags = append(diags, &hcl2.Diagnostic{ + Severity: hcl2.DiagError, + Summary: "Invalid attribute", + Detail: fmt.Sprintf("The correct attribute name is %q.", b.Name), + Subject: b.Expr.Range().Ptr(), + }) + } + return content, diags +} + +func (b SingleAttrBody) PartialContent(schema *hcl2.BodySchema) (*hcl2.BodyContent, hcl2.Body, hcl2.Diagnostics) { + content, all, diags := b.content(schema) + var remain hcl2.Body + if all { + // If the request matched the one attribute we represent, then the + // remaining body is empty. + remain = hcl2.EmptyBody() + } else { + remain = b + } + return content, remain, diags +} + +func (b SingleAttrBody) content(schema *hcl2.BodySchema) (*hcl2.BodyContent, bool, hcl2.Diagnostics) { + ret := &hcl2.BodyContent{} + all := false + var diags hcl2.Diagnostics + + for _, attrS := range schema.Attributes { + if attrS.Name == b.Name { + attrs, _ := b.JustAttributes() + ret.Attributes = attrs + all = true + } else if attrS.Required { + diags = append(diags, &hcl2.Diagnostic{ + Severity: hcl2.DiagError, + Summary: "Missing attribute", + Detail: fmt.Sprintf("The attribute %q is required.", attrS.Name), + Subject: b.Expr.Range().Ptr(), + }) + } + } + + return ret, all, diags +} + +func (b SingleAttrBody) JustAttributes() (hcl2.Attributes, hcl2.Diagnostics) { + return hcl2.Attributes{ + b.Name: { + Expr: b.Expr, + Name: b.Name, + NameRange: b.Expr.Range(), + Range: b.Expr.Range(), + }, + }, nil +} + +func (b SingleAttrBody) MissingItemRange() hcl2.Range { + return b.Expr.Range() +} diff --git a/vendor/github.com/hashicorp/terraform/config/hcl2shim/values.go b/vendor/github.com/hashicorp/terraform/config/hcl2shim/values.go new file mode 100644 index 0000000..0b697a5 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/config/hcl2shim/values.go @@ -0,0 +1,246 @@ +package hcl2shim + +import ( + "fmt" + "math/big" + + "github.com/hashicorp/hil/ast" + "github.com/zclconf/go-cty/cty" +) + +// UnknownVariableValue is a sentinel value that can be used +// to denote that the value of a variable is unknown at this time. +// RawConfig uses this information to build up data about +// unknown keys. +const UnknownVariableValue = "74D93920-ED26-11E3-AC10-0800200C9A66" + +// ConfigValueFromHCL2 converts a value from HCL2 (really, from the cty dynamic +// types library that HCL2 uses) to a value type that matches what would've +// been produced from the HCL-based interpolator for an equivalent structure. +// +// This function will transform a cty null value into a Go nil value, which +// isn't a possible outcome of the HCL/HIL-based decoder and so callers may +// need to detect and reject any null values. +func ConfigValueFromHCL2(v cty.Value) interface{} { + if !v.IsKnown() { + return UnknownVariableValue + } + if v.IsNull() { + return nil + } + + switch v.Type() { + case cty.Bool: + return v.True() // like HCL.BOOL + case cty.String: + return v.AsString() // like HCL token.STRING or token.HEREDOC + case cty.Number: + // We can't match HCL _exactly_ here because it distinguishes between + // int and float values, but we'll get as close as we can by using + // an int if the number is exactly representable, and a float if not. + // The conversion to float will force precision to that of a float64, + // which is potentially losing information from the specific number + // given, but no worse than what HCL would've done in its own conversion + // to float. + + f := v.AsBigFloat() + if i, acc := f.Int64(); acc == big.Exact { + // if we're on a 32-bit system and the number is too big for 32-bit + // int then we'll fall through here and use a float64. + const MaxInt = int(^uint(0) >> 1) + const MinInt = -MaxInt - 1 + if i <= int64(MaxInt) && i >= int64(MinInt) { + return int(i) // Like HCL token.NUMBER + } + } + + f64, _ := f.Float64() + return f64 // like HCL token.FLOAT + } + + if v.Type().IsListType() || v.Type().IsSetType() || v.Type().IsTupleType() { + l := make([]interface{}, 0, v.LengthInt()) + it := v.ElementIterator() + for it.Next() { + _, ev := it.Element() + l = append(l, ConfigValueFromHCL2(ev)) + } + return l + } + + if v.Type().IsMapType() || v.Type().IsObjectType() { + l := make(map[string]interface{}) + it := v.ElementIterator() + for it.Next() { + ek, ev := it.Element() + l[ek.AsString()] = ConfigValueFromHCL2(ev) + } + return l + } + + // If we fall out here then we have some weird type that we haven't + // accounted for. This should never happen unless the caller is using + // capsule types, and we don't currently have any such types defined. + panic(fmt.Errorf("can't convert %#v to config value", v)) +} + +// HCL2ValueFromConfigValue is the opposite of configValueFromHCL2: it takes +// a value as would be returned from the old interpolator and turns it into +// a cty.Value so it can be used within, for example, an HCL2 EvalContext. +func HCL2ValueFromConfigValue(v interface{}) cty.Value { + if v == nil { + return cty.NullVal(cty.DynamicPseudoType) + } + if v == UnknownVariableValue { + return cty.DynamicVal + } + + switch tv := v.(type) { + case bool: + return cty.BoolVal(tv) + case string: + return cty.StringVal(tv) + case int: + return cty.NumberIntVal(int64(tv)) + case float64: + return cty.NumberFloatVal(tv) + case []interface{}: + vals := make([]cty.Value, len(tv)) + for i, ev := range tv { + vals[i] = HCL2ValueFromConfigValue(ev) + } + return cty.TupleVal(vals) + case map[string]interface{}: + vals := map[string]cty.Value{} + for k, ev := range tv { + vals[k] = HCL2ValueFromConfigValue(ev) + } + return cty.ObjectVal(vals) + default: + // HCL/HIL should never generate anything that isn't caught by + // the above, so if we get here something has gone very wrong. + panic(fmt.Errorf("can't convert %#v to cty.Value", v)) + } +} + +func HILVariableFromHCL2Value(v cty.Value) ast.Variable { + if v.IsNull() { + // Caller should guarantee/check this before calling + panic("Null values cannot be represented in HIL") + } + if !v.IsKnown() { + return ast.Variable{ + Type: ast.TypeUnknown, + Value: UnknownVariableValue, + } + } + + switch v.Type() { + case cty.Bool: + return ast.Variable{ + Type: ast.TypeBool, + Value: v.True(), + } + case cty.Number: + v := ConfigValueFromHCL2(v) + switch tv := v.(type) { + case int: + return ast.Variable{ + Type: ast.TypeInt, + Value: tv, + } + case float64: + return ast.Variable{ + Type: ast.TypeFloat, + Value: tv, + } + default: + // should never happen + panic("invalid return value for configValueFromHCL2") + } + case cty.String: + return ast.Variable{ + Type: ast.TypeString, + Value: v.AsString(), + } + } + + if v.Type().IsListType() || v.Type().IsSetType() || v.Type().IsTupleType() { + l := make([]ast.Variable, 0, v.LengthInt()) + it := v.ElementIterator() + for it.Next() { + _, ev := it.Element() + l = append(l, HILVariableFromHCL2Value(ev)) + } + // If we were given a tuple then this could actually produce an invalid + // list with non-homogenous types, which we expect to be caught inside + // HIL just like a user-supplied non-homogenous list would be. + return ast.Variable{ + Type: ast.TypeList, + Value: l, + } + } + + if v.Type().IsMapType() || v.Type().IsObjectType() { + l := make(map[string]ast.Variable) + it := v.ElementIterator() + for it.Next() { + ek, ev := it.Element() + l[ek.AsString()] = HILVariableFromHCL2Value(ev) + } + // If we were given an object then this could actually produce an invalid + // map with non-homogenous types, which we expect to be caught inside + // HIL just like a user-supplied non-homogenous map would be. + return ast.Variable{ + Type: ast.TypeMap, + Value: l, + } + } + + // If we fall out here then we have some weird type that we haven't + // accounted for. This should never happen unless the caller is using + // capsule types, and we don't currently have any such types defined. + panic(fmt.Errorf("can't convert %#v to HIL variable", v)) +} + +func HCL2ValueFromHILVariable(v ast.Variable) cty.Value { + switch v.Type { + case ast.TypeList: + vals := make([]cty.Value, len(v.Value.([]ast.Variable))) + for i, ev := range v.Value.([]ast.Variable) { + vals[i] = HCL2ValueFromHILVariable(ev) + } + return cty.TupleVal(vals) + case ast.TypeMap: + vals := make(map[string]cty.Value, len(v.Value.(map[string]ast.Variable))) + for k, ev := range v.Value.(map[string]ast.Variable) { + vals[k] = HCL2ValueFromHILVariable(ev) + } + return cty.ObjectVal(vals) + default: + return HCL2ValueFromConfigValue(v.Value) + } +} + +func HCL2TypeForHILType(hilType ast.Type) cty.Type { + switch hilType { + case ast.TypeAny: + return cty.DynamicPseudoType + case ast.TypeUnknown: + return cty.DynamicPseudoType + case ast.TypeBool: + return cty.Bool + case ast.TypeInt: + return cty.Number + case ast.TypeFloat: + return cty.Number + case ast.TypeString: + return cty.String + case ast.TypeList: + return cty.List(cty.DynamicPseudoType) + case ast.TypeMap: + return cty.Map(cty.DynamicPseudoType) + default: + return cty.NilType // equilvalent to ast.TypeInvalid + } +} diff --git a/vendor/github.com/hashicorp/terraform/config/import_tree.go b/vendor/github.com/hashicorp/terraform/config/import_tree.go index 37ec11a..08cbc77 100644 --- a/vendor/github.com/hashicorp/terraform/config/import_tree.go +++ b/vendor/github.com/hashicorp/terraform/config/import_tree.go @@ -1,8 +1,12 @@ package config import ( + "bufio" "fmt" "io" + "os" + + "github.com/hashicorp/errwrap" ) // configurable is an interface that must be implemented by any configuration @@ -27,15 +31,52 @@ type importTree struct { // imports. type fileLoaderFunc func(path string) (configurable, []string, error) +// Set this to a non-empty value at link time to enable the HCL2 experiment. +// This is not currently enabled for release builds. +// +// For example: +// go install -ldflags="-X github.com/hashicorp/terraform/config.enableHCL2Experiment=true" github.com/hashicorp/terraform +var enableHCL2Experiment = "" + // loadTree takes a single file and loads the entire importTree for that // file. This function detects what kind of configuration file it is an // executes the proper fileLoaderFunc. func loadTree(root string) (*importTree, error) { var f fileLoaderFunc - switch ext(root) { - case ".tf", ".tf.json": - f = loadFileHcl - default: + + // HCL2 experiment is currently activated at build time via the linker. + // See the comment on this variable for more information. + if enableHCL2Experiment == "" { + // Main-line behavior: always use the original HCL parser + switch ext(root) { + case ".tf", ".tf.json": + f = loadFileHcl + default: + } + } else { + // Experimental behavior: use the HCL2 parser if the opt-in comment + // is present. + switch ext(root) { + case ".tf": + // We need to sniff the file for the opt-in comment line to decide + // if the file is participating in the HCL2 experiment. + cf, err := os.Open(root) + if err != nil { + return nil, err + } + sc := bufio.NewScanner(cf) + for sc.Scan() { + if sc.Text() == "#terraform:hcl2" { + f = globalHCL2Loader.loadFile + } + } + if f == nil { + f = loadFileHcl + } + case ".tf.json": + f = loadFileHcl + default: + } } if f == nil { @@ -86,10 +127,7 @@ func (t *importTree) Close() error { func (t *importTree) ConfigTree() (*configTree, error) { config, err := t.Raw.Config() if err != nil { - return nil, fmt.Errorf( - "Error loading %s: %s", - t.Path, - err) + return nil, errwrap.Wrapf(fmt.Sprintf("Error loading %s: {{err}}", t.Path), err) } // Build our result diff --git a/vendor/github.com/hashicorp/terraform/config/interpolate.go b/vendor/github.com/hashicorp/terraform/config/interpolate.go index bbb3555..599e5ec 100644 --- a/vendor/github.com/hashicorp/terraform/config/interpolate.go +++ b/vendor/github.com/hashicorp/terraform/config/interpolate.go @@ -5,6 +5,8 @@ import ( "strconv" "strings" + "github.com/hashicorp/terraform/tfdiags" + "github.com/hashicorp/hil/ast" ) @@ -14,6 +16,21 @@ import ( // variables can come from: user variables, resources, etc. type InterpolatedVariable interface { FullKey() string + SourceRange() tfdiags.SourceRange +} + +// varRange can be embedded into an InterpolatedVariable implementation to +// implement the SourceRange method. +type varRange struct { + rng tfdiags.SourceRange +} + +func (r varRange) SourceRange() tfdiags.SourceRange { + return r.rng +} + +func makeVarRange(rng tfdiags.SourceRange) varRange { + return varRange{rng} } // CountVariable is a variable for referencing information about @@ -21,6 +38,7 @@ type InterpolatedVariable interface { type CountVariable struct { Type CountValueType key string + varRange } // CountValueType is the type of the count variable that is referenced. @@ -37,6 +55,7 @@ type ModuleVariable struct { Name string Field string key string + varRange } // A PathVariable is a variable that references path information about the @@ -44,6 +63,7 @@ type ModuleVariable struct { type PathVariable struct { Type PathValueType key string + varRange } type PathValueType byte @@ -67,6 +87,7 @@ type ResourceVariable struct { Index int // Index for multi-variable: aws_instance.foo.1.id == 1 key string + varRange } // SelfVariable is a variable that is referencing the same resource @@ -75,6 +96,7 @@ type SelfVariable struct { Field string key string + varRange } // SimpleVariable is an unprefixed variable, which can show up when users have @@ -82,6 +104,7 @@ type SelfVariable struct { // internally. The template_file resource is an example of this. type SimpleVariable struct { Key string + varRange } // TerraformVariable is a "terraform."-prefixed variable used to access @@ -89,6 +112,7 @@ type SimpleVariable struct { type TerraformVariable struct { Field string key string + varRange } // A UserVariable is a variable that is referencing a user variable @@ -99,6 +123,14 @@ type UserVariable struct { Elem string key string + varRange +} + +// 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 + varRange } func NewInterpolatedVariable(v string) (InterpolatedVariable, error) { @@ -112,6 +144,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, '.') { @@ -276,7 +310,7 @@ func (v *SelfVariable) GoString() string { } func NewSimpleVariable(key string) (*SimpleVariable, error) { - return &SimpleVariable{key}, nil + return &SimpleVariable{Key: key}, nil } func (v *SimpleVariable) FullKey() string { @@ -331,6 +365,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) { diff --git a/vendor/github.com/hashicorp/terraform/config/interpolate_funcs.go b/vendor/github.com/hashicorp/terraform/config/interpolate_funcs.go index a298cf2..421edb0 100644 --- a/vendor/github.com/hashicorp/terraform/config/interpolate_funcs.go +++ b/vendor/github.com/hashicorp/terraform/config/interpolate_funcs.go @@ -1,17 +1,23 @@ package config import ( + "bytes" + "compress/gzip" "crypto/md5" + "crypto/rsa" "crypto/sha1" "crypto/sha256" "crypto/sha512" + "crypto/x509" "encoding/base64" "encoding/hex" "encoding/json" + "encoding/pem" "fmt" "io/ioutil" "math" "net" + "net/url" "path/filepath" "regexp" "sort" @@ -55,59 +61,74 @@ func listVariableValueToStringSlice(values []ast.Variable) ([]string, error) { // Funcs is the mapping of built-in functions for configuration. func Funcs() map[string]ast.Function { return map[string]ast.Function{ - "basename": interpolationFuncBasename(), - "base64decode": interpolationFuncBase64Decode(), - "base64encode": interpolationFuncBase64Encode(), - "base64sha256": interpolationFuncBase64Sha256(), - "base64sha512": interpolationFuncBase64Sha512(), - "bcrypt": interpolationFuncBcrypt(), - "ceil": interpolationFuncCeil(), - "chomp": interpolationFuncChomp(), - "cidrhost": interpolationFuncCidrHost(), - "cidrnetmask": interpolationFuncCidrNetmask(), - "cidrsubnet": interpolationFuncCidrSubnet(), - "coalesce": interpolationFuncCoalesce(), - "coalescelist": interpolationFuncCoalesceList(), - "compact": interpolationFuncCompact(), - "concat": interpolationFuncConcat(), - "contains": interpolationFuncContains(), - "dirname": interpolationFuncDirname(), - "distinct": interpolationFuncDistinct(), - "element": interpolationFuncElement(), - "file": interpolationFuncFile(), - "matchkeys": interpolationFuncMatchKeys(), - "floor": interpolationFuncFloor(), - "format": interpolationFuncFormat(), - "formatlist": interpolationFuncFormatList(), - "index": interpolationFuncIndex(), - "join": interpolationFuncJoin(), - "jsonencode": interpolationFuncJSONEncode(), - "length": interpolationFuncLength(), - "list": interpolationFuncList(), - "log": interpolationFuncLog(), - "lower": interpolationFuncLower(), - "map": interpolationFuncMap(), - "max": interpolationFuncMax(), - "md5": interpolationFuncMd5(), - "merge": interpolationFuncMerge(), - "min": interpolationFuncMin(), - "pathexpand": interpolationFuncPathExpand(), - "pow": interpolationFuncPow(), - "uuid": interpolationFuncUUID(), - "replace": interpolationFuncReplace(), - "sha1": interpolationFuncSha1(), - "sha256": interpolationFuncSha256(), - "sha512": interpolationFuncSha512(), - "signum": interpolationFuncSignum(), - "slice": interpolationFuncSlice(), - "sort": interpolationFuncSort(), - "split": interpolationFuncSplit(), - "substr": interpolationFuncSubstr(), - "timestamp": interpolationFuncTimestamp(), - "title": interpolationFuncTitle(), - "trimspace": interpolationFuncTrimSpace(), - "upper": interpolationFuncUpper(), - "zipmap": interpolationFuncZipMap(), + "abs": interpolationFuncAbs(), + "basename": interpolationFuncBasename(), + "base64decode": interpolationFuncBase64Decode(), + "base64encode": interpolationFuncBase64Encode(), + "base64gzip": interpolationFuncBase64Gzip(), + "base64sha256": interpolationFuncBase64Sha256(), + "base64sha512": interpolationFuncBase64Sha512(), + "bcrypt": interpolationFuncBcrypt(), + "ceil": interpolationFuncCeil(), + "chomp": interpolationFuncChomp(), + "cidrhost": interpolationFuncCidrHost(), + "cidrnetmask": interpolationFuncCidrNetmask(), + "cidrsubnet": interpolationFuncCidrSubnet(), + "coalesce": interpolationFuncCoalesce(), + "coalescelist": interpolationFuncCoalesceList(), + "compact": interpolationFuncCompact(), + "concat": interpolationFuncConcat(), + "contains": interpolationFuncContains(), + "dirname": interpolationFuncDirname(), + "distinct": interpolationFuncDistinct(), + "element": interpolationFuncElement(), + "chunklist": interpolationFuncChunklist(), + "file": interpolationFuncFile(), + "filebase64sha256": interpolationFuncMakeFileHash(interpolationFuncBase64Sha256()), + "filebase64sha512": interpolationFuncMakeFileHash(interpolationFuncBase64Sha512()), + "filemd5": interpolationFuncMakeFileHash(interpolationFuncMd5()), + "filesha1": interpolationFuncMakeFileHash(interpolationFuncSha1()), + "filesha256": interpolationFuncMakeFileHash(interpolationFuncSha256()), + "filesha512": interpolationFuncMakeFileHash(interpolationFuncSha512()), + "matchkeys": interpolationFuncMatchKeys(), + "flatten": interpolationFuncFlatten(), + "floor": interpolationFuncFloor(), + "format": interpolationFuncFormat(), + "formatlist": interpolationFuncFormatList(), + "indent": interpolationFuncIndent(), + "index": interpolationFuncIndex(), + "join": interpolationFuncJoin(), + "jsonencode": interpolationFuncJSONEncode(), + "length": interpolationFuncLength(), + "list": interpolationFuncList(), + "log": interpolationFuncLog(), + "lower": interpolationFuncLower(), + "map": interpolationFuncMap(), + "max": interpolationFuncMax(), + "md5": interpolationFuncMd5(), + "merge": interpolationFuncMerge(), + "min": interpolationFuncMin(), + "pathexpand": interpolationFuncPathExpand(), + "pow": interpolationFuncPow(), + "uuid": interpolationFuncUUID(), + "replace": interpolationFuncReplace(), + "rsadecrypt": interpolationFuncRsaDecrypt(), + "sha1": interpolationFuncSha1(), + "sha256": interpolationFuncSha256(), + "sha512": interpolationFuncSha512(), + "signum": interpolationFuncSignum(), + "slice": interpolationFuncSlice(), + "sort": interpolationFuncSort(), + "split": interpolationFuncSplit(), + "substr": interpolationFuncSubstr(), + "timestamp": interpolationFuncTimestamp(), + "timeadd": interpolationFuncTimeAdd(), + "title": interpolationFuncTitle(), + "transpose": interpolationFuncTranspose(), + "trimspace": interpolationFuncTrimSpace(), + "upper": interpolationFuncUpper(), + "urlencode": interpolationFuncURLEncode(), + "zipmap": interpolationFuncZipMap(), } } @@ -669,6 +690,21 @@ func interpolationFuncFormatList() ast.Function { } } +// interpolationFuncIndent indents a multi-line string with the +// specified number of spaces +func interpolationFuncIndent() ast.Function { + return ast.Function{ + ArgTypes: []ast.Type{ast.TypeInt, ast.TypeString}, + ReturnType: ast.TypeString, + Callback: func(args []interface{}) (interface{}, error) { + spaces := args[0].(int) + data := args[1].(string) + pad := strings.Repeat(" ", spaces) + return strings.Replace(data, "\n", "\n"+pad, -1), nil + }, + } +} + // interpolationFuncIndex implements the "index" function that allows one to // find the index of a specific element in a list func interpolationFuncIndex() ast.Function { @@ -823,8 +859,7 @@ func interpolationFuncJoin() ast.Function { } // interpolationFuncJSONEncode implements the "jsonencode" function that encodes -// a string, list, or map as its JSON representation. For now, values in the -// list or map may only be strings. +// a string, list, or map as its JSON representation. func interpolationFuncJSONEncode() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeAny}, @@ -837,28 +872,36 @@ func interpolationFuncJSONEncode() ast.Function { toEncode = typedArg case []ast.Variable: - // We preallocate the list here. Note that it's important that in - // the length 0 case, we have an empty list rather than nil, as - // they encode differently. - // XXX It would be nice to support arbitrarily nested data here. Is - // there an inverse of hil.InterfaceToVariable? strings := make([]string, len(typedArg)) for i, v := range typedArg { if v.Type != ast.TypeString { - return "", fmt.Errorf("list elements must be strings") + variable, _ := hil.InterfaceToVariable(typedArg) + toEncode, _ = hil.VariableToInterface(variable) + + jEnc, err := json.Marshal(toEncode) + if err != nil { + return "", fmt.Errorf("failed to encode JSON data '%s'", toEncode) + } + return string(jEnc), nil + } strings[i] = v.Value.(string) } toEncode = strings case map[string]ast.Variable: - // XXX It would be nice to support arbitrarily nested data here. Is - // there an inverse of hil.InterfaceToVariable? stringMap := make(map[string]string) for k, v := range typedArg { if v.Type != ast.TypeString { - return "", fmt.Errorf("map values must be strings") + variable, _ := hil.InterfaceToVariable(typedArg) + toEncode, _ = hil.VariableToInterface(variable) + + jEnc, err := json.Marshal(toEncode) + if err != nil { + return "", fmt.Errorf("failed to encode JSON data '%s'", toEncode) + } + return string(jEnc), nil } stringMap[k] = v.Value.(string) } @@ -1098,6 +1141,56 @@ func interpolationFuncElement() ast.Function { } } +// returns the `list` items chunked by `size`. +func interpolationFuncChunklist() ast.Function { + return ast.Function{ + ArgTypes: []ast.Type{ + ast.TypeList, // inputList + ast.TypeInt, // size + }, + ReturnType: ast.TypeList, + Callback: func(args []interface{}) (interface{}, error) { + output := make([]ast.Variable, 0) + + values, _ := args[0].([]ast.Variable) + size, _ := args[1].(int) + + // errors if size is negative + if size < 0 { + return nil, fmt.Errorf("The size argument must be positive") + } + + // if size is 0, returns a list made of the initial list + if size == 0 { + output = append(output, ast.Variable{ + Type: ast.TypeList, + Value: values, + }) + return output, nil + } + + variables := make([]ast.Variable, 0) + chunk := ast.Variable{ + Type: ast.TypeList, + Value: variables, + } + l := len(values) + for i, v := range values { + variables = append(variables, v) + + // Chunk when index isn't 0, or when reaching the values's length + if (i+1)%size == 0 || (i+1) == l { + chunk.Value = variables + output = append(output, chunk) + variables = make([]ast.Variable, 0) + } + } + + return output, nil + }, + } +} + // interpolationFuncKeys implements the "keys" function that yields a list of // keys of map types within a Terraform configuration. func interpolationFuncKeys(vs map[string]ast.Variable) ast.Function { @@ -1197,6 +1290,32 @@ func interpolationFuncBase64Decode() ast.Function { } } +// interpolationFuncBase64Gzip implements the "gzip" function that allows gzip +// compression encoding the result using base64 +func interpolationFuncBase64Gzip() ast.Function { + return ast.Function{ + ArgTypes: []ast.Type{ast.TypeString}, + ReturnType: ast.TypeString, + Callback: func(args []interface{}) (interface{}, error) { + s := args[0].(string) + + var b bytes.Buffer + gz := gzip.NewWriter(&b) + if _, err := gz.Write([]byte(s)); err != nil { + return "", fmt.Errorf("failed to write gzip raw data: '%s'", s) + } + if err := gz.Flush(); err != nil { + return "", fmt.Errorf("failed to flush gzip writer: '%s'", s) + } + if err := gz.Close(); err != nil { + return "", fmt.Errorf("failed to close gzip writer: '%s'", s) + } + + return base64.StdEncoding.EncodeToString(b.Bytes()), nil + }, + } +} + // interpolationFuncLower implements the "lower" function that does // string lower casing. func interpolationFuncLower() ast.Function { @@ -1396,6 +1515,29 @@ func interpolationFuncTimestamp() ast.Function { } } +func interpolationFuncTimeAdd() ast.Function { + return ast.Function{ + ArgTypes: []ast.Type{ + ast.TypeString, // input timestamp string in RFC3339 format + ast.TypeString, // duration to add to input timestamp that should be parsable by time.ParseDuration + }, + ReturnType: ast.TypeString, + Callback: func(args []interface{}) (interface{}, error) { + + ts, err := time.Parse(time.RFC3339, args[0].(string)) + if err != nil { + return nil, err + } + duration, err := time.ParseDuration(args[1].(string)) + if err != nil { + return nil, err + } + + return ts.Add(duration).Format(time.RFC3339), nil + }, + } +} + // interpolationFuncTitle implements the "title" function that returns a copy of the // string in which first characters of all the words are capitalized. func interpolationFuncTitle() ast.Function { @@ -1441,7 +1583,7 @@ func interpolationFuncSubstr() ast.Function { return nil, fmt.Errorf("length should be a non-negative integer") } - if offset > len(str) { + if offset > len(str) || offset < 0 { return nil, fmt.Errorf("offset cannot be larger than the length of the string") } @@ -1453,3 +1595,160 @@ func interpolationFuncSubstr() ast.Function { }, } } + +// Flatten until it's not ast.TypeList +func flattener(finalList []ast.Variable, flattenList []ast.Variable) []ast.Variable { + for _, val := range flattenList { + if val.Type == ast.TypeList { + finalList = flattener(finalList, val.Value.([]ast.Variable)) + } else { + finalList = append(finalList, val) + } + } + return finalList +} + +// Flatten to single list +func interpolationFuncFlatten() ast.Function { + return ast.Function{ + ArgTypes: []ast.Type{ast.TypeList}, + ReturnType: ast.TypeList, + Variadic: false, + Callback: func(args []interface{}) (interface{}, error) { + inputList := args[0].([]ast.Variable) + + var outputList []ast.Variable + return flattener(outputList, inputList), nil + }, + } +} + +func interpolationFuncURLEncode() ast.Function { + return ast.Function{ + ArgTypes: []ast.Type{ast.TypeString}, + ReturnType: ast.TypeString, + Callback: func(args []interface{}) (interface{}, error) { + s := args[0].(string) + return url.QueryEscape(s), nil + }, + } +} + +// interpolationFuncTranspose implements the "transpose" function +// that converts a map (string,list) to a map (string,list) where +// the unique values of the original lists become the keys of the +// new map and the keys of the original map become values for the +// corresponding new keys. +func interpolationFuncTranspose() ast.Function { + return ast.Function{ + ArgTypes: []ast.Type{ast.TypeMap}, + ReturnType: ast.TypeMap, + Callback: func(args []interface{}) (interface{}, error) { + + inputMap := args[0].(map[string]ast.Variable) + outputMap := make(map[string]ast.Variable) + tmpMap := make(map[string][]string) + + for inKey, inVal := range inputMap { + if inVal.Type != ast.TypeList { + return nil, fmt.Errorf("transpose requires a map of lists of strings") + } + values := inVal.Value.([]ast.Variable) + for _, listVal := range values { + if listVal.Type != ast.TypeString { + return nil, fmt.Errorf("transpose requires the given map values to be lists of strings") + } + outKey := listVal.Value.(string) + if _, ok := tmpMap[outKey]; !ok { + tmpMap[outKey] = make([]string, 0) + } + outVal := tmpMap[outKey] + outVal = append(outVal, inKey) + sort.Strings(outVal) + tmpMap[outKey] = outVal + } + } + + for outKey, outVal := range tmpMap { + values := make([]ast.Variable, 0) + for _, v := range outVal { + values = append(values, ast.Variable{Type: ast.TypeString, Value: v}) + } + outputMap[outKey] = ast.Variable{Type: ast.TypeList, Value: values} + } + return outputMap, nil + }, + } +} + +// interpolationFuncAbs returns the absolute value of a given float. +func interpolationFuncAbs() ast.Function { + return ast.Function{ + ArgTypes: []ast.Type{ast.TypeFloat}, + ReturnType: ast.TypeFloat, + Callback: func(args []interface{}) (interface{}, error) { + return math.Abs(args[0].(float64)), nil + }, + } +} + +// interpolationFuncRsaDecrypt implements the "rsadecrypt" function that does +// RSA decryption. +func interpolationFuncRsaDecrypt() ast.Function { + return ast.Function{ + ArgTypes: []ast.Type{ast.TypeString, ast.TypeString}, + ReturnType: ast.TypeString, + Callback: func(args []interface{}) (interface{}, error) { + s := args[0].(string) + key := args[1].(string) + + b, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return "", fmt.Errorf("Failed to decode input %q: cipher text must be base64-encoded", s) + } + + block, _ := pem.Decode([]byte(key)) + if block == nil { + return "", fmt.Errorf("Failed to read key %q: no key found", key) + } + if block.Headers["Proc-Type"] == "4,ENCRYPTED" { + return "", fmt.Errorf( + "Failed to read key %q: password protected keys are\n"+ + "not supported. Please decrypt the key prior to use.", key) + } + + x509Key, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return "", err + } + + out, err := rsa.DecryptPKCS1v15(nil, x509Key, b) + if err != nil { + return "", err + } + + return string(out), nil + }, + } +} + +// interpolationFuncMakeFileHash constructs a function that hashes the contents +// of a file by combining the implementations of the file(...) function and +// a given other function that is assumed to take a single string argument and +// return a hash value. +func interpolationFuncMakeFileHash(hashFunc ast.Function) ast.Function { + fileFunc := interpolationFuncFile() + + return ast.Function{ + ArgTypes: []ast.Type{ast.TypeString}, + ReturnType: ast.TypeString, + Callback: func(args []interface{}) (interface{}, error) { + filename := args[0].(string) + contents, err := fileFunc.Callback([]interface{}{filename}) + if err != nil { + return nil, err + } + return hashFunc.Callback([]interface{}{contents}) + }, + } +} diff --git a/vendor/github.com/hashicorp/terraform/config/interpolate_walk.go b/vendor/github.com/hashicorp/terraform/config/interpolate_walk.go index ead3d10..66a677d 100644 --- a/vendor/github.com/hashicorp/terraform/config/interpolate_walk.go +++ b/vendor/github.com/hashicorp/terraform/config/interpolate_walk.go @@ -271,9 +271,7 @@ func (w *interpolationWalker) splitSlice() { result = append(result, val.Value) } case []interface{}: - for _, element := range val { - result = append(result, element) - } + result = append(result, val...) default: result = append(result, v) } diff --git a/vendor/github.com/hashicorp/terraform/config/loader.go b/vendor/github.com/hashicorp/terraform/config/loader.go index 5dd7d46..6e34781 100644 --- a/vendor/github.com/hashicorp/terraform/config/loader.go +++ b/vendor/github.com/hashicorp/terraform/config/loader.go @@ -80,7 +80,7 @@ func LoadDir(root string) (*Config, error) { if err != nil { return nil, err } - if len(files) == 0 { + if len(files) == 0 && len(overrides) == 0 { return nil, &ErrNoConfigsFound{Dir: root} } @@ -112,6 +112,9 @@ func LoadDir(root string) (*Config, error) { result = c } } + if len(files) == 0 { + result = &Config{} + } // Load all the overrides, and merge them into the config for _, f := range overrides { diff --git a/vendor/github.com/hashicorp/terraform/config/loader_hcl.go b/vendor/github.com/hashicorp/terraform/config/loader_hcl.go index e85e493..68cffe2 100644 --- a/vendor/github.com/hashicorp/terraform/config/loader_hcl.go +++ b/vendor/github.com/hashicorp/terraform/config/loader_hcl.go @@ -17,10 +17,20 @@ type hclConfigurable struct { Root *ast.File } +var ReservedDataSourceFields = []string{ + "connection", + "count", + "depends_on", + "lifecycle", + "provider", + "provisioner", +} + var ReservedResourceFields = []string{ "connection", "count", "depends_on", + "id", "lifecycle", "provider", "provisioner", @@ -35,6 +45,7 @@ func (t *hclConfigurable) Config() (*Config, error) { validKeys := map[string]struct{}{ "atlas": struct{}{}, "data": struct{}{}, + "locals": struct{}{}, "module": struct{}{}, "output": struct{}{}, "provider": struct{}{}, @@ -70,6 +81,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 @@ -373,9 +393,6 @@ func loadModulesHcl(list *ast.ObjectList) ([]*Module, error) { err) } - // Remove the fields we handle specially - delete(config, "source") - rawConfig, err := NewRawConfig(config) if err != nil { return nil, fmt.Errorf( @@ -384,7 +401,11 @@ func loadModulesHcl(list *ast.ObjectList) ([]*Module, error) { err) } - // If we have a count, then figure it out + // Remove the fields we handle specially + delete(config, "source") + delete(config, "version") + delete(config, "providers") + var source string if o := listVal.Filter("source"); len(o.Items) > 0 { err = hcl.DecodeObject(&source, o.Items[0].Val) @@ -396,9 +417,33 @@ func loadModulesHcl(list *ast.ObjectList) ([]*Module, error) { } } + var version string + if o := listVal.Filter("version"); len(o.Items) > 0 { + err = hcl.DecodeObject(&version, o.Items[0].Val) + if err != nil { + return nil, fmt.Errorf( + "Error parsing version for %s: %s", + k, + err) + } + } + + var providers map[string]string + if o := listVal.Filter("providers"); len(o.Items) > 0 { + err = hcl.DecodeObject(&providers, o.Items[0].Val) + if err != nil { + return nil, fmt.Errorf( + "Error parsing providers for %s: %s", + k, + err) + } + } + result = append(result, &Module{ Name: k, Source: source, + Version: version, + Providers: providers, RawConfig: rawConfig, }) } @@ -406,6 +451,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) { @@ -434,6 +532,7 @@ func loadOutputsHcl(list *ast.ObjectList) ([]*Output, error) { // Delete special keys delete(config, "depends_on") + delete(config, "description") rawConfig, err := NewRawConfig(config) if err != nil { @@ -455,10 +554,23 @@ func loadOutputsHcl(list *ast.ObjectList) ([]*Output, error) { } } + // If we have a description field, then filter that + var description string + if o := listVal.Filter("description"); len(o.Items) > 0 { + err := hcl.DecodeObject(&description, o.Items[0].Val) + if err != nil { + return nil, fmt.Errorf( + "Error reading description for output %q: %s", + n, + err) + } + } + result = append(result, &Output{ - Name: n, - RawConfig: rawConfig, - DependsOn: dependsOn, + Name: n, + RawConfig: rawConfig, + DependsOn: dependsOn, + Description: description, }) } diff --git a/vendor/github.com/hashicorp/terraform/config/loader_hcl2.go b/vendor/github.com/hashicorp/terraform/config/loader_hcl2.go new file mode 100644 index 0000000..4f9f129 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/config/loader_hcl2.go @@ -0,0 +1,473 @@ +package config + +import ( + "fmt" + "sort" + "strings" + + gohcl2 "github.com/hashicorp/hcl2/gohcl" + hcl2 "github.com/hashicorp/hcl2/hcl" + hcl2parse "github.com/hashicorp/hcl2/hclparse" + "github.com/hashicorp/terraform/config/hcl2shim" + "github.com/zclconf/go-cty/cty" +) + +// hcl2Configurable is an implementation of configurable that knows +// how to turn a HCL Body into a *Config object. +type hcl2Configurable struct { + SourceFilename string + Body hcl2.Body +} + +// hcl2Loader is a wrapper around a HCL parser that provides a fileLoaderFunc. +type hcl2Loader struct { + Parser *hcl2parse.Parser +} + +// For the moment we'll just have a global loader since we don't have anywhere +// better to stash this. +// TODO: refactor the loader API so that it uses some sort of object we can +// stash the parser inside. +var globalHCL2Loader = newHCL2Loader() + +// newHCL2Loader creates a new hcl2Loader containing a new HCL Parser. +// +// HCL parsers retain information about files that are loaded to aid in +// producing diagnostic messages, so all files within a single configuration +// should be loaded with the same parser to ensure the availability of +// full diagnostic information. +func newHCL2Loader() hcl2Loader { + return hcl2Loader{ + Parser: hcl2parse.NewParser(), + } +} + +// loadFile is a fileLoaderFunc that knows how to read a HCL2 file and turn it +// into a hcl2Configurable. +func (l hcl2Loader) loadFile(filename string) (configurable, []string, error) { + var f *hcl2.File + var diags hcl2.Diagnostics + if strings.HasSuffix(filename, ".json") { + f, diags = l.Parser.ParseJSONFile(filename) + } else { + f, diags = l.Parser.ParseHCLFile(filename) + } + if diags.HasErrors() { + // Return diagnostics as an error; callers may type-assert this to + // recover the original diagnostics, if it doesn't end up wrapped + // in another error. + return nil, nil, diags + } + + return &hcl2Configurable{ + SourceFilename: filename, + Body: f.Body, + }, nil, nil +} + +func (t *hcl2Configurable) Config() (*Config, error) { + config := &Config{} + + // these structs are used only for the initial shallow decoding; we'll + // expand this into the main, public-facing config structs afterwards. + type atlas struct { + Name string `hcl:"name"` + Include *[]string `hcl:"include"` + Exclude *[]string `hcl:"exclude"` + } + type provider struct { + Name string `hcl:"name,label"` + Alias *string `hcl:"alias,attr"` + Version *string `hcl:"version,attr"` + Config hcl2.Body `hcl:",remain"` + } + type module struct { + Name string `hcl:"name,label"` + Source string `hcl:"source,attr"` + Version *string `hcl:"version,attr"` + Providers *map[string]string `hcl:"providers,attr"` + Config hcl2.Body `hcl:",remain"` + } + type resourceLifecycle struct { + CreateBeforeDestroy *bool `hcl:"create_before_destroy,attr"` + PreventDestroy *bool `hcl:"prevent_destroy,attr"` + IgnoreChanges *[]string `hcl:"ignore_changes,attr"` + } + type connection struct { + Config hcl2.Body `hcl:",remain"` + } + type provisioner struct { + Type string `hcl:"type,label"` + + When *string `hcl:"when,attr"` + OnFailure *string `hcl:"on_failure,attr"` + + Connection *connection `hcl:"connection,block"` + Config hcl2.Body `hcl:",remain"` + } + type managedResource struct { + Type string `hcl:"type,label"` + Name string `hcl:"name,label"` + + CountExpr hcl2.Expression `hcl:"count,attr"` + Provider *string `hcl:"provider,attr"` + DependsOn *[]string `hcl:"depends_on,attr"` + + Lifecycle *resourceLifecycle `hcl:"lifecycle,block"` + Provisioners []provisioner `hcl:"provisioner,block"` + Connection *connection `hcl:"connection,block"` + + Config hcl2.Body `hcl:",remain"` + } + type dataResource struct { + Type string `hcl:"type,label"` + Name string `hcl:"name,label"` + + CountExpr hcl2.Expression `hcl:"count,attr"` + Provider *string `hcl:"provider,attr"` + DependsOn *[]string `hcl:"depends_on,attr"` + + Config hcl2.Body `hcl:",remain"` + } + type variable struct { + Name string `hcl:"name,label"` + + DeclaredType *string `hcl:"type,attr"` + Default *cty.Value `hcl:"default,attr"` + Description *string `hcl:"description,attr"` + Sensitive *bool `hcl:"sensitive,attr"` + } + type output struct { + Name string `hcl:"name,label"` + + ValueExpr hcl2.Expression `hcl:"value,attr"` + DependsOn *[]string `hcl:"depends_on,attr"` + Description *string `hcl:"description,attr"` + Sensitive *bool `hcl:"sensitive,attr"` + } + type locals struct { + Definitions hcl2.Attributes `hcl:",remain"` + } + type backend struct { + Type string `hcl:"type,label"` + Config hcl2.Body `hcl:",remain"` + } + type terraform struct { + RequiredVersion *string `hcl:"required_version,attr"` + Backend *backend `hcl:"backend,block"` + } + type topLevel struct { + Atlas *atlas `hcl:"atlas,block"` + Datas []dataResource `hcl:"data,block"` + Modules []module `hcl:"module,block"` + Outputs []output `hcl:"output,block"` + Providers []provider `hcl:"provider,block"` + Resources []managedResource `hcl:"resource,block"` + Terraform *terraform `hcl:"terraform,block"` + Variables []variable `hcl:"variable,block"` + Locals []*locals `hcl:"locals,block"` + } + + var raw topLevel + diags := gohcl2.DecodeBody(t.Body, nil, &raw) + if diags.HasErrors() { + // Do some minimal decoding to see if we can at least get the + // required Terraform version, which might help explain why we + // couldn't parse the rest. + if raw.Terraform != nil && raw.Terraform.RequiredVersion != nil { + config.Terraform = &Terraform{ + RequiredVersion: *raw.Terraform.RequiredVersion, + } + } + + // We return the diags as an implementation of error, which the + // caller than then type-assert if desired to recover the individual + // diagnostics. + // FIXME: The current API gives us no way to return warnings in the + // absense of any errors. + return config, diags + } + + if raw.Terraform != nil { + var reqdVersion string + var backend *Backend + + if raw.Terraform.RequiredVersion != nil { + reqdVersion = *raw.Terraform.RequiredVersion + } + if raw.Terraform.Backend != nil { + backend = new(Backend) + backend.Type = raw.Terraform.Backend.Type + + // We don't permit interpolations or nested blocks inside the + // backend config, so we can decode the config early here and + // get direct access to the values, which is important for the + // config hashing to work as expected. + var config map[string]string + configDiags := gohcl2.DecodeBody(raw.Terraform.Backend.Config, nil, &config) + diags = append(diags, configDiags...) + + raw := make(map[string]interface{}, len(config)) + for k, v := range config { + raw[k] = v + } + + var err error + backend.RawConfig, err = NewRawConfig(raw) + if err != nil { + diags = append(diags, &hcl2.Diagnostic{ + Severity: hcl2.DiagError, + Summary: "Invalid backend configuration", + Detail: fmt.Sprintf("Error in backend configuration: %s", err), + }) + } + } + + config.Terraform = &Terraform{ + RequiredVersion: reqdVersion, + Backend: backend, + } + } + + if raw.Atlas != nil { + var include, exclude []string + if raw.Atlas.Include != nil { + include = *raw.Atlas.Include + } + if raw.Atlas.Exclude != nil { + exclude = *raw.Atlas.Exclude + } + config.Atlas = &AtlasConfig{ + Name: raw.Atlas.Name, + Include: include, + Exclude: exclude, + } + } + + for _, rawM := range raw.Modules { + m := &Module{ + Name: rawM.Name, + Source: rawM.Source, + RawConfig: NewRawConfigHCL2(rawM.Config), + } + + if rawM.Version != nil { + m.Version = *rawM.Version + } + + if rawM.Providers != nil { + m.Providers = *rawM.Providers + } + + config.Modules = append(config.Modules, m) + } + + for _, rawV := range raw.Variables { + v := &Variable{ + Name: rawV.Name, + } + if rawV.DeclaredType != nil { + v.DeclaredType = *rawV.DeclaredType + } + if rawV.Default != nil { + v.Default = hcl2shim.ConfigValueFromHCL2(*rawV.Default) + } + if rawV.Description != nil { + v.Description = *rawV.Description + } + + config.Variables = append(config.Variables, v) + } + + for _, rawO := range raw.Outputs { + o := &Output{ + Name: rawO.Name, + } + + if rawO.Description != nil { + o.Description = *rawO.Description + } + if rawO.DependsOn != nil { + o.DependsOn = *rawO.DependsOn + } + if rawO.Sensitive != nil { + o.Sensitive = *rawO.Sensitive + } + + // The result is expected to be a map like map[string]interface{}{"value": something}, + // so we'll fake that with our hcl2shim.SingleAttrBody shim. + o.RawConfig = NewRawConfigHCL2(hcl2shim.SingleAttrBody{ + Name: "value", + Expr: rawO.ValueExpr, + }) + + config.Outputs = append(config.Outputs, o) + } + + for _, rawR := range raw.Resources { + r := &Resource{ + Mode: ManagedResourceMode, + Type: rawR.Type, + Name: rawR.Name, + } + if rawR.Lifecycle != nil { + var l ResourceLifecycle + if rawR.Lifecycle.CreateBeforeDestroy != nil { + l.CreateBeforeDestroy = *rawR.Lifecycle.CreateBeforeDestroy + } + if rawR.Lifecycle.PreventDestroy != nil { + l.PreventDestroy = *rawR.Lifecycle.PreventDestroy + } + if rawR.Lifecycle.IgnoreChanges != nil { + l.IgnoreChanges = *rawR.Lifecycle.IgnoreChanges + } + r.Lifecycle = l + } + if rawR.Provider != nil { + r.Provider = *rawR.Provider + } + if rawR.DependsOn != nil { + r.DependsOn = *rawR.DependsOn + } + + var defaultConnInfo *RawConfig + if rawR.Connection != nil { + defaultConnInfo = NewRawConfigHCL2(rawR.Connection.Config) + } + + for _, rawP := range rawR.Provisioners { + p := &Provisioner{ + Type: rawP.Type, + } + + switch { + case rawP.When == nil: + p.When = ProvisionerWhenCreate + case *rawP.When == "create": + p.When = ProvisionerWhenCreate + case *rawP.When == "destroy": + p.When = ProvisionerWhenDestroy + default: + p.When = ProvisionerWhenInvalid + } + + switch { + case rawP.OnFailure == nil: + p.OnFailure = ProvisionerOnFailureFail + case *rawP.When == "fail": + p.OnFailure = ProvisionerOnFailureFail + case *rawP.When == "continue": + p.OnFailure = ProvisionerOnFailureContinue + default: + p.OnFailure = ProvisionerOnFailureInvalid + } + + if rawP.Connection != nil { + p.ConnInfo = NewRawConfigHCL2(rawP.Connection.Config) + } else { + p.ConnInfo = defaultConnInfo + } + + p.RawConfig = NewRawConfigHCL2(rawP.Config) + + r.Provisioners = append(r.Provisioners, p) + } + + // The old loader records the count expression as a weird RawConfig with + // a single-element map inside. Since the rest of the world is assuming + // that, we'll mimic it here. + { + countBody := hcl2shim.SingleAttrBody{ + Name: "count", + Expr: rawR.CountExpr, + } + + r.RawCount = NewRawConfigHCL2(countBody) + r.RawCount.Key = "count" + } + + r.RawConfig = NewRawConfigHCL2(rawR.Config) + + config.Resources = append(config.Resources, r) + + } + + for _, rawR := range raw.Datas { + r := &Resource{ + Mode: DataResourceMode, + Type: rawR.Type, + Name: rawR.Name, + } + + if rawR.Provider != nil { + r.Provider = *rawR.Provider + } + if rawR.DependsOn != nil { + r.DependsOn = *rawR.DependsOn + } + + // The old loader records the count expression as a weird RawConfig with + // a single-element map inside. Since the rest of the world is assuming + // that, we'll mimic it here. + { + countBody := hcl2shim.SingleAttrBody{ + Name: "count", + Expr: rawR.CountExpr, + } + + r.RawCount = NewRawConfigHCL2(countBody) + r.RawCount.Key = "count" + } + + r.RawConfig = NewRawConfigHCL2(rawR.Config) + + config.Resources = append(config.Resources, r) + } + + for _, rawP := range raw.Providers { + p := &ProviderConfig{ + Name: rawP.Name, + } + + if rawP.Alias != nil { + p.Alias = *rawP.Alias + } + if rawP.Version != nil { + p.Version = *rawP.Version + } + + // The result is expected to be a map like map[string]interface{}{"value": something}, + // so we'll fake that with our hcl2shim.SingleAttrBody shim. + p.RawConfig = NewRawConfigHCL2(rawP.Config) + + config.ProviderConfigs = append(config.ProviderConfigs, p) + } + + for _, rawL := range raw.Locals { + names := make([]string, 0, len(rawL.Definitions)) + for n := range rawL.Definitions { + names = append(names, n) + } + sort.Strings(names) + for _, n := range names { + attr := rawL.Definitions[n] + l := &Local{ + Name: n, + RawConfig: NewRawConfigHCL2(hcl2shim.SingleAttrBody{ + Name: "value", + Expr: attr.Expr, + }), + } + config.Locals = append(config.Locals, l) + } + } + + // FIXME: The current API gives us no way to return warnings in the + // absense of any errors. + var err error + if diags.HasErrors() { + err = diags + } + + return config, err +} diff --git a/vendor/github.com/hashicorp/terraform/config/merge.go b/vendor/github.com/hashicorp/terraform/config/merge.go index db214be..55fc864 100644 --- a/vendor/github.com/hashicorp/terraform/config/merge.go +++ b/vendor/github.com/hashicorp/terraform/config/merge.go @@ -137,6 +137,17 @@ func Merge(c1, c2 *Config) (*Config, error) { } } + // Local Values + // These are simpler than the other config elements because they are just + // flat values and so no deep merging is required. + if localsCount := len(c1.Locals) + len(c2.Locals); localsCount != 0 { + // Explicit length check above because we want c.Locals to remain + // nil if the result would be empty. + c.Locals = make([]*Local, 0, len(c1.Locals)+len(c2.Locals)) + c.Locals = append(c.Locals, c1.Locals...) + c.Locals = append(c.Locals, c2.Locals...) + } + return c, nil } diff --git a/vendor/github.com/hashicorp/terraform/config/module/get.go b/vendor/github.com/hashicorp/terraform/config/module/get.go index 96b4a63..5073d0d 100644 --- a/vendor/github.com/hashicorp/terraform/config/module/get.go +++ b/vendor/github.com/hashicorp/terraform/config/module/get.go @@ -3,6 +3,7 @@ package module import ( "io/ioutil" "os" + "path/filepath" "github.com/hashicorp/go-getter" ) @@ -37,13 +38,10 @@ func GetCopy(dst, src string) error { if err != nil { return err } - // FIXME: This isn't completely safe. Creating and removing our temp path - // exposes where to race to inject files. - if err := os.RemoveAll(tmpDir); err != nil { - return err - } defer os.RemoveAll(tmpDir) + tmpDir = filepath.Join(tmpDir, "module") + // Get to that temporary dir if err := getter.Get(tmpDir, src); err != nil { return err @@ -57,15 +55,3 @@ func GetCopy(dst, src string) error { // Copy to the final location return copyDir(dst, tmpDir) } - -func getStorage(s getter.Storage, key string, src string, mode GetMode) (string, bool, error) { - // Get the module with the level specified if we were told to. - if mode > GetModeNone { - if err := s.Get(key, src, mode == GetModeUpdate); err != nil { - return "", false, err - } - } - - // Get the directory where the module is. - return s.Dir(key) -} diff --git a/vendor/github.com/hashicorp/terraform/config/module/inode.go b/vendor/github.com/hashicorp/terraform/config/module/inode.go index 8603ee2..da520ab 100644 --- a/vendor/github.com/hashicorp/terraform/config/module/inode.go +++ b/vendor/github.com/hashicorp/terraform/config/module/inode.go @@ -1,4 +1,4 @@ -// +build linux darwin openbsd netbsd solaris +// +build linux darwin openbsd netbsd solaris dragonfly package module diff --git a/vendor/github.com/hashicorp/terraform/config/module/module.go b/vendor/github.com/hashicorp/terraform/config/module/module.go index f8649f6..7dc8fcc 100644 --- a/vendor/github.com/hashicorp/terraform/config/module/module.go +++ b/vendor/github.com/hashicorp/terraform/config/module/module.go @@ -2,6 +2,8 @@ package module // Module represents the metadata for a single module. type Module struct { - Name string - Source string + Name string + Source string + Version string + Providers map[string]string } diff --git a/vendor/github.com/hashicorp/terraform/config/module/storage.go b/vendor/github.com/hashicorp/terraform/config/module/storage.go new file mode 100644 index 0000000..58e3a10 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/config/module/storage.go @@ -0,0 +1,365 @@ +package module + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "strings" + + getter "github.com/hashicorp/go-getter" + "github.com/hashicorp/terraform/registry" + "github.com/hashicorp/terraform/registry/regsrc" + "github.com/hashicorp/terraform/svchost/disco" + "github.com/mitchellh/cli" +) + +const manifestName = "modules.json" + +// moduleManifest is the serialization structure used to record the stored +// module's metadata. +type moduleManifest struct { + Modules []moduleRecord +} + +// moduleRecords represents the stored module's metadata. +// This is compared for equality using '==', so all fields needs to remain +// comparable. +type moduleRecord struct { + // Source is the module source string from the config, minus any + // subdirectory. + Source string + + // Key is the locally unique identifier for this module. + Key string + + // Version is the exact version string for the stored module. + Version string + + // Dir is the directory name returned by the FileStorage. This is what + // allows us to correlate a particular module version with the location on + // disk. + Dir string + + // Root is the root directory containing the module. If the module is + // unpacked from an archive, and not located in the root directory, this is + // used to direct the loader to the correct subdirectory. This is + // independent from any subdirectory in the original source string, which + // may traverse further into the module tree. + Root string + + // url is the location of the module source + url string + + // Registry is true if this module is sourced from a registry + registry bool +} + +// Storage implements methods to manage the storage of modules. +// This is used by Tree.Load to query registries, authenticate requests, and +// store modules locally. +type Storage struct { + // StorageDir is the full path to the directory where all modules will be + // stored. + StorageDir string + + // Ui is an optional cli.Ui for user output + Ui cli.Ui + + // Mode is the GetMode that will be used for various operations. + Mode GetMode + + registry *registry.Client +} + +// NewStorage returns a new initialized Storage object. +func NewStorage(dir string, services *disco.Disco) *Storage { + regClient := registry.NewClient(services, nil) + + return &Storage{ + StorageDir: dir, + registry: regClient, + } +} + +// loadManifest returns the moduleManifest file from the parent directory. +func (s Storage) loadManifest() (moduleManifest, error) { + manifest := moduleManifest{} + + manifestPath := filepath.Join(s.StorageDir, manifestName) + data, err := ioutil.ReadFile(manifestPath) + if err != nil && !os.IsNotExist(err) { + return manifest, err + } + + if len(data) == 0 { + return manifest, nil + } + + if err := json.Unmarshal(data, &manifest); err != nil { + return manifest, err + } + + for i, rec := range manifest.Modules { + // If the path was recorded before we changed to always using a + // slash as separator, we delete the record from the manifest so + // it can be discovered again and will be recorded using a slash. + if strings.Contains(rec.Dir, "\\") { + manifest.Modules[i] = manifest.Modules[len(manifest.Modules)-1] + manifest.Modules = manifest.Modules[:len(manifest.Modules)-1] + continue + } + + // Make sure we use the correct path separator. + rec.Dir = filepath.FromSlash(rec.Dir) + } + + return manifest, nil +} + +// Store the location of the module, along with the version used and the module +// root directory. The storage method loads the entire file and rewrites it +// each time. This is only done a few times during init, so efficiency is +// not a concern. +func (s Storage) recordModule(rec moduleRecord) error { + manifest, err := s.loadManifest() + if err != nil { + // if there was a problem with the file, we will attempt to write a new + // one. Any non-data related error should surface there. + log.Printf("[WARN] error reading module manifest: %s", err) + } + + // do nothing if we already have the exact module + for i, stored := range manifest.Modules { + if rec == stored { + return nil + } + + // they are not equal, but if the storage path is the same we need to + // remove this rec to be replaced. + if rec.Dir == stored.Dir { + manifest.Modules[i] = manifest.Modules[len(manifest.Modules)-1] + manifest.Modules = manifest.Modules[:len(manifest.Modules)-1] + break + } + } + + // Make sure we always use a slash separator. + rec.Dir = filepath.ToSlash(rec.Dir) + + manifest.Modules = append(manifest.Modules, rec) + + js, err := json.Marshal(manifest) + if err != nil { + panic(err) + } + + manifestPath := filepath.Join(s.StorageDir, manifestName) + return ioutil.WriteFile(manifestPath, js, 0644) +} + +// load the manifest from dir, and return all module versions matching the +// provided source. Records with no version info will be skipped, as they need +// to be uniquely identified by other means. +func (s Storage) moduleVersions(source string) ([]moduleRecord, error) { + manifest, err := s.loadManifest() + if err != nil { + return manifest.Modules, err + } + + var matching []moduleRecord + + for _, m := range manifest.Modules { + if m.Source == source && m.Version != "" { + log.Printf("[DEBUG] found local version %q for module %s", m.Version, m.Source) + matching = append(matching, m) + } + } + + return matching, nil +} + +func (s Storage) moduleDir(key string) (string, error) { + manifest, err := s.loadManifest() + if err != nil { + return "", err + } + + for _, m := range manifest.Modules { + if m.Key == key { + return m.Dir, nil + } + } + + return "", nil +} + +// return only the root directory of the module stored in dir. +func (s Storage) getModuleRoot(dir string) (string, error) { + manifest, err := s.loadManifest() + if err != nil { + return "", err + } + + for _, mod := range manifest.Modules { + if mod.Dir == dir { + return mod.Root, nil + } + } + return "", nil +} + +// record only the Root directory for the module stored at dir. +func (s Storage) recordModuleRoot(dir, root string) error { + rec := moduleRecord{ + Dir: dir, + Root: root, + } + + return s.recordModule(rec) +} + +func (s Storage) output(msg string) { + if s.Ui == nil || s.Mode == GetModeNone { + return + } + s.Ui.Output(msg) +} + +func (s Storage) getStorage(key string, src string) (string, bool, error) { + storage := &getter.FolderStorage{ + StorageDir: s.StorageDir, + } + + log.Printf("[DEBUG] fetching module from %s", src) + + // Get the module with the level specified if we were told to. + if s.Mode > GetModeNone { + log.Printf("[DEBUG] fetching %q with key %q", src, key) + if err := storage.Get(key, src, s.Mode == GetModeUpdate); err != nil { + return "", false, err + } + } + + // Get the directory where the module is. + dir, found, err := storage.Dir(key) + log.Printf("[DEBUG] found %q in %q: %t", src, dir, found) + return dir, found, err +} + +// find a stored module that's not from a registry +func (s Storage) findModule(key string) (string, error) { + if s.Mode == GetModeUpdate { + return "", nil + } + + return s.moduleDir(key) +} + +// GetModule fetches a module source into the specified directory. This is used +// as a convenience function by the CLI to initialize a configuration. +func (s Storage) GetModule(dst, src string) error { + // reset this in case the caller was going to re-use it + mode := s.Mode + s.Mode = GetModeUpdate + defer func() { + s.Mode = mode + }() + + rec, err := s.findRegistryModule(src, anyVersion) + if err != nil { + return err + } + + pwd, err := os.Getwd() + if err != nil { + return err + } + + source := rec.url + if source == "" { + source, err = getter.Detect(src, pwd, getter.Detectors) + if err != nil { + return fmt.Errorf("module %s: %s", src, err) + } + } + + if source == "" { + return fmt.Errorf("module %q not found", src) + } + + return GetCopy(dst, source) +} + +// find a registry module +func (s Storage) findRegistryModule(mSource, constraint string) (moduleRecord, error) { + rec := moduleRecord{ + Source: mSource, + } + // detect if we have a registry source + mod, err := regsrc.ParseModuleSource(mSource) + switch err { + case nil: + //ok + case regsrc.ErrInvalidModuleSource: + return rec, nil + default: + return rec, err + } + rec.registry = true + + log.Printf("[TRACE] %q is a registry module", mod.Display()) + + versions, err := s.moduleVersions(mod.String()) + if err != nil { + log.Printf("[ERROR] error looking up versions for %q: %s", mod.Display(), err) + return rec, err + } + + match, err := newestRecord(versions, constraint) + if err != nil { + log.Printf("[INFO] no matching version for %q<%s>, %s", mod.Display(), constraint, err) + } + log.Printf("[DEBUG] matched %q version %s for %s", mod, match.Version, constraint) + + rec.Dir = match.Dir + rec.Version = match.Version + found := rec.Dir != "" + + // we need to lookup available versions + // Only on Get if it's not found, on unconditionally on Update + if (s.Mode == GetModeGet && !found) || (s.Mode == GetModeUpdate) { + resp, err := s.registry.Versions(mod) + if err != nil { + return rec, err + } + + if len(resp.Modules) == 0 { + return rec, fmt.Errorf("module %q not found in registry", mod.Display()) + } + + match, err := newestVersion(resp.Modules[0].Versions, constraint) + if err != nil { + return rec, err + } + + if match == nil { + return rec, fmt.Errorf("no versions for %q found matching %q", mod.Display(), constraint) + } + + rec.Version = match.Version + + rec.url, err = s.registry.Location(mod, rec.Version) + if err != nil { + return rec, err + } + + // we've already validated this by now + host, _ := mod.SvcHost() + s.output(fmt.Sprintf(" Found version %s of %s on %s", rec.Version, mod.Module(), host.ForDisplay())) + + } + return rec, nil +} diff --git a/vendor/github.com/hashicorp/terraform/config/module/testing.go b/vendor/github.com/hashicorp/terraform/config/module/testing.go index fc9e733..6f1ff05 100644 --- a/vendor/github.com/hashicorp/terraform/config/module/testing.go +++ b/vendor/github.com/hashicorp/terraform/config/module/testing.go @@ -4,8 +4,6 @@ import ( "io/ioutil" "os" "testing" - - "github.com/hashicorp/go-getter" ) // TestTree loads a module at the given path and returns the tree as well @@ -26,8 +24,8 @@ func TestTree(t *testing.T, path string) (*Tree, func()) { } // Get the child modules - s := &getter.FolderStorage{StorageDir: dir} - if err := mod.Load(s, GetModeGet); err != nil { + s := &Storage{StorageDir: dir, Mode: GetModeGet} + if err := mod.Load(s); err != nil { t.Fatalf("err: %s", err) return nil, nil } diff --git a/vendor/github.com/hashicorp/terraform/config/module/tree.go b/vendor/github.com/hashicorp/terraform/config/module/tree.go index 4b0b153..f56d69b 100644 --- a/vendor/github.com/hashicorp/terraform/config/module/tree.go +++ b/vendor/github.com/hashicorp/terraform/config/module/tree.go @@ -4,11 +4,14 @@ import ( "bufio" "bytes" "fmt" + "log" "path/filepath" "strings" "sync" - "github.com/hashicorp/go-getter" + "github.com/hashicorp/terraform/tfdiags" + + getter "github.com/hashicorp/go-getter" "github.com/hashicorp/terraform/config" ) @@ -26,6 +29,17 @@ type Tree struct { children map[string]*Tree path []string lock sync.RWMutex + + // version is the final version of the config loaded for the Tree's module + version string + // source is the "source" string used to load this module. It's possible + // for a module source to change, but the path remains the same, preventing + // it from being reloaded. + source string + // parent allows us to walk back up the tree and determine if there are any + // versioned ancestor modules which may effect the stored location of + // submodules + parent *Tree } // NewTree returns a new Tree for the given config structure. @@ -40,7 +54,7 @@ func NewEmptyTree() *Tree { // We do this dummy load so that the tree is marked as "loaded". It // should never fail because this is just about a no-op. If it does fail // we panic so we can know its a bug. - if err := t.Load(nil, GetModeGet); err != nil { + if err := t.Load(&Storage{Mode: GetModeGet}); err != nil { panic(err) } @@ -126,8 +140,10 @@ func (t *Tree) Modules() []*Module { result := make([]*Module, len(t.config.Modules)) for i, m := range t.config.Modules { result[i] = &Module{ - Name: m.Name, - Source: m.Source, + Name: m.Name, + Version: m.Version, + Source: m.Source, + Providers: m.Providers, } } @@ -155,81 +171,178 @@ func (t *Tree) Name() string { // module trees inherently require the configuration to be in a reasonably // sane state: no circular dependencies, proper module sources, etc. A full // suite of validations can be done by running Validate (after loading). -func (t *Tree) Load(s getter.Storage, mode GetMode) error { +func (t *Tree) Load(s *Storage) error { t.lock.Lock() defer t.lock.Unlock() - // Reset the children if we have any - t.children = nil + children, err := t.getChildren(s) + if err != nil { + return err + } + + // Go through all the children and load them. + for _, c := range children { + if err := c.Load(s); err != nil { + return err + } + } + + // Set our tree up + t.children = children - modules := t.Modules() + return nil +} + +func (t *Tree) getChildren(s *Storage) (map[string]*Tree, error) { children := make(map[string]*Tree) // Go through all the modules and get the directory for them. - for _, m := range modules { + for _, m := range t.Modules() { if _, ok := children[m.Name]; ok { - return fmt.Errorf( + return nil, fmt.Errorf( "module %s: duplicated. module names must be unique", m.Name) } // Determine the path to this child - path := make([]string, len(t.path), len(t.path)+1) - copy(path, t.path) - path = append(path, m.Name) + modPath := make([]string, len(t.path), len(t.path)+1) + copy(modPath, t.path) + modPath = append(modPath, m.Name) - // Split out the subdir if we have one - source, subDir := getter.SourceDirSubdir(m.Source) + log.Printf("[TRACE] module source: %q", m.Source) - source, err := getter.Detect(source, t.config.Dir, getter.Detectors) + // add the module path to help indicate where modules with relative + // paths are being loaded from + s.output(fmt.Sprintf("- module.%s", strings.Join(modPath, "."))) + + // Lookup the local location of the module. + // dir is the local directory where the module is stored + mod, err := s.findRegistryModule(m.Source, m.Version) if err != nil { - return fmt.Errorf("module %s: %s", m.Name, err) + return nil, err } + // The key is the string that will be used to uniquely id the Source in + // the local storage. The prefix digit can be incremented to + // invalidate the local module storage. + key := "1." + t.versionedPathKey(m) + if mod.Version != "" { + key += "." + mod.Version + } + + // Check for the exact key if it's not a registry module + if !mod.registry { + mod.Dir, err = s.findModule(key) + if err != nil { + return nil, err + } + } + + if mod.Dir != "" && s.Mode != GetModeUpdate { + // We found it locally, but in order to load the Tree we need to + // find out if there was another subDir stored from detection. + subDir, err := s.getModuleRoot(mod.Dir) + if err != nil { + // If there's a problem with the subdir record, we'll let the + // recordSubdir method fix it up. Any other filesystem errors + // will turn up again below. + log.Println("[WARN] error reading subdir record:", err) + } + + fullDir := filepath.Join(mod.Dir, subDir) + + child, err := NewTreeModule(m.Name, fullDir) + if err != nil { + return nil, fmt.Errorf("module %s: %s", m.Name, err) + } + child.path = modPath + child.parent = t + child.version = mod.Version + child.source = m.Source + children[m.Name] = child + continue + } + + // Split out the subdir if we have one. + // Terraform keeps the entire requested tree, so that modules can + // reference sibling modules from the same archive or repo. + rawSource, subDir := getter.SourceDirSubdir(m.Source) + + // we haven't found a source, so fallback to the go-getter detectors + source := mod.url + if source == "" { + source, err = getter.Detect(rawSource, t.config.Dir, getter.Detectors) + if err != nil { + return nil, fmt.Errorf("module %s: %s", m.Name, err) + } + } + + log.Printf("[TRACE] detected module source %q", source) + // Check if the detector introduced something new. - source, subDir2 := getter.SourceDirSubdir(source) - if subDir2 != "" { - subDir = filepath.Join(subDir2, subDir) + // For example, the registry always adds a subdir of `//*`, + // indicating that we need to strip off the first component from the + // tar archive, though we may not yet know what it is called. + source, detectedSubDir := getter.SourceDirSubdir(source) + if detectedSubDir != "" { + subDir = filepath.Join(detectedSubDir, subDir) + } + + output := "" + switch s.Mode { + case GetModeUpdate: + output = fmt.Sprintf(" Updating source %q", m.Source) + default: + output = fmt.Sprintf(" Getting source %q", m.Source) } + s.output(output) - // Get the directory where this module is so we can load it - key := strings.Join(path, ".") - key = fmt.Sprintf("root.%s-%s", key, m.Source) - dir, ok, err := getStorage(s, key, source, mode) + dir, ok, err := s.getStorage(key, source) if err != nil { - return err + return nil, err } if !ok { - return fmt.Errorf( - "module %s: not found, may need to be downloaded using 'terraform get'", m.Name) + return nil, fmt.Errorf("module %s: not found, may need to run 'terraform init'", m.Name) } - // If we have a subdirectory, then merge that in + log.Printf("[TRACE] %q stored in %q", source, dir) + + // expand and record the subDir for later + fullDir := dir if subDir != "" { - dir = filepath.Join(dir, subDir) - } + fullDir, err = getter.SubdirGlob(dir, subDir) + if err != nil { + return nil, err + } - // Load the configurations.Dir(source) - children[m.Name], err = NewTreeModule(m.Name, dir) - if err != nil { - return fmt.Errorf( - "module %s: %s", m.Name, err) + // +1 to account for the pathsep + if len(dir)+1 > len(fullDir) { + return nil, fmt.Errorf("invalid module storage path %q", fullDir) + } + subDir = fullDir[len(dir)+1:] } - // Set the path of this child - children[m.Name].path = path - } + // add new info to the module record + mod.Key = key + mod.Dir = dir + mod.Root = subDir - // Go through all the children and load them. - for _, c := range children { - if err := c.Load(s, mode); err != nil { - return err + // record the module in our manifest + if err := s.recordModule(mod); err != nil { + return nil, err } - } - // Set our tree up - t.children = children + child, err := NewTreeModule(m.Name, fullDir) + if err != nil { + return nil, fmt.Errorf("module %s: %s", m.Name, err) + } + child.path = modPath + child.parent = t + child.version = mod.Version + child.source = m.Source + children[m.Name] = child + } - return nil + return children, nil } // Path is the full path to this tree. @@ -272,32 +385,35 @@ func (t *Tree) String() string { // as verifying things such as parameters/outputs between the various modules. // // Load must be called prior to calling Validate or an error will be returned. -func (t *Tree) Validate() error { +func (t *Tree) Validate() tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + if !t.Loaded() { - return fmt.Errorf("tree must be loaded before calling Validate") + diags = diags.Append(fmt.Errorf( + "tree must be loaded before calling Validate", + )) + return diags } - // If something goes wrong, here is our error template - newErr := &treeError{Name: []string{t.Name()}} - // Terraform core does not handle root module children named "root". // We plan to fix this in the future but this bug was brought up in // the middle of a release and we don't want to introduce wide-sweeping // changes at that time. if len(t.path) == 1 && t.name == "root" { - return fmt.Errorf("root module cannot contain module named 'root'") + diags = diags.Append(fmt.Errorf( + "root module cannot contain module named 'root'", + )) + return diags } // Validate our configuration first. - if err := t.config.Validate(); err != nil { - newErr.Add(err) - } + diags = diags.Append(t.config.Validate()) // If we're the root, we do extra validation. This validation usually // requires the entire tree (since children don't have parent pointers). if len(t.path) == 0 { if err := t.validateProviderAlias(); err != nil { - newErr.Add(err) + diags = diags.Append(err) } } @@ -306,20 +422,11 @@ func (t *Tree) Validate() error { // Validate all our children for _, c := range children { - err := c.Validate() - if err == nil { + childDiags := c.Validate() + diags = diags.Append(childDiags) + if diags.HasErrors() { continue } - - verr, ok := err.(*treeError) - if !ok { - // Unknown error, just return... - return err - } - - // Append ourselves to the error and then return - verr.Name = append(verr.Name, t.Name()) - newErr.AddChild(verr) } // Go over all the modules and verify that any parameters are valid @@ -345,9 +452,10 @@ func (t *Tree) Validate() error { // Compare to the keys in our raw config for the module for k, _ := range m.RawConfig.Raw { if _, ok := varMap[k]; !ok { - newErr.Add(fmt.Errorf( - "module %s: %s is not a valid parameter", - m.Name, k)) + diags = diags.Append(fmt.Errorf( + "module %q: %q is not a valid argument", + m.Name, k, + )) } // Remove the required @@ -356,9 +464,10 @@ func (t *Tree) Validate() error { // If we have any required left over, they aren't set. for k, _ := range requiredMap { - newErr.Add(fmt.Errorf( - "module %s: required variable %q not set", - m.Name, k)) + diags = diags.Append(fmt.Errorf( + "module %q: missing required argument %q", + m.Name, k, + )) } } @@ -373,9 +482,10 @@ func (t *Tree) Validate() error { tree, ok := children[mv.Name] if !ok { - newErr.Add(fmt.Errorf( - "%s: undefined module referenced %s", - source, mv.Name)) + diags = diags.Append(fmt.Errorf( + "%s: reference to undefined module %q", + source, mv.Name, + )) continue } @@ -387,14 +497,56 @@ func (t *Tree) Validate() error { } } if !found { - newErr.Add(fmt.Errorf( - "%s: %s is not a valid output for module %s", - source, mv.Field, mv.Name)) + diags = diags.Append(fmt.Errorf( + "%s: %q is not a valid output for module %q", + source, mv.Field, mv.Name, + )) } } } - return newErr.ErrOrNil() + return diags +} + +// versionedPathKey returns a path string with every levels full name, version +// and source encoded. This is to provide a unique key for our module storage, +// since submodules need to know which versions of their ancestor modules they +// are loaded from. +// For example, if module A has a subdirectory B, if module A's source or +// version is updated B's storage key must reflect this change in order for the +// correct version of B's source to be loaded. +func (t *Tree) versionedPathKey(m *Module) string { + path := make([]string, len(t.path)+1) + path[len(path)-1] = m.Name + ";" + m.Source + // We're going to load these in order for easier reading and debugging, but + // in practice they only need to be unique and consistent. + + p := t + i := len(path) - 2 + for ; i >= 0; i-- { + if p == nil { + break + } + // we may have been loaded under a blank Tree, so always check for a name + // too. + if p.name == "" { + break + } + seg := p.name + if p.version != "" { + seg += "#" + p.version + } + + if p.source != "" { + seg += ";" + p.source + } + + path[i] = seg + p = p.parent + } + + key := strings.Join(path, "|") + return key } // treeError is an error use by Tree.Validate to accumulates all diff --git a/vendor/github.com/hashicorp/terraform/config/module/validate_provider_alias.go b/vendor/github.com/hashicorp/terraform/config/module/validate_provider_alias.go index 090d4f7..f203556 100644 --- a/vendor/github.com/hashicorp/terraform/config/module/validate_provider_alias.go +++ b/vendor/github.com/hashicorp/terraform/config/module/validate_provider_alias.go @@ -67,7 +67,7 @@ func (t *Tree) validateProviderAlias() error { // We didn't find the alias, error! err = multierror.Append(err, fmt.Errorf( - "module %s: provider alias must be defined by the module or a parent: %s", + "module %s: provider alias must be defined by the module: %s", strings.Join(pv.Path, "."), k)) } } diff --git a/vendor/github.com/hashicorp/terraform/config/module/versions.go b/vendor/github.com/hashicorp/terraform/config/module/versions.go new file mode 100644 index 0000000..8348d4b --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/config/module/versions.go @@ -0,0 +1,95 @@ +package module + +import ( + "errors" + "fmt" + "sort" + + version "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform/registry/response" +) + +const anyVersion = ">=0.0.0" + +// return the newest version that satisfies the provided constraint +func newest(versions []string, constraint string) (string, error) { + if constraint == "" { + constraint = anyVersion + } + cs, err := version.NewConstraint(constraint) + if err != nil { + return "", err + } + + switch len(versions) { + case 0: + return "", errors.New("no versions found") + case 1: + v, err := version.NewVersion(versions[0]) + if err != nil { + return "", err + } + + if !cs.Check(v) { + return "", fmt.Errorf("no version found matching constraint %q", constraint) + } + return versions[0], nil + } + + sort.Slice(versions, func(i, j int) bool { + // versions should have already been validated + // sort invalid version strings to the end + iv, err := version.NewVersion(versions[i]) + if err != nil { + return true + } + jv, err := version.NewVersion(versions[j]) + if err != nil { + return true + } + return iv.GreaterThan(jv) + }) + + // versions are now in order, so just find the first which satisfies the + // constraint + for i := range versions { + v, err := version.NewVersion(versions[i]) + if err != nil { + continue + } + if cs.Check(v) { + return versions[i], nil + } + } + + return "", nil +} + +// return the newest *moduleVersion that matches the given constraint +// TODO: reconcile these two types and newest* functions +func newestVersion(moduleVersions []*response.ModuleVersion, constraint string) (*response.ModuleVersion, error) { + var versions []string + modules := make(map[string]*response.ModuleVersion) + + for _, m := range moduleVersions { + versions = append(versions, m.Version) + modules[m.Version] = m + } + + match, err := newest(versions, constraint) + return modules[match], err +} + +// return the newest moduleRecord that matches the given constraint +func newestRecord(moduleVersions []moduleRecord, constraint string) (moduleRecord, error) { + var versions []string + modules := make(map[string]moduleRecord) + + for _, m := range moduleVersions { + versions = append(versions, m.Version) + modules[m.Version] = m + } + + match, err := newest(versions, constraint) + return modules[match], err +} diff --git a/vendor/github.com/hashicorp/terraform/config/raw_config.go b/vendor/github.com/hashicorp/terraform/config/raw_config.go index f8498d8..1854a8b 100644 --- a/vendor/github.com/hashicorp/terraform/config/raw_config.go +++ b/vendor/github.com/hashicorp/terraform/config/raw_config.go @@ -3,8 +3,14 @@ package config import ( "bytes" "encoding/gob" + "errors" + "strconv" "sync" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" + + hcl2 "github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/hil" "github.com/hashicorp/hil/ast" "github.com/mitchellh/copystructure" @@ -27,8 +33,24 @@ const UnknownVariableValue = "74D93920-ED26-11E3-AC10-0800200C9A66" // RawConfig supports a query-like interface to request // information from deep within the structure. type RawConfig struct { - Key string - Raw map[string]interface{} + Key string + + // Only _one_ of Raw and Body may be populated at a time. + // + // In the normal case, Raw is populated and Body is nil. + // + // When the experimental HCL2 parsing mode is enabled, "Body" + // is populated and RawConfig serves only to transport the hcl2.Body + // through the rest of Terraform core so we can ultimately decode it + // once its schema is known. + // + // Once we transition to HCL2 as the primary representation, RawConfig + // should be removed altogether and the hcl2.Body should be passed + // around directly. + + Raw map[string]interface{} + Body hcl2.Body + Interpolations []ast.Node Variables map[string]InterpolatedVariable @@ -48,6 +70,26 @@ func NewRawConfig(raw map[string]interface{}) (*RawConfig, error) { return result, nil } +// NewRawConfigHCL2 creates a new RawConfig that is serving as a capsule +// to transport a hcl2.Body. In this mode, the publicly-readable struct +// fields are not populated since all operations should instead be diverted +// to the HCL2 body. +// +// For a RawConfig object constructed with this function, the only valid use +// is to later retrieve the Body value and call its own methods. Callers +// may choose to set and then later handle the Key field, in a manner +// consistent with how it is handled by the Value method, but the Value +// method itself must not be used. +// +// This is an experimental codepath to be used only by the HCL2 config loader. +// Non-experimental parsing should _always_ use NewRawConfig to produce a +// fully-functional RawConfig object. +func NewRawConfigHCL2(body hcl2.Body) *RawConfig { + return &RawConfig{ + Body: body, + } +} + // RawMap returns a copy of the RawConfig.Raw map. func (r *RawConfig) RawMap() map[string]interface{} { r.lock.Lock() @@ -69,6 +111,10 @@ func (r *RawConfig) Copy() *RawConfig { r.lock.Lock() defer r.lock.Unlock() + if r.Body != nil { + return NewRawConfigHCL2(r.Body) + } + newRaw := make(map[string]interface{}) for k, v := range r.Raw { newRaw[k] = v @@ -223,6 +269,13 @@ func (r *RawConfig) init() error { } func (r *RawConfig) interpolate(fn interpolationWalkerFunc) error { + if r.Body != nil { + // For RawConfigs created for the HCL2 experiement, callers must + // use the HCL2 Body API directly rather than interpolating via + // the RawConfig. + return errors.New("this feature is not yet supported under the HCL2 experiment") + } + config, err := copystructure.Copy(r.Raw) if err != nil { return err @@ -268,6 +321,74 @@ func (r *RawConfig) merge(r2 *RawConfig) *RawConfig { return result } +// couldBeInteger is a helper that determines if the represented value could +// result in an integer. +// +// This function only works for RawConfigs that have "Key" set, meaning that +// a single result can be produced. Calling this function will overwrite +// the Config and Value results to be a test value. +// +// This function is conservative. If there is some doubt about whether the +// result could be an integer -- for example, if it depends on a variable +// whose type we don't know yet -- it will still return true. +func (r *RawConfig) couldBeInteger() bool { + if r.Key == "" { + // un-keyed RawConfigs can never produce numbers + return false + } + if r.Body == nil { + // Normal path: using the interpolator in this package + // Interpolate with a fixed number to verify that its a number. + r.interpolate(func(root ast.Node) (interface{}, error) { + // Execute the node but transform the AST so that it returns + // a fixed value of "5" for all interpolations. + result, err := hil.Eval( + hil.FixedValueTransform( + root, &ast.LiteralNode{Value: "5", Typex: ast.TypeString}), + nil) + if err != nil { + return "", err + } + + return result.Value, nil + }) + _, err := strconv.ParseInt(r.Value().(string), 0, 0) + return err == nil + } else { + // HCL2 experiment path: using the HCL2 API via shims + // + // This path catches fewer situations because we have to assume all + // variables are entirely unknown in HCL2, rather than the assumption + // above that all variables can be numbers because names like "var.foo" + // are considered a single variable rather than an attribute access. + // This is fine in practice, because we get a definitive answer + // during the graph walk when we have real values to work with. + attrs, diags := r.Body.JustAttributes() + if diags.HasErrors() { + // This body is not just a single attribute with a value, so + // this can't be a number. + return false + } + attr, hasAttr := attrs[r.Key] + if !hasAttr { + return false + } + result, diags := hcl2EvalWithUnknownVars(attr.Expr) + if diags.HasErrors() { + // We'll conservatively assume that this error is a result of + // us not being ready to fully-populate the scope, and catch + // any further problems during the main graph walk. + return true + } + + // If the result is convertable to number then we'll allow it. + // We do this because an unknown string is optimistically convertable + // to number (might be "5") but a _known_ string "hello" is not. + _, err := convert.Convert(result, cty.Number) + return err == nil + } +} + // UnknownKeys returns the keys of the configuration that are unknown // because they had interpolated variables that must be computed. func (r *RawConfig) UnknownKeys() []string { diff --git a/vendor/github.com/hashicorp/terraform/config/resource_mode_string.go b/vendor/github.com/hashicorp/terraform/config/resource_mode_string.go index ea68b4f..8a55e06 100644 --- a/vendor/github.com/hashicorp/terraform/config/resource_mode_string.go +++ b/vendor/github.com/hashicorp/terraform/config/resource_mode_string.go @@ -2,7 +2,7 @@ package config -import "fmt" +import "strconv" const _ResourceMode_name = "ManagedResourceModeDataResourceMode" @@ -10,7 +10,7 @@ var _ResourceMode_index = [...]uint8{0, 19, 35} func (i ResourceMode) String() string { if i < 0 || i >= ResourceMode(len(_ResourceMode_index)-1) { - return fmt.Sprintf("ResourceMode(%d)", i) + return "ResourceMode(" + strconv.FormatInt(int64(i), 10) + ")" } return _ResourceMode_name[_ResourceMode_index[i]:_ResourceMode_index[i+1]] } diff --git a/vendor/github.com/hashicorp/terraform/config/testing.go b/vendor/github.com/hashicorp/terraform/config/testing.go index f7bfadd..831fc77 100644 --- a/vendor/github.com/hashicorp/terraform/config/testing.go +++ b/vendor/github.com/hashicorp/terraform/config/testing.go @@ -6,6 +6,8 @@ import ( // TestRawConfig is used to create a RawConfig for testing. func TestRawConfig(t *testing.T, c map[string]interface{}) *RawConfig { + t.Helper() + cfg, err := NewRawConfig(c) if err != nil { t.Fatalf("err: %s", err) -- cgit v1.2.3