package configs import ( "fmt" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/ext/typeexpr" "github.com/hashicorp/hcl/v2/gohcl" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/convert" "github.com/hashicorp/terraform/addrs" ) // A consistent detail message for all "not a valid identifier" diagnostics. const badIdentifierDetail = "A name must start with a letter or underscore and may contain only letters, digits, underscores, and dashes." // Variable represents a "variable" block in a module or file. type Variable struct { Name string Description string Default cty.Value Type cty.Type ParsingMode VariableParsingMode DescriptionSet bool DeclRange hcl.Range } func decodeVariableBlock(block *hcl.Block, override bool) (*Variable, hcl.Diagnostics) { v := &Variable{ Name: block.Labels[0], DeclRange: block.DefRange, } // Unless we're building an override, we'll set some defaults // which we might override with attributes below. We leave these // as zero-value in the override case so we can recognize whether // or not they are set when we merge. if !override { v.Type = cty.DynamicPseudoType v.ParsingMode = VariableParseLiteral } content, diags := block.Body.Content(variableBlockSchema) if !hclsyntax.ValidIdentifier(v.Name) { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid variable name", Detail: badIdentifierDetail, Subject: &block.LabelRanges[0], }) } // Don't allow declaration of variables that would conflict with the // reserved attribute and block type names in a "module" block, since // these won't be usable for child modules. for _, attr := range moduleBlockSchema.Attributes { if attr.Name == v.Name { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid variable name", Detail: fmt.Sprintf("The variable name %q is reserved due to its special meaning inside module blocks.", attr.Name), Subject: &block.LabelRanges[0], }) } } for _, blockS := range moduleBlockSchema.Blocks { if blockS.Type == v.Name { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid variable name", Detail: fmt.Sprintf("The variable name %q is reserved due to its special meaning inside module blocks.", blockS.Type), Subject: &block.LabelRanges[0], }) } } if attr, exists := content.Attributes["description"]; exists { valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Description) diags = append(diags, valDiags...) v.DescriptionSet = true } if attr, exists := content.Attributes["type"]; exists { ty, parseMode, tyDiags := decodeVariableType(attr.Expr) diags = append(diags, tyDiags...) v.Type = ty v.ParsingMode = parseMode } if attr, exists := content.Attributes["default"]; exists { val, valDiags := attr.Expr.Value(nil) diags = append(diags, valDiags...) // Convert the default to the expected type so we can catch invalid // defaults early and allow later code to assume validity. // Note that this depends on us having already processed any "type" // attribute above. // However, we can't do this if we're in an override file where // the type might not be set; we'll catch that during merge. if v.Type != cty.NilType { var err error val, err = convert.Convert(val, v.Type) if err != nil { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid default value for variable", Detail: fmt.Sprintf("This default value is not compatible with the variable's type constraint: %s.", err), Subject: attr.Expr.Range().Ptr(), }) val = cty.DynamicVal } } v.Default = val } return v, diags } func decodeVariableType(expr hcl.Expression) (cty.Type, VariableParsingMode, hcl.Diagnostics) { if exprIsNativeQuotedString(expr) { // Here we're accepting the pre-0.12 form of variable type argument where // the string values "string", "list" and "map" are accepted has a hint // about the type used primarily for deciding how to parse values // given on the command line and in environment variables. // Only the native syntax ends up in this codepath; we handle the // JSON syntax (which is, of course, quoted even in the new format) // in the normal codepath below. val, diags := expr.Value(nil) if diags.HasErrors() { return cty.DynamicPseudoType, VariableParseHCL, diags } str := val.AsString() switch str { case "string": diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagWarning, Summary: "Quoted type constraints are deprecated", Detail: "Terraform 0.11 and earlier required type constraints to be given in quotes, but that form is now deprecated and will be removed in a future version of Terraform. To silence this warning, remove the quotes around \"string\".", Subject: expr.Range().Ptr(), }) return cty.String, VariableParseLiteral, diags case "list": diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagWarning, Summary: "Quoted type constraints are deprecated", Detail: "Terraform 0.11 and earlier required type constraints to be given in quotes, but that form is now deprecated and will be removed in a future version of Terraform. To silence this warning, remove the quotes around \"list\" and write list(string) instead to explicitly indicate that the list elements are strings.", Subject: expr.Range().Ptr(), }) return cty.List(cty.DynamicPseudoType), VariableParseHCL, diags case "map": diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagWarning, Summary: "Quoted type constraints are deprecated", Detail: "Terraform 0.11 and earlier required type constraints to be given in quotes, but that form is now deprecated and will be removed in a future version of Terraform. To silence this warning, remove the quotes around \"map\" and write map(string) instead to explicitly indicate that the map elements are strings.", Subject: expr.Range().Ptr(), }) return cty.Map(cty.DynamicPseudoType), VariableParseHCL, diags default: return cty.DynamicPseudoType, VariableParseHCL, hcl.Diagnostics{{ Severity: hcl.DiagError, Summary: "Invalid legacy variable type hint", Detail: `The legacy variable type hint form, using a quoted string, allows only the values "string", "list", and "map". To provide a full type expression, remove the surrounding quotes and give the type expression directly.`, Subject: expr.Range().Ptr(), }} } } // First we'll deal with some shorthand forms that the HCL-level type // expression parser doesn't include. These both emulate pre-0.12 behavior // of allowing a list or map of any element type as long as all of the // elements are consistent. This is the same as list(any) or map(any). switch hcl.ExprAsKeyword(expr) { case "list": return cty.List(cty.DynamicPseudoType), VariableParseHCL, nil case "map": return cty.Map(cty.DynamicPseudoType), VariableParseHCL, nil } ty, diags := typeexpr.TypeConstraint(expr) if diags.HasErrors() { return cty.DynamicPseudoType, VariableParseHCL, diags } switch { case ty.IsPrimitiveType(): // Primitive types use literal parsing. return ty, VariableParseLiteral, diags default: // Everything else uses HCL parsing return ty, VariableParseHCL, diags } } // Required returns true if this variable is required to be set by the caller, // or false if there is a default value that will be used when it isn't set. func (v *Variable) Required() bool { return v.Default == cty.NilVal } // VariableParsingMode defines how values of a particular variable given by // text-only mechanisms (command line arguments and environment variables) // should be parsed to produce the final value. type VariableParsingMode rune // VariableParseLiteral is a variable parsing mode that just takes the given // string directly as a cty.String value. const VariableParseLiteral VariableParsingMode = 'L' // VariableParseHCL is a variable parsing mode that attempts to parse the given // string as an HCL expression and returns the result. const VariableParseHCL VariableParsingMode = 'H' // Parse uses the receiving parsing mode to process the given variable value // string, returning the result along with any diagnostics. // // A VariableParsingMode does not know the expected type of the corresponding // variable, so it's the caller's responsibility to attempt to convert the // result to the appropriate type and return to the user any diagnostics that // conversion may produce. // // The given name is used to create a synthetic filename in case any diagnostics // must be generated about the given string value. This should be the name // of the root module variable whose value will be populated from the given // string. // // If the returned diagnostics has errors, the returned value may not be // valid. func (m VariableParsingMode) Parse(name, value string) (cty.Value, hcl.Diagnostics) { switch m { case VariableParseLiteral: return cty.StringVal(value), nil case VariableParseHCL: fakeFilename := fmt.Sprintf("", name) expr, diags := hclsyntax.ParseExpression([]byte(value), fakeFilename, hcl.Pos{Line: 1, Column: 1}) if diags.HasErrors() { return cty.DynamicVal, diags } val, valDiags := expr.Value(nil) diags = append(diags, valDiags...) return val, diags default: // Should never happen panic(fmt.Errorf("Parse called on invalid VariableParsingMode %#v", m)) } } // Output represents an "output" block in a module or file. type Output struct { Name string Description string Expr hcl.Expression DependsOn []hcl.Traversal Sensitive bool DescriptionSet bool SensitiveSet bool DeclRange hcl.Range } func decodeOutputBlock(block *hcl.Block, override bool) (*Output, hcl.Diagnostics) { o := &Output{ Name: block.Labels[0], DeclRange: block.DefRange, } schema := outputBlockSchema if override { schema = schemaForOverrides(schema) } content, diags := block.Body.Content(schema) if !hclsyntax.ValidIdentifier(o.Name) { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid output name", Detail: badIdentifierDetail, Subject: &block.LabelRanges[0], }) } if attr, exists := content.Attributes["description"]; exists { valDiags := gohcl.DecodeExpression(attr.Expr, nil, &o.Description) diags = append(diags, valDiags...) o.DescriptionSet = true } if attr, exists := content.Attributes["value"]; exists { o.Expr = attr.Expr } if attr, exists := content.Attributes["sensitive"]; exists { valDiags := gohcl.DecodeExpression(attr.Expr, nil, &o.Sensitive) diags = append(diags, valDiags...) o.SensitiveSet = true } if attr, exists := content.Attributes["depends_on"]; exists { deps, depsDiags := decodeDependsOn(attr) diags = append(diags, depsDiags...) o.DependsOn = append(o.DependsOn, deps...) } return o, diags } // Local represents a single entry from a "locals" block in a module or file. // The "locals" block itself is not represented, because it serves only to // provide context for us to interpret its contents. type Local struct { Name string Expr hcl.Expression DeclRange hcl.Range } func decodeLocalsBlock(block *hcl.Block) ([]*Local, hcl.Diagnostics) { attrs, diags := block.Body.JustAttributes() if len(attrs) == 0 { return nil, diags } locals := make([]*Local, 0, len(attrs)) for name, attr := range attrs { if !hclsyntax.ValidIdentifier(name) { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid local value name", Detail: badIdentifierDetail, Subject: &attr.NameRange, }) } locals = append(locals, &Local{ Name: name, Expr: attr.Expr, DeclRange: attr.Range, }) } return locals, diags } // Addr returns the address of the local value declared by the receiver, // relative to its containing module. func (l *Local) Addr() addrs.LocalValue { return addrs.LocalValue{ Name: l.Name, } } var variableBlockSchema = &hcl.BodySchema{ Attributes: []hcl.AttributeSchema{ { Name: "description", }, { Name: "default", }, { Name: "type", }, }, } var outputBlockSchema = &hcl.BodySchema{ Attributes: []hcl.AttributeSchema{ { Name: "description", }, { Name: "value", Required: true, }, { Name: "depends_on", }, { Name: "sensitive", }, }, }