github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/configs/named_values.go (about)

     1  package configs
     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/zclconf/go-cty/cty"
    11  	"github.com/zclconf/go-cty/cty/convert"
    12  )
    13  
    14  // A consistent detail message for all "not a valid identifier" diagnostics.
    15  const badIdentifierDetail = "A name must start with a letter and may contain only letters, digits, underscores, and dashes."
    16  
    17  // Variable represents a "variable" block in a module or file.
    18  type Variable struct {
    19  	Name        string
    20  	Description string
    21  	Default     cty.Value
    22  	Type        cty.Type
    23  	ParsingMode VariableParsingMode
    24  
    25  	DescriptionSet bool
    26  
    27  	DeclRange hcl.Range
    28  }
    29  
    30  func decodeVariableBlock(block *hcl.Block, override bool) (*Variable, hcl.Diagnostics) {
    31  	v := &Variable{
    32  		Name:      block.Labels[0],
    33  		DeclRange: block.DefRange,
    34  	}
    35  
    36  	// Unless we're building an override, we'll set some defaults
    37  	// which we might override with attributes below. We leave these
    38  	// as zero-value in the override case so we can recognize whether
    39  	// or not they are set when we merge.
    40  	if !override {
    41  		v.Type = cty.DynamicPseudoType
    42  		v.ParsingMode = VariableParseLiteral
    43  	}
    44  
    45  	content, diags := block.Body.Content(variableBlockSchema)
    46  
    47  	if !hclsyntax.ValidIdentifier(v.Name) {
    48  		diags = append(diags, &hcl.Diagnostic{
    49  			Severity: hcl.DiagError,
    50  			Summary:  "Invalid variable name",
    51  			Detail:   badIdentifierDetail,
    52  			Subject:  &block.LabelRanges[0],
    53  		})
    54  	}
    55  
    56  	// Don't allow declaration of variables that would conflict with the
    57  	// reserved attribute and block type names in a "module" block, since
    58  	// these won't be usable for child modules.
    59  	for _, attr := range moduleBlockSchema.Attributes {
    60  		if attr.Name == v.Name {
    61  			diags = append(diags, &hcl.Diagnostic{
    62  				Severity: hcl.DiagError,
    63  				Summary:  "Invalid variable name",
    64  				Detail:   fmt.Sprintf("The variable name %q is reserved due to its special meaning inside module blocks.", attr.Name),
    65  				Subject:  &block.LabelRanges[0],
    66  			})
    67  		}
    68  	}
    69  	for _, blockS := range moduleBlockSchema.Blocks {
    70  		if blockS.Type == v.Name {
    71  			diags = append(diags, &hcl.Diagnostic{
    72  				Severity: hcl.DiagError,
    73  				Summary:  "Invalid variable name",
    74  				Detail:   fmt.Sprintf("The variable name %q is reserved due to its special meaning inside module blocks.", blockS.Type),
    75  				Subject:  &block.LabelRanges[0],
    76  			})
    77  		}
    78  	}
    79  
    80  	if attr, exists := content.Attributes["description"]; exists {
    81  		valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Description)
    82  		diags = append(diags, valDiags...)
    83  		v.DescriptionSet = true
    84  	}
    85  
    86  	if attr, exists := content.Attributes["type"]; exists {
    87  		ty, parseMode, tyDiags := decodeVariableType(attr.Expr)
    88  		diags = append(diags, tyDiags...)
    89  		v.Type = ty
    90  		v.ParsingMode = parseMode
    91  	}
    92  
    93  	if attr, exists := content.Attributes["default"]; exists {
    94  		val, valDiags := attr.Expr.Value(nil)
    95  		diags = append(diags, valDiags...)
    96  
    97  		// Convert the default to the expected type so we can catch invalid
    98  		// defaults early and allow later code to assume validity.
    99  		// Note that this depends on us having already processed any "type"
   100  		// attribute above.
   101  		// However, we can't do this if we're in an override file where
   102  		// the type might not be set; we'll catch that during merge.
   103  		if v.Type != cty.NilType {
   104  			var err error
   105  			val, err = convert.Convert(val, v.Type)
   106  			if err != nil {
   107  				diags = append(diags, &hcl.Diagnostic{
   108  					Severity: hcl.DiagError,
   109  					Summary:  "Invalid default value for variable",
   110  					Detail:   fmt.Sprintf("This default value is not compatible with the variable's type constraint: %s.", err),
   111  					Subject:  attr.Expr.Range().Ptr(),
   112  				})
   113  				val = cty.DynamicVal
   114  			}
   115  		}
   116  
   117  		v.Default = val
   118  	}
   119  
   120  	return v, diags
   121  }
   122  
   123  func decodeVariableType(expr hcl.Expression) (cty.Type, VariableParsingMode, hcl.Diagnostics) {
   124  	if exprIsNativeQuotedString(expr) {
   125  		// Here we're accepting the pre-0.12 form of variable type argument where
   126  		// the string values "string", "list" and "map" are accepted has a hint
   127  		// about the type used primarily for deciding how to parse values
   128  		// given on the command line and in environment variables.
   129  		// Only the native syntax ends up in this codepath; we handle the
   130  		// JSON syntax (which is, of course, quoted even in the new format)
   131  		// in the normal codepath below.
   132  		val, diags := expr.Value(nil)
   133  		if diags.HasErrors() {
   134  			return cty.DynamicPseudoType, VariableParseHCL, diags
   135  		}
   136  		str := val.AsString()
   137  		switch str {
   138  		case "string":
   139  			return cty.String, VariableParseLiteral, diags
   140  		case "list":
   141  			return cty.List(cty.DynamicPseudoType), VariableParseHCL, diags
   142  		case "map":
   143  			return cty.Map(cty.DynamicPseudoType), VariableParseHCL, diags
   144  		default:
   145  			return cty.DynamicPseudoType, VariableParseHCL, hcl.Diagnostics{{
   146  				Severity: hcl.DiagError,
   147  				Summary:  "Invalid legacy variable type hint",
   148  				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.`,
   149  				Subject:  expr.Range().Ptr(),
   150  			}}
   151  		}
   152  	}
   153  
   154  	// First we'll deal with some shorthand forms that the HCL-level type
   155  	// expression parser doesn't include. These both emulate pre-0.12 behavior
   156  	// of allowing a list or map of any element type as long as all of the
   157  	// elements are consistent. This is the same as list(any) or map(any).
   158  	switch hcl.ExprAsKeyword(expr) {
   159  	case "list":
   160  		return cty.List(cty.DynamicPseudoType), VariableParseHCL, nil
   161  	case "map":
   162  		return cty.Map(cty.DynamicPseudoType), VariableParseHCL, nil
   163  	}
   164  
   165  	ty, diags := typeexpr.TypeConstraint(expr)
   166  	if diags.HasErrors() {
   167  		return cty.DynamicPseudoType, VariableParseHCL, diags
   168  	}
   169  
   170  	switch {
   171  	case ty.IsPrimitiveType():
   172  		// Primitive types use literal parsing.
   173  		return ty, VariableParseLiteral, diags
   174  	default:
   175  		// Everything else uses HCL parsing
   176  		return ty, VariableParseHCL, diags
   177  	}
   178  }
   179  
   180  // VariableParsingMode defines how values of a particular variable given by
   181  // text-only mechanisms (command line arguments and environment variables)
   182  // should be parsed to produce the final value.
   183  type VariableParsingMode rune
   184  
   185  // VariableParseLiteral is a variable parsing mode that just takes the given
   186  // string directly as a cty.String value.
   187  const VariableParseLiteral VariableParsingMode = 'L'
   188  
   189  // VariableParseHCL is a variable parsing mode that attempts to parse the given
   190  // string as an HCL expression and returns the result.
   191  const VariableParseHCL VariableParsingMode = 'H'
   192  
   193  // Parse uses the receiving parsing mode to process the given variable value
   194  // string, returning the result along with any diagnostics.
   195  //
   196  // A VariableParsingMode does not know the expected type of the corresponding
   197  // variable, so it's the caller's responsibility to attempt to convert the
   198  // result to the appropriate type and return to the user any diagnostics that
   199  // conversion may produce.
   200  //
   201  // The given name is used to create a synthetic filename in case any diagnostics
   202  // must be generated about the given string value. This should be the name
   203  // of the root module variable whose value will be populated from the given
   204  // string.
   205  //
   206  // If the returned diagnostics has errors, the returned value may not be
   207  // valid.
   208  func (m VariableParsingMode) Parse(name, value string) (cty.Value, hcl.Diagnostics) {
   209  	switch m {
   210  	case VariableParseLiteral:
   211  		return cty.StringVal(value), nil
   212  	case VariableParseHCL:
   213  		fakeFilename := fmt.Sprintf("<value for var.%s>", name)
   214  		expr, diags := hclsyntax.ParseExpression([]byte(value), fakeFilename, hcl.Pos{Line: 1, Column: 1})
   215  		if diags.HasErrors() {
   216  			return cty.DynamicVal, diags
   217  		}
   218  		val, valDiags := expr.Value(nil)
   219  		diags = append(diags, valDiags...)
   220  		return val, diags
   221  	default:
   222  		// Should never happen
   223  		panic(fmt.Errorf("Parse called on invalid VariableParsingMode %#v", m))
   224  	}
   225  }
   226  
   227  // Output represents an "output" block in a module or file.
   228  type Output struct {
   229  	Name        string
   230  	Description string
   231  	Expr        hcl.Expression
   232  	DependsOn   []hcl.Traversal
   233  	Sensitive   bool
   234  
   235  	DescriptionSet bool
   236  	SensitiveSet   bool
   237  
   238  	DeclRange hcl.Range
   239  }
   240  
   241  func decodeOutputBlock(block *hcl.Block, override bool) (*Output, hcl.Diagnostics) {
   242  	o := &Output{
   243  		Name:      block.Labels[0],
   244  		DeclRange: block.DefRange,
   245  	}
   246  
   247  	schema := outputBlockSchema
   248  	if override {
   249  		schema = schemaForOverrides(schema)
   250  	}
   251  
   252  	content, diags := block.Body.Content(schema)
   253  
   254  	if !hclsyntax.ValidIdentifier(o.Name) {
   255  		diags = append(diags, &hcl.Diagnostic{
   256  			Severity: hcl.DiagError,
   257  			Summary:  "Invalid output name",
   258  			Detail:   badIdentifierDetail,
   259  			Subject:  &block.LabelRanges[0],
   260  		})
   261  	}
   262  
   263  	if attr, exists := content.Attributes["description"]; exists {
   264  		valDiags := gohcl.DecodeExpression(attr.Expr, nil, &o.Description)
   265  		diags = append(diags, valDiags...)
   266  		o.DescriptionSet = true
   267  	}
   268  
   269  	if attr, exists := content.Attributes["value"]; exists {
   270  		o.Expr = attr.Expr
   271  	}
   272  
   273  	if attr, exists := content.Attributes["sensitive"]; exists {
   274  		valDiags := gohcl.DecodeExpression(attr.Expr, nil, &o.Sensitive)
   275  		diags = append(diags, valDiags...)
   276  		o.SensitiveSet = true
   277  	}
   278  
   279  	if attr, exists := content.Attributes["depends_on"]; exists {
   280  		deps, depsDiags := decodeDependsOn(attr)
   281  		diags = append(diags, depsDiags...)
   282  		o.DependsOn = append(o.DependsOn, deps...)
   283  	}
   284  
   285  	return o, diags
   286  }
   287  
   288  // Local represents a single entry from a "locals" block in a module or file.
   289  // The "locals" block itself is not represented, because it serves only to
   290  // provide context for us to interpret its contents.
   291  type Local struct {
   292  	Name string
   293  	Expr hcl.Expression
   294  
   295  	DeclRange hcl.Range
   296  }
   297  
   298  func decodeLocalsBlock(block *hcl.Block) ([]*Local, hcl.Diagnostics) {
   299  	attrs, diags := block.Body.JustAttributes()
   300  	if len(attrs) == 0 {
   301  		return nil, diags
   302  	}
   303  
   304  	locals := make([]*Local, 0, len(attrs))
   305  	for name, attr := range attrs {
   306  		if !hclsyntax.ValidIdentifier(name) {
   307  			diags = append(diags, &hcl.Diagnostic{
   308  				Severity: hcl.DiagError,
   309  				Summary:  "Invalid local value name",
   310  				Detail:   badIdentifierDetail,
   311  				Subject:  &attr.NameRange,
   312  			})
   313  		}
   314  
   315  		locals = append(locals, &Local{
   316  			Name:      name,
   317  			Expr:      attr.Expr,
   318  			DeclRange: attr.Range,
   319  		})
   320  	}
   321  	return locals, diags
   322  }
   323  
   324  var variableBlockSchema = &hcl.BodySchema{
   325  	Attributes: []hcl.AttributeSchema{
   326  		{
   327  			Name: "description",
   328  		},
   329  		{
   330  			Name: "default",
   331  		},
   332  		{
   333  			Name: "type",
   334  		},
   335  	},
   336  }
   337  
   338  var outputBlockSchema = &hcl.BodySchema{
   339  	Attributes: []hcl.AttributeSchema{
   340  		{
   341  			Name: "description",
   342  		},
   343  		{
   344  			Name:     "value",
   345  			Required: true,
   346  		},
   347  		{
   348  			Name: "depends_on",
   349  		},
   350  		{
   351  			Name: "sensitive",
   352  		},
   353  	},
   354  }