github.com/terraform-linters/tflint@v0.51.2-0.20240520175844-3750771571b6/terraform/variable.go (about) 1 package terraform 2 3 import ( 4 "fmt" 5 6 "github.com/hashicorp/hcl/v2" 7 "github.com/hashicorp/hcl/v2/ext/typeexpr" 8 "github.com/hashicorp/hcl/v2/gohcl" 9 "github.com/hashicorp/hcl/v2/hclsyntax" 10 "github.com/terraform-linters/tflint-plugin-sdk/hclext" 11 "github.com/zclconf/go-cty/cty" 12 "github.com/zclconf/go-cty/cty/convert" 13 ) 14 15 type Variable struct { 16 Name string 17 Default cty.Value 18 19 Type cty.Type 20 ConstraintType cty.Type 21 TypeDefaults *typeexpr.Defaults 22 23 DeclRange hcl.Range 24 25 ParsingMode VariableParsingMode 26 Sensitive bool 27 Nullable bool 28 } 29 30 func decodeVairableBlock(block *hclext.Block) (*Variable, hcl.Diagnostics) { 31 v := &Variable{ 32 Name: block.Labels[0], 33 Type: cty.DynamicPseudoType, 34 ConstraintType: cty.DynamicPseudoType, 35 ParsingMode: VariableParseLiteral, 36 DeclRange: block.DefRange, 37 } 38 diags := hcl.Diagnostics{} 39 40 if attr, exists := block.Body.Attributes["type"]; exists { 41 ty, tyDefaults, parseMode, tyDiags := decodeVariableType(attr.Expr) 42 diags = diags.Extend(tyDiags) 43 v.ConstraintType = ty 44 v.TypeDefaults = tyDefaults 45 v.Type = ty.WithoutOptionalAttributesDeep() 46 v.ParsingMode = parseMode 47 } 48 49 if attr, exists := block.Body.Attributes["sensitive"]; exists { 50 valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Sensitive) 51 diags = diags.Extend(valDiags) 52 } 53 54 if attr, exists := block.Body.Attributes["nullable"]; exists { 55 valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Nullable) 56 diags = append(diags, valDiags...) 57 } else { 58 // The current default is true, which is subject to change in a future 59 // language edition. 60 v.Nullable = true 61 } 62 63 if attr, exists := block.Body.Attributes["default"]; exists { 64 val, valDiags := attr.Expr.Value(nil) 65 diags = diags.Extend(valDiags) 66 67 if v.ConstraintType != cty.NilType { 68 var err error 69 // defaults to the variable default value before type conversion, 70 // unless the default value is null. Null is excluded from the 71 // type default application process as a special case, to allow 72 // nullable variables to have a null default value. 73 if v.TypeDefaults != nil && !val.IsNull() { 74 val = v.TypeDefaults.Apply(val) 75 } 76 val, err = convert.Convert(val, v.ConstraintType) 77 if err != nil { 78 diags = append(diags, &hcl.Diagnostic{ 79 Severity: hcl.DiagError, 80 Summary: "Invalid default value for variable", 81 Detail: fmt.Sprintf("This default value is not compatible with the variable's type constraint: %s.", err), 82 Subject: attr.Expr.Range().Ptr(), 83 }) 84 val = cty.DynamicVal 85 } 86 } 87 88 v.Default = val 89 } 90 91 return v, diags 92 } 93 94 func decodeVariableType(expr hcl.Expression) (cty.Type, *typeexpr.Defaults, VariableParsingMode, hcl.Diagnostics) { 95 if exprIsNativeQuotedString(expr) { 96 // If a user provides the pre-0.12 form of variable type argument where 97 // the string values "string", "list" and "map" are accepted, we 98 // provide an error to point the user towards using the type system 99 // correctly has a hint. 100 // Only the native syntax ends up in this codepath; we handle the 101 // JSON syntax (which is, of course, quoted within the type system) 102 // in the normal codepath below. 103 val, diags := expr.Value(nil) 104 if diags.HasErrors() { 105 return cty.DynamicPseudoType, nil, VariableParseHCL, diags 106 } 107 str := val.AsString() 108 switch str { 109 case "string": 110 diags = append(diags, &hcl.Diagnostic{ 111 Severity: hcl.DiagError, 112 Summary: "Invalid quoted type constraints", 113 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. Remove the quotes around \"string\".", 114 Subject: expr.Range().Ptr(), 115 }) 116 return cty.DynamicPseudoType, nil, VariableParseLiteral, diags 117 case "list": 118 diags = append(diags, &hcl.Diagnostic{ 119 Severity: hcl.DiagError, 120 Summary: "Invalid quoted type constraints", 121 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. Remove the quotes around \"list\" and write list(string) instead to explicitly indicate that the list elements are strings.", 122 Subject: expr.Range().Ptr(), 123 }) 124 return cty.DynamicPseudoType, nil, VariableParseHCL, diags 125 case "map": 126 diags = append(diags, &hcl.Diagnostic{ 127 Severity: hcl.DiagError, 128 Summary: "Invalid quoted type constraints", 129 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. Remove the quotes around \"map\" and write map(string) instead to explicitly indicate that the map elements are strings.", 130 Subject: expr.Range().Ptr(), 131 }) 132 return cty.DynamicPseudoType, nil, VariableParseHCL, diags 133 default: 134 return cty.DynamicPseudoType, nil, VariableParseHCL, hcl.Diagnostics{{ 135 Severity: hcl.DiagError, 136 Summary: "Invalid legacy variable type hint", 137 Detail: `To provide a full type expression, remove the surrounding quotes and give the type expression directly.`, 138 Subject: expr.Range().Ptr(), 139 }} 140 } 141 } 142 143 // First we'll deal with some shorthand forms that the HCL-level type 144 // expression parser doesn't include. These both emulate pre-0.12 behavior 145 // of allowing a list or map of any element type as long as all of the 146 // elements are consistent. This is the same as list(any) or map(any). 147 switch hcl.ExprAsKeyword(expr) { 148 case "list": 149 return cty.List(cty.DynamicPseudoType), nil, VariableParseHCL, nil 150 case "map": 151 return cty.Map(cty.DynamicPseudoType), nil, VariableParseHCL, nil 152 } 153 154 ty, typeDefaults, diags := typeexpr.TypeConstraintWithDefaults(expr) 155 if diags.HasErrors() { 156 return cty.DynamicPseudoType, nil, VariableParseHCL, diags 157 } 158 159 switch { 160 case ty.IsPrimitiveType(): 161 // Primitive types use literal parsing. 162 return ty, typeDefaults, VariableParseLiteral, diags 163 default: 164 // Everything else uses HCL parsing 165 return ty, typeDefaults, VariableParseHCL, diags 166 } 167 } 168 169 // VariableParsingMode defines how values of a particular variable given by 170 // text-only mechanisms (command line arguments and environment variables) 171 // should be parsed to produce the final value. 172 type VariableParsingMode rune 173 174 // VariableParseLiteral is a variable parsing mode that just takes the given 175 // string directly as a cty.String value. 176 const VariableParseLiteral VariableParsingMode = 'L' 177 178 // VariableParseHCL is a variable parsing mode that attempts to parse the given 179 // string as an HCL expression and returns the result. 180 const VariableParseHCL VariableParsingMode = 'H' 181 182 // Parse uses the receiving parsing mode to process the given variable value 183 // string, returning the result along with any diagnostics. 184 // 185 // A VariableParsingMode does not know the expected type of the corresponding 186 // variable, so it's the caller's responsibility to attempt to convert the 187 // result to the appropriate type and return to the user any diagnostics that 188 // conversion may produce. 189 // 190 // The given name is used to create a synthetic filename in case any diagnostics 191 // must be generated about the given string value. This should be the name 192 // of the root module variable whose value will be populated from the given 193 // string. 194 // 195 // If the returned diagnostics has errors, the returned value may not be 196 // valid. 197 func (m VariableParsingMode) Parse(name, value string) (cty.Value, hcl.Diagnostics) { 198 switch m { 199 case VariableParseLiteral: 200 return cty.StringVal(value), nil 201 case VariableParseHCL: 202 fakeFilename := fmt.Sprintf("<value for var.%s>", name) 203 expr, diags := hclsyntax.ParseExpression([]byte(value), fakeFilename, hcl.Pos{Line: 1, Column: 1}) 204 if diags.HasErrors() { 205 return cty.DynamicVal, diags 206 } 207 val, valDiags := expr.Value(nil) 208 diags = append(diags, valDiags...) 209 return val, diags 210 default: 211 // Should never happen 212 panic(fmt.Errorf("Parse called on invalid VariableParsingMode %#v", m)) 213 } 214 } 215 216 func exprIsNativeQuotedString(expr hcl.Expression) bool { 217 _, ok := expr.(*hclsyntax.TemplateExpr) 218 return ok 219 } 220 221 var variableBlockSchema = &hclext.BodySchema{ 222 Attributes: []hclext.AttributeSchema{ 223 { 224 Name: "default", 225 }, 226 { 227 Name: "type", 228 }, 229 { 230 Name: "sensitive", 231 }, 232 { 233 Name: "nullable", 234 }, 235 }, 236 }