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  }