github.com/medzin/terraform@v0.11.11/configs/named_values.go (about)

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