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