github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/configs/named_values.go (about)

     1  package configs
     2  
     3  import (
     4  	"fmt"
     5  	"unicode"
     6  
     7  	"github.com/hashicorp/hcl/v2"
     8  	"github.com/hashicorp/hcl/v2/ext/typeexpr"
     9  	"github.com/hashicorp/hcl/v2/gohcl"
    10  	"github.com/hashicorp/hcl/v2/hclsyntax"
    11  	"github.com/zclconf/go-cty/cty"
    12  	"github.com/zclconf/go-cty/cty/convert"
    13  
    14  	"github.com/hashicorp/terraform/addrs"
    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  
    29  	DescriptionSet bool
    30  
    31  	DeclRange hcl.Range
    32  }
    33  
    34  func decodeVariableBlock(block *hcl.Block, override bool) (*Variable, hcl.Diagnostics) {
    35  	v := &Variable{
    36  		Name:      block.Labels[0],
    37  		DeclRange: block.DefRange,
    38  	}
    39  
    40  	// Unless we're building an override, we'll set some defaults
    41  	// which we might override with attributes below. We leave these
    42  	// as zero-value in the override case so we can recognize whether
    43  	// or not they are set when we merge.
    44  	if !override {
    45  		v.Type = cty.DynamicPseudoType
    46  		v.ParsingMode = VariableParseLiteral
    47  	}
    48  
    49  	content, diags := block.Body.Content(variableBlockSchema)
    50  
    51  	if !hclsyntax.ValidIdentifier(v.Name) {
    52  		diags = append(diags, &hcl.Diagnostic{
    53  			Severity: hcl.DiagError,
    54  			Summary:  "Invalid variable name",
    55  			Detail:   badIdentifierDetail,
    56  			Subject:  &block.LabelRanges[0],
    57  		})
    58  	}
    59  
    60  	// Don't allow declaration of variables that would conflict with the
    61  	// reserved attribute and block type names in a "module" block, since
    62  	// these won't be usable for child modules.
    63  	for _, attr := range moduleBlockSchema.Attributes {
    64  		if attr.Name == v.Name {
    65  			diags = append(diags, &hcl.Diagnostic{
    66  				Severity: hcl.DiagError,
    67  				Summary:  "Invalid variable name",
    68  				Detail:   fmt.Sprintf("The variable name %q is reserved due to its special meaning inside module blocks.", attr.Name),
    69  				Subject:  &block.LabelRanges[0],
    70  			})
    71  		}
    72  	}
    73  	for _, blockS := range moduleBlockSchema.Blocks {
    74  		if blockS.Type == v.Name {
    75  			diags = append(diags, &hcl.Diagnostic{
    76  				Severity: hcl.DiagError,
    77  				Summary:  "Invalid variable name",
    78  				Detail:   fmt.Sprintf("The variable name %q is reserved due to its special meaning inside module blocks.", blockS.Type),
    79  				Subject:  &block.LabelRanges[0],
    80  			})
    81  		}
    82  	}
    83  
    84  	if attr, exists := content.Attributes["description"]; exists {
    85  		valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Description)
    86  		diags = append(diags, valDiags...)
    87  		v.DescriptionSet = true
    88  	}
    89  
    90  	if attr, exists := content.Attributes["type"]; exists {
    91  		ty, parseMode, tyDiags := decodeVariableType(attr.Expr)
    92  		diags = append(diags, tyDiags...)
    93  		v.Type = ty
    94  		v.ParsingMode = parseMode
    95  	}
    96  
    97  	if attr, exists := content.Attributes["default"]; exists {
    98  		val, valDiags := attr.Expr.Value(nil)
    99  		diags = append(diags, valDiags...)
   100  
   101  		// Convert the default to the expected type so we can catch invalid
   102  		// defaults early and allow later code to assume validity.
   103  		// Note that this depends on us having already processed any "type"
   104  		// attribute above.
   105  		// However, we can't do this if we're in an override file where
   106  		// the type might not be set; we'll catch that during merge.
   107  		if v.Type != cty.NilType {
   108  			var err error
   109  			val, err = convert.Convert(val, v.Type)
   110  			if err != nil {
   111  				diags = append(diags, &hcl.Diagnostic{
   112  					Severity: hcl.DiagError,
   113  					Summary:  "Invalid default value for variable",
   114  					Detail:   fmt.Sprintf("This default value is not compatible with the variable's type constraint: %s.", err),
   115  					Subject:  attr.Expr.Range().Ptr(),
   116  				})
   117  				val = cty.DynamicVal
   118  			}
   119  		}
   120  
   121  		v.Default = val
   122  	}
   123  
   124  	for _, block := range content.Blocks {
   125  		switch block.Type {
   126  
   127  		case "validation":
   128  			vv, moreDiags := decodeVariableValidationBlock(v.Name, block, override)
   129  			diags = append(diags, moreDiags...)
   130  			v.Validations = append(v.Validations, vv)
   131  
   132  		default:
   133  			// The above cases should be exhaustive for all block types
   134  			// defined in variableBlockSchema
   135  			panic(fmt.Sprintf("unhandled block type %q", block.Type))
   136  		}
   137  	}
   138  
   139  	return v, diags
   140  }
   141  
   142  func decodeVariableType(expr hcl.Expression) (cty.Type, VariableParsingMode, hcl.Diagnostics) {
   143  	if exprIsNativeQuotedString(expr) {
   144  		// Here we're accepting the pre-0.12 form of variable type argument where
   145  		// the string values "string", "list" and "map" are accepted has a hint
   146  		// about the type used primarily for deciding how to parse values
   147  		// given on the command line and in environment variables.
   148  		// Only the native syntax ends up in this codepath; we handle the
   149  		// JSON syntax (which is, of course, quoted even in the new format)
   150  		// in the normal codepath below.
   151  		val, diags := expr.Value(nil)
   152  		if diags.HasErrors() {
   153  			return cty.DynamicPseudoType, VariableParseHCL, diags
   154  		}
   155  		str := val.AsString()
   156  		switch str {
   157  		case "string":
   158  			diags = append(diags, &hcl.Diagnostic{
   159  				Severity: hcl.DiagWarning,
   160  				Summary:  "Quoted type constraints are deprecated",
   161  				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. To silence this warning, remove the quotes around \"string\".",
   162  				Subject:  expr.Range().Ptr(),
   163  			})
   164  			return cty.String, VariableParseLiteral, diags
   165  		case "list":
   166  			diags = append(diags, &hcl.Diagnostic{
   167  				Severity: hcl.DiagWarning,
   168  				Summary:  "Quoted type constraints are deprecated",
   169  				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. To silence this warning, remove the quotes around \"list\" and write list(string) instead to explicitly indicate that the list elements are strings.",
   170  				Subject:  expr.Range().Ptr(),
   171  			})
   172  			return cty.List(cty.DynamicPseudoType), VariableParseHCL, diags
   173  		case "map":
   174  			diags = append(diags, &hcl.Diagnostic{
   175  				Severity: hcl.DiagWarning,
   176  				Summary:  "Quoted type constraints are deprecated",
   177  				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. To silence this warning, remove the quotes around \"map\" and write map(string) instead to explicitly indicate that the map elements are strings.",
   178  				Subject:  expr.Range().Ptr(),
   179  			})
   180  			return cty.Map(cty.DynamicPseudoType), VariableParseHCL, diags
   181  		default:
   182  			return cty.DynamicPseudoType, VariableParseHCL, hcl.Diagnostics{{
   183  				Severity: hcl.DiagError,
   184  				Summary:  "Invalid legacy variable type hint",
   185  				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.`,
   186  				Subject:  expr.Range().Ptr(),
   187  			}}
   188  		}
   189  	}
   190  
   191  	// First we'll deal with some shorthand forms that the HCL-level type
   192  	// expression parser doesn't include. These both emulate pre-0.12 behavior
   193  	// of allowing a list or map of any element type as long as all of the
   194  	// elements are consistent. This is the same as list(any) or map(any).
   195  	switch hcl.ExprAsKeyword(expr) {
   196  	case "list":
   197  		return cty.List(cty.DynamicPseudoType), VariableParseHCL, nil
   198  	case "map":
   199  		return cty.Map(cty.DynamicPseudoType), VariableParseHCL, nil
   200  	}
   201  
   202  	ty, diags := typeexpr.TypeConstraint(expr)
   203  	if diags.HasErrors() {
   204  		return cty.DynamicPseudoType, VariableParseHCL, diags
   205  	}
   206  
   207  	switch {
   208  	case ty.IsPrimitiveType():
   209  		// Primitive types use literal parsing.
   210  		return ty, VariableParseLiteral, diags
   211  	default:
   212  		// Everything else uses HCL parsing
   213  		return ty, VariableParseHCL, diags
   214  	}
   215  }
   216  
   217  // Required returns true if this variable is required to be set by the caller,
   218  // or false if there is a default value that will be used when it isn't set.
   219  func (v *Variable) Required() bool {
   220  	return v.Default == cty.NilVal
   221  }
   222  
   223  // VariableParsingMode defines how values of a particular variable given by
   224  // text-only mechanisms (command line arguments and environment variables)
   225  // should be parsed to produce the final value.
   226  type VariableParsingMode rune
   227  
   228  // VariableParseLiteral is a variable parsing mode that just takes the given
   229  // string directly as a cty.String value.
   230  const VariableParseLiteral VariableParsingMode = 'L'
   231  
   232  // VariableParseHCL is a variable parsing mode that attempts to parse the given
   233  // string as an HCL expression and returns the result.
   234  const VariableParseHCL VariableParsingMode = 'H'
   235  
   236  // Parse uses the receiving parsing mode to process the given variable value
   237  // string, returning the result along with any diagnostics.
   238  //
   239  // A VariableParsingMode does not know the expected type of the corresponding
   240  // variable, so it's the caller's responsibility to attempt to convert the
   241  // result to the appropriate type and return to the user any diagnostics that
   242  // conversion may produce.
   243  //
   244  // The given name is used to create a synthetic filename in case any diagnostics
   245  // must be generated about the given string value. This should be the name
   246  // of the root module variable whose value will be populated from the given
   247  // string.
   248  //
   249  // If the returned diagnostics has errors, the returned value may not be
   250  // valid.
   251  func (m VariableParsingMode) Parse(name, value string) (cty.Value, hcl.Diagnostics) {
   252  	switch m {
   253  	case VariableParseLiteral:
   254  		return cty.StringVal(value), nil
   255  	case VariableParseHCL:
   256  		fakeFilename := fmt.Sprintf("<value for var.%s>", name)
   257  		expr, diags := hclsyntax.ParseExpression([]byte(value), fakeFilename, hcl.Pos{Line: 1, Column: 1})
   258  		if diags.HasErrors() {
   259  			return cty.DynamicVal, diags
   260  		}
   261  		val, valDiags := expr.Value(nil)
   262  		diags = append(diags, valDiags...)
   263  		return val, diags
   264  	default:
   265  		// Should never happen
   266  		panic(fmt.Errorf("Parse called on invalid VariableParsingMode %#v", m))
   267  	}
   268  }
   269  
   270  // VariableValidation represents a configuration-defined validation rule
   271  // for a particular input variable, given as a "validation" block inside
   272  // a "variable" block.
   273  type VariableValidation struct {
   274  	// Condition is an expression that refers to the variable being tested
   275  	// and contains no other references. The expression must return true
   276  	// to indicate that the value is valid or false to indicate that it is
   277  	// invalid. If the expression produces an error, that's considered a bug
   278  	// in the module defining the validation rule, not an error in the caller.
   279  	Condition hcl.Expression
   280  
   281  	// ErrorMessage is one or more full sentences, which would need to be in
   282  	// English for consistency with the rest of the error message output but
   283  	// can in practice be in any language as long as it ends with a period.
   284  	// The message should describe what is required for the condition to return
   285  	// true in a way that would make sense to a caller of the module.
   286  	ErrorMessage string
   287  
   288  	DeclRange hcl.Range
   289  }
   290  
   291  func decodeVariableValidationBlock(varName string, block *hcl.Block, override bool) (*VariableValidation, hcl.Diagnostics) {
   292  	var diags hcl.Diagnostics
   293  	vv := &VariableValidation{
   294  		DeclRange: block.DefRange,
   295  	}
   296  
   297  	if override {
   298  		// For now we'll just forbid overriding validation blocks, to simplify
   299  		// the initial design. If we can find a clear use-case for overriding
   300  		// validations in override files and there's a way to define it that
   301  		// isn't confusing then we could relax this.
   302  		diags = diags.Append(&hcl.Diagnostic{
   303  			Severity: hcl.DiagError,
   304  			Summary:  "Can't override variable validation rules",
   305  			Detail:   "Variable \"validation\" blocks cannot be used in override files.",
   306  			Subject:  vv.DeclRange.Ptr(),
   307  		})
   308  		return vv, diags
   309  	}
   310  
   311  	content, moreDiags := block.Body.Content(variableValidationBlockSchema)
   312  	diags = append(diags, moreDiags...)
   313  
   314  	if attr, exists := content.Attributes["condition"]; exists {
   315  		vv.Condition = attr.Expr
   316  
   317  		// The validation condition can only refer to the variable itself,
   318  		// to ensure that the variable declaration can't create additional
   319  		// edges in the dependency graph.
   320  		goodRefs := 0
   321  		for _, traversal := range vv.Condition.Variables() {
   322  			ref, moreDiags := addrs.ParseRef(traversal)
   323  			if !moreDiags.HasErrors() {
   324  				if addr, ok := ref.Subject.(addrs.InputVariable); ok {
   325  					if addr.Name == varName {
   326  						goodRefs++
   327  						continue // Reference is valid
   328  					}
   329  				}
   330  			}
   331  			// If we fall out here then the reference is invalid.
   332  			diags = diags.Append(&hcl.Diagnostic{
   333  				Severity: hcl.DiagError,
   334  				Summary:  "Invalid reference in variable validation",
   335  				Detail:   fmt.Sprintf("The condition for variable %q can only refer to the variable itself, using var.%s.", varName, varName),
   336  				Subject:  traversal.SourceRange().Ptr(),
   337  			})
   338  		}
   339  		if goodRefs < 1 {
   340  			diags = diags.Append(&hcl.Diagnostic{
   341  				Severity: hcl.DiagError,
   342  				Summary:  "Invalid variable validation condition",
   343  				Detail:   fmt.Sprintf("The condition for variable %q must refer to var.%s in order to test incoming values.", varName, varName),
   344  				Subject:  attr.Expr.Range().Ptr(),
   345  			})
   346  		}
   347  	}
   348  
   349  	if attr, exists := content.Attributes["error_message"]; exists {
   350  		moreDiags := gohcl.DecodeExpression(attr.Expr, nil, &vv.ErrorMessage)
   351  		diags = append(diags, moreDiags...)
   352  		if !moreDiags.HasErrors() {
   353  			const errSummary = "Invalid validation error message"
   354  			switch {
   355  			case vv.ErrorMessage == "":
   356  				diags = diags.Append(&hcl.Diagnostic{
   357  					Severity: hcl.DiagError,
   358  					Summary:  errSummary,
   359  					Detail:   "An empty string is not a valid nor useful error message.",
   360  					Subject:  attr.Expr.Range().Ptr(),
   361  				})
   362  			case !looksLikeSentences(vv.ErrorMessage):
   363  				// Because we're going to include this string verbatim as part
   364  				// of a bigger error message written in our usual style in
   365  				// English, we'll require the given error message to conform
   366  				// to that. We might relax this in future if e.g. we start
   367  				// presenting these error messages in a different way, or if
   368  				// Terraform starts supporting producing error messages in
   369  				// other human languages, etc.
   370  				// For pragmatism we also allow sentences ending with
   371  				// exclamation points, but we don't mention it explicitly here
   372  				// because that's not really consistent with the Terraform UI
   373  				// writing style.
   374  				diags = diags.Append(&hcl.Diagnostic{
   375  					Severity: hcl.DiagError,
   376  					Summary:  errSummary,
   377  					Detail:   "Validation error message must be at least one full English sentence starting with an uppercase letter and ending with a period or question mark.",
   378  					Subject:  attr.Expr.Range().Ptr(),
   379  				})
   380  			}
   381  		}
   382  	}
   383  
   384  	return vv, diags
   385  }
   386  
   387  // looksLikeSentence is a simple heuristic that encourages writing error
   388  // messages that will be presentable when included as part of a larger
   389  // Terraform error diagnostic whose other text is written in the Terraform
   390  // UI writing style.
   391  //
   392  // This is intentionally not a very strong validation since we're assuming
   393  // that module authors want to write good messages and might just need a nudge
   394  // about Terraform's specific style, rather than that they are going to try
   395  // to work around these rules to write a lower-quality message.
   396  func looksLikeSentences(s string) bool {
   397  	if len(s) < 1 {
   398  		return false
   399  	}
   400  	runes := []rune(s) // HCL guarantees that all strings are valid UTF-8
   401  	first := runes[0]
   402  	last := runes[len(s)-1]
   403  
   404  	// If the first rune is a letter then it must be an uppercase letter.
   405  	// (This will only see the first rune in a multi-rune combining sequence,
   406  	// but the first rune is generally the letter if any are, and if not then
   407  	// we'll just ignore it because we're primarily expecting English messages
   408  	// right now anyway, for consistency with all of Terraform's other output.)
   409  	if unicode.IsLetter(first) && !unicode.IsUpper(first) {
   410  		return false
   411  	}
   412  
   413  	// The string must be at least one full sentence, which implies having
   414  	// sentence-ending punctuation.
   415  	// (This assumes that if a sentence ends with quotes then the period
   416  	// will be outside the quotes, which is consistent with Terraform's UI
   417  	// writing style.)
   418  	return last == '.' || last == '?' || last == '!'
   419  }
   420  
   421  // Output represents an "output" block in a module or file.
   422  type Output struct {
   423  	Name        string
   424  	Description string
   425  	Expr        hcl.Expression
   426  	DependsOn   []hcl.Traversal
   427  	Sensitive   bool
   428  
   429  	DescriptionSet bool
   430  	SensitiveSet   bool
   431  
   432  	DeclRange hcl.Range
   433  }
   434  
   435  func decodeOutputBlock(block *hcl.Block, override bool) (*Output, hcl.Diagnostics) {
   436  	o := &Output{
   437  		Name:      block.Labels[0],
   438  		DeclRange: block.DefRange,
   439  	}
   440  
   441  	schema := outputBlockSchema
   442  	if override {
   443  		schema = schemaForOverrides(schema)
   444  	}
   445  
   446  	content, diags := block.Body.Content(schema)
   447  
   448  	if !hclsyntax.ValidIdentifier(o.Name) {
   449  		diags = append(diags, &hcl.Diagnostic{
   450  			Severity: hcl.DiagError,
   451  			Summary:  "Invalid output name",
   452  			Detail:   badIdentifierDetail,
   453  			Subject:  &block.LabelRanges[0],
   454  		})
   455  	}
   456  
   457  	if attr, exists := content.Attributes["description"]; exists {
   458  		valDiags := gohcl.DecodeExpression(attr.Expr, nil, &o.Description)
   459  		diags = append(diags, valDiags...)
   460  		o.DescriptionSet = true
   461  	}
   462  
   463  	if attr, exists := content.Attributes["value"]; exists {
   464  		o.Expr = attr.Expr
   465  	}
   466  
   467  	if attr, exists := content.Attributes["sensitive"]; exists {
   468  		valDiags := gohcl.DecodeExpression(attr.Expr, nil, &o.Sensitive)
   469  		diags = append(diags, valDiags...)
   470  		o.SensitiveSet = true
   471  	}
   472  
   473  	if attr, exists := content.Attributes["depends_on"]; exists {
   474  		deps, depsDiags := decodeDependsOn(attr)
   475  		diags = append(diags, depsDiags...)
   476  		o.DependsOn = append(o.DependsOn, deps...)
   477  	}
   478  
   479  	return o, diags
   480  }
   481  
   482  // Local represents a single entry from a "locals" block in a module or file.
   483  // The "locals" block itself is not represented, because it serves only to
   484  // provide context for us to interpret its contents.
   485  type Local struct {
   486  	Name string
   487  	Expr hcl.Expression
   488  
   489  	DeclRange hcl.Range
   490  }
   491  
   492  func decodeLocalsBlock(block *hcl.Block) ([]*Local, hcl.Diagnostics) {
   493  	attrs, diags := block.Body.JustAttributes()
   494  	if len(attrs) == 0 {
   495  		return nil, diags
   496  	}
   497  
   498  	locals := make([]*Local, 0, len(attrs))
   499  	for name, attr := range attrs {
   500  		if !hclsyntax.ValidIdentifier(name) {
   501  			diags = append(diags, &hcl.Diagnostic{
   502  				Severity: hcl.DiagError,
   503  				Summary:  "Invalid local value name",
   504  				Detail:   badIdentifierDetail,
   505  				Subject:  &attr.NameRange,
   506  			})
   507  		}
   508  
   509  		locals = append(locals, &Local{
   510  			Name:      name,
   511  			Expr:      attr.Expr,
   512  			DeclRange: attr.Range,
   513  		})
   514  	}
   515  	return locals, diags
   516  }
   517  
   518  // Addr returns the address of the local value declared by the receiver,
   519  // relative to its containing module.
   520  func (l *Local) Addr() addrs.LocalValue {
   521  	return addrs.LocalValue{
   522  		Name: l.Name,
   523  	}
   524  }
   525  
   526  var variableBlockSchema = &hcl.BodySchema{
   527  	Attributes: []hcl.AttributeSchema{
   528  		{
   529  			Name: "description",
   530  		},
   531  		{
   532  			Name: "default",
   533  		},
   534  		{
   535  			Name: "type",
   536  		},
   537  	},
   538  	Blocks: []hcl.BlockHeaderSchema{
   539  		{
   540  			Type: "validation",
   541  		},
   542  	},
   543  }
   544  
   545  var variableValidationBlockSchema = &hcl.BodySchema{
   546  	Attributes: []hcl.AttributeSchema{
   547  		{
   548  			Name:     "condition",
   549  			Required: true,
   550  		},
   551  		{
   552  			Name:     "error_message",
   553  			Required: true,
   554  		},
   555  	},
   556  }
   557  
   558  var outputBlockSchema = &hcl.BodySchema{
   559  	Attributes: []hcl.AttributeSchema{
   560  		{
   561  			Name: "description",
   562  		},
   563  		{
   564  			Name:     "value",
   565  			Required: true,
   566  		},
   567  		{
   568  			Name: "depends_on",
   569  		},
   570  		{
   571  			Name: "sensitive",
   572  		},
   573  	},
   574  }