github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/steampipeconfig/modconfig/var_config/named_values.go (about) 1 package var_config 2 3 // github.com/hashicorp/terraform/configs/parser_config.go 4 import ( 5 "fmt" 6 "unicode" 7 8 "github.com/hashicorp/hcl/v2" 9 "github.com/hashicorp/hcl/v2/gohcl" 10 "github.com/hashicorp/hcl/v2/hclsyntax" 11 "github.com/turbot/pipe-fittings/hclhelpers" 12 "github.com/turbot/steampipe/pkg/steampipeconfig/inputvars/typeexpr" 13 "github.com/zclconf/go-cty/cty" 14 "github.com/zclconf/go-cty/cty/convert" 15 ) 16 17 // A consistent detail message for all "not a valid identifier" diagnostics. 18 const badIdentifierDetail = "A name must start with a letter or underscore and may contain only letters, digits, underscores, and dashes." 19 20 // Variable represents a "variable" block in a module or file. 21 type Variable struct { 22 Name string 23 Description string 24 Default cty.Value 25 Type cty.Type 26 ParsingMode VariableParsingMode 27 //Validations []*VariableValidation 28 //Sensitive bool 29 30 DescriptionSet bool 31 //SensitiveSet bool 32 33 DeclRange hcl.Range 34 } 35 36 func DecodeVariableBlock(block *hcl.Block, content *hcl.BodyContent, override bool) (*Variable, hcl.Diagnostics) { 37 v := &Variable{ 38 Name: block.Labels[0], 39 DeclRange: hclhelpers.BlockRange(block), 40 } 41 var diags hcl.Diagnostics 42 43 // Unless we're building an override, we'll set some defaults 44 // which we might override with attributes below. We leave these 45 // as zero-value in the override case so we can recognize whether 46 // or not they are set when we merge. 47 if !override { 48 v.Type = cty.DynamicPseudoType 49 v.ParsingMode = VariableParseLiteral 50 } 51 52 if !hclsyntax.ValidIdentifier(v.Name) { 53 diags = append(diags, &hcl.Diagnostic{ 54 Severity: hcl.DiagError, 55 Summary: "Invalid variable name", 56 Detail: badIdentifierDetail, 57 Subject: &block.LabelRanges[0], 58 }) 59 } 60 61 if attr, exists := content.Attributes["description"]; exists { 62 valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Description) 63 diags = append(diags, valDiags...) 64 v.DescriptionSet = true 65 } 66 67 if attr, exists := content.Attributes["type"]; exists { 68 ty, parseMode, tyDiags := decodeVariableType(attr.Expr) 69 diags = append(diags, tyDiags...) 70 v.Type = ty 71 v.ParsingMode = parseMode 72 } 73 if attr, exists := content.Attributes["default"]; exists { 74 val, valDiags := attr.Expr.Value(nil) 75 diags = append(diags, valDiags...) 76 77 // Convert the default to the expected type so we can catch invalid 78 // defaults early and allow later code to assume validity. 79 // Note that this depends on us having already processed any "type" 80 // attribute above. 81 // However, we can't do this if we're in an override file where 82 // the type might not be set; we'll catch that during merge. 83 if v.Type != cty.NilType { 84 var err error 85 val, err = convert.Convert(val, v.Type) 86 if err != nil { 87 diags = append(diags, &hcl.Diagnostic{ 88 Severity: hcl.DiagError, 89 Summary: "Invalid default value for variable", 90 Detail: fmt.Sprintf("This default value is not compatible with the variable's type constraint: %s.", err), 91 Subject: attr.Expr.Range().Ptr(), 92 }) 93 val = cty.DynamicVal 94 } 95 } 96 97 v.Default = val 98 } 99 100 for _, block := range content.Blocks { 101 switch block.Type { 102 103 default: 104 // The above cases should be exhaustive for all block types 105 // defined in variableBlockSchema 106 panic(fmt.Sprintf("unhandled block type %q", block.Type)) 107 } 108 } 109 110 return v, diags 111 } 112 113 func decodeVariableType(expr hcl.Expression) (cty.Type, VariableParsingMode, hcl.Diagnostics) { 114 if exprIsNativeQuotedString(expr) { 115 val, diags := expr.Value(nil) 116 if diags.HasErrors() { 117 return cty.DynamicPseudoType, VariableParseHCL, diags 118 } 119 str := val.AsString() 120 switch str { 121 case "string": 122 diags = append(diags, &hcl.Diagnostic{ 123 Severity: hcl.DiagError, 124 Summary: "Invalid quoted type constraints", 125 Subject: expr.Range().Ptr(), 126 }) 127 return cty.DynamicPseudoType, VariableParseLiteral, diags 128 case "list": 129 diags = append(diags, &hcl.Diagnostic{ 130 Severity: hcl.DiagError, 131 Summary: "Invalid quoted type constraints", 132 Subject: expr.Range().Ptr(), 133 }) 134 return cty.DynamicPseudoType, VariableParseHCL, diags 135 case "map": 136 diags = append(diags, &hcl.Diagnostic{ 137 Severity: hcl.DiagError, 138 Summary: "Invalid quoted type constraints", 139 Subject: expr.Range().Ptr(), 140 }) 141 return cty.DynamicPseudoType, VariableParseHCL, diags 142 default: 143 return cty.DynamicPseudoType, VariableParseHCL, hcl.Diagnostics{{ 144 Severity: hcl.DiagError, 145 Summary: "Invalid legacy variable type hint", 146 Subject: expr.Range().Ptr(), 147 }} 148 } 149 } 150 151 // First we'll deal with some shorthand forms that the HCL-level type 152 // expression parser doesn't include. These both emulate pre-0.12 behavior 153 // of allowing a list or map of any element type as long as all of the 154 // elements are consistent. This is the same as list(any) or map(any). 155 switch hcl.ExprAsKeyword(expr) { 156 case "list": 157 return cty.List(cty.DynamicPseudoType), VariableParseHCL, nil 158 case "map": 159 return cty.Map(cty.DynamicPseudoType), VariableParseHCL, nil 160 } 161 162 ty, diags := typeexpr.TypeConstraint(expr) 163 if diags.HasErrors() { 164 return cty.DynamicPseudoType, VariableParseHCL, diags 165 } 166 167 switch { 168 case ty.IsPrimitiveType(): 169 // Primitive types use literal parsing. 170 return ty, VariableParseLiteral, diags 171 default: 172 // Everything else uses HCL parsing 173 return ty, VariableParseHCL, diags 174 } 175 } 176 177 // Required returns true if this variable is required to be set by the caller, 178 // or false if there is a default value that will be used when it isn't set. 179 func (v *Variable) Required() bool { 180 return v.Default == cty.NilVal 181 } 182 183 // VariableParsingMode defines how values of a particular variable given by 184 // text-only mechanisms (command line arguments and environment variables) 185 // should be parsed to produce the final value. 186 type VariableParsingMode rune 187 188 // VariableParseLiteral is a variable parsing mode that just takes the given 189 // string directly as a cty.String value. 190 const VariableParseLiteral VariableParsingMode = 'L' 191 192 // VariableParseHCL is a variable parsing mode that attempts to parse the given 193 // string as an HCL expression and returns the result. 194 const VariableParseHCL VariableParsingMode = 'H' 195 196 // Parse uses the receiving parsing mode to process the given variable value 197 // string, returning the result along with any diagnostics. 198 // 199 // A VariableParsingMode does not know the expected type of the corresponding 200 // variable, so it's the caller's responsibility to attempt to convert the 201 // result to the appropriate type and return to the user any diagnostics that 202 // conversion may produce. 203 // 204 // The given name is used to create a synthetic filename in case any diagnostics 205 // must be generated about the given string value. This should be the name 206 // of the configuration variable whose value will be populated from the given 207 // string. 208 // 209 // If the returned diagnostics has errors, the returned value may not be 210 // valid. 211 func (m VariableParsingMode) Parse(name, value string) (cty.Value, hcl.Diagnostics) { 212 switch m { 213 case VariableParseLiteral: 214 return cty.StringVal(value), nil 215 case VariableParseHCL: 216 fakeFilename := fmt.Sprintf("<value for var.%s>", name) 217 expr, diags := hclsyntax.ParseExpression([]byte(value), fakeFilename, hcl.Pos{Line: 1, Column: 1}) 218 if diags.HasErrors() { 219 return cty.DynamicVal, diags 220 } 221 val, valDiags := expr.Value(nil) 222 diags = append(diags, valDiags...) 223 return val, diags 224 default: 225 // Should never happen 226 panic(fmt.Errorf("parse called on invalid VariableParsingMode %#v", m)) 227 } 228 } 229 230 // VariableValidation represents a configuration-defined validation rule 231 // for a particular input variable, given as a "validation" block inside 232 // a "variable" block. 233 type VariableValidation struct { 234 // Condition is an expression that refers to the variable being tested 235 // and contains no other references. The expression must return true 236 // to indicate that the value is valid or false to indicate that it is 237 // invalid. If the expression produces an error, that's considered a bug 238 // in the module defining the validation rule, not an error in the caller. 239 Condition hcl.Expression 240 241 // ErrorMessage is one or more full sentences, which would need to be in 242 // English for consistency with the rest of the error message output but 243 // can in practice be in any language as long as it ends with a period. 244 // The message should describe what is required for the condition to return 245 // true in a way that would make sense to a caller of the module. 246 ErrorMessage string 247 248 DeclRange hcl.Range 249 } 250 251 // looksLikeSentence is a simple heuristic that encourages writing error 252 // messages that will be presentable when included as part of a larger error diagnostic 253 func looksLikeSentences(s string) bool { 254 if len(s) < 1 { 255 return false 256 } 257 runes := []rune(s) // HCL guarantees that all strings are valid UTF-8 258 first := runes[0] 259 last := runes[len(runes)-1] 260 261 // If the first rune is a letter then it must be an uppercase letter. 262 // (This will only see the first rune in a multi-rune combining sequence, 263 // but the first rune is generally the letter if any are, and if not then 264 // we'll just ignore it because we're primarily expecting English messages 265 // right now anyway) 266 if unicode.IsLetter(first) && !unicode.IsUpper(first) { 267 return false 268 } 269 270 // The string must be at least one full sentence, which implies having 271 // sentence-ending punctuation. 272 return last == '.' || last == '?' || last == '!' 273 }