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