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  }