github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/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/gohcl"
     8  	"github.com/hashicorp/hcl/v2/hclsyntax"
     9  	"github.com/zclconf/go-cty/cty"
    10  	"github.com/zclconf/go-cty/cty/convert"
    11  
    12  	"github.com/eliastor/durgaform/internal/addrs"
    13  	"github.com/eliastor/durgaform/internal/typeexpr"
    14  )
    15  
    16  // A consistent detail message for all "not a valid identifier" diagnostics.
    17  const badIdentifierDetail = "A name must start with a letter or underscore and may contain only letters, digits, underscores, and dashes."
    18  
    19  // Variable represents a "variable" block in a module or file.
    20  type Variable struct {
    21  	Name        string
    22  	Description string
    23  	Default     cty.Value
    24  
    25  	// Type is the concrete type of the variable value.
    26  	Type cty.Type
    27  	// ConstraintType is used for decoding and type conversions, and may
    28  	// contain nested ObjectWithOptionalAttr types.
    29  	ConstraintType cty.Type
    30  	TypeDefaults   *typeexpr.Defaults
    31  
    32  	ParsingMode VariableParsingMode
    33  	Validations []*CheckRule
    34  	Sensitive   bool
    35  
    36  	DescriptionSet bool
    37  	SensitiveSet   bool
    38  
    39  	// Nullable indicates that null is a valid value for this variable. Setting
    40  	// Nullable to false means that the module can expect this variable to
    41  	// never be null.
    42  	Nullable    bool
    43  	NullableSet bool
    44  
    45  	DeclRange hcl.Range
    46  }
    47  
    48  func decodeVariableBlock(block *hcl.Block, override bool) (*Variable, hcl.Diagnostics) {
    49  	v := &Variable{
    50  		Name:      block.Labels[0],
    51  		DeclRange: block.DefRange,
    52  	}
    53  
    54  	// Unless we're building an override, we'll set some defaults
    55  	// which we might override with attributes below. We leave these
    56  	// as zero-value in the override case so we can recognize whether
    57  	// or not they are set when we merge.
    58  	if !override {
    59  		v.Type = cty.DynamicPseudoType
    60  		v.ConstraintType = cty.DynamicPseudoType
    61  		v.ParsingMode = VariableParseLiteral
    62  	}
    63  
    64  	content, diags := block.Body.Content(variableBlockSchema)
    65  
    66  	if !hclsyntax.ValidIdentifier(v.Name) {
    67  		diags = append(diags, &hcl.Diagnostic{
    68  			Severity: hcl.DiagError,
    69  			Summary:  "Invalid variable name",
    70  			Detail:   badIdentifierDetail,
    71  			Subject:  &block.LabelRanges[0],
    72  		})
    73  	}
    74  
    75  	// Don't allow declaration of variables that would conflict with the
    76  	// reserved attribute and block type names in a "module" block, since
    77  	// these won't be usable for child modules.
    78  	for _, attr := range moduleBlockSchema.Attributes {
    79  		if attr.Name == v.Name {
    80  			diags = append(diags, &hcl.Diagnostic{
    81  				Severity: hcl.DiagError,
    82  				Summary:  "Invalid variable name",
    83  				Detail:   fmt.Sprintf("The variable name %q is reserved due to its special meaning inside module blocks.", attr.Name),
    84  				Subject:  &block.LabelRanges[0],
    85  			})
    86  		}
    87  	}
    88  	for _, blockS := range moduleBlockSchema.Blocks {
    89  		if blockS.Type == v.Name {
    90  			diags = append(diags, &hcl.Diagnostic{
    91  				Severity: hcl.DiagError,
    92  				Summary:  "Invalid variable name",
    93  				Detail:   fmt.Sprintf("The variable name %q is reserved due to its special meaning inside module blocks.", blockS.Type),
    94  				Subject:  &block.LabelRanges[0],
    95  			})
    96  		}
    97  	}
    98  
    99  	if attr, exists := content.Attributes["description"]; exists {
   100  		valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Description)
   101  		diags = append(diags, valDiags...)
   102  		v.DescriptionSet = true
   103  	}
   104  
   105  	if attr, exists := content.Attributes["type"]; exists {
   106  		ty, tyDefaults, parseMode, tyDiags := decodeVariableType(attr.Expr)
   107  		diags = append(diags, tyDiags...)
   108  		v.ConstraintType = ty
   109  		v.TypeDefaults = tyDefaults
   110  		v.Type = ty.WithoutOptionalAttributesDeep()
   111  		v.ParsingMode = parseMode
   112  	}
   113  
   114  	if attr, exists := content.Attributes["sensitive"]; exists {
   115  		valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Sensitive)
   116  		diags = append(diags, valDiags...)
   117  		v.SensitiveSet = true
   118  	}
   119  
   120  	if attr, exists := content.Attributes["nullable"]; exists {
   121  		valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Nullable)
   122  		diags = append(diags, valDiags...)
   123  		v.NullableSet = true
   124  	} else {
   125  		// The current default is true, which is subject to change in a future
   126  		// language edition.
   127  		v.Nullable = true
   128  	}
   129  
   130  	if attr, exists := content.Attributes["default"]; exists {
   131  		val, valDiags := attr.Expr.Value(nil)
   132  		diags = append(diags, valDiags...)
   133  
   134  		// Convert the default to the expected type so we can catch invalid
   135  		// defaults early and allow later code to assume validity.
   136  		// Note that this depends on us having already processed any "type"
   137  		// attribute above.
   138  		// However, we can't do this if we're in an override file where
   139  		// the type might not be set; we'll catch that during merge.
   140  		if v.ConstraintType != cty.NilType {
   141  			var err error
   142  			// If the type constraint has defaults, we must apply those
   143  			// defaults to the variable default value before type conversion.
   144  			if v.TypeDefaults != nil {
   145  				val = v.TypeDefaults.Apply(val)
   146  			}
   147  			val, err = convert.Convert(val, v.ConstraintType)
   148  			if err != nil {
   149  				diags = append(diags, &hcl.Diagnostic{
   150  					Severity: hcl.DiagError,
   151  					Summary:  "Invalid default value for variable",
   152  					Detail:   fmt.Sprintf("This default value is not compatible with the variable's type constraint: %s.", err),
   153  					Subject:  attr.Expr.Range().Ptr(),
   154  				})
   155  				val = cty.DynamicVal
   156  			}
   157  		}
   158  
   159  		if !v.Nullable && val.IsNull() {
   160  			diags = append(diags, &hcl.Diagnostic{
   161  				Severity: hcl.DiagError,
   162  				Summary:  "Invalid default value for variable",
   163  				Detail:   "A null default value is not valid when nullable=false.",
   164  				Subject:  attr.Expr.Range().Ptr(),
   165  			})
   166  		}
   167  
   168  		v.Default = val
   169  	}
   170  
   171  	for _, block := range content.Blocks {
   172  		switch block.Type {
   173  
   174  		case "validation":
   175  			vv, moreDiags := decodeVariableValidationBlock(v.Name, block, override)
   176  			diags = append(diags, moreDiags...)
   177  			v.Validations = append(v.Validations, vv)
   178  
   179  		default:
   180  			// The above cases should be exhaustive for all block types
   181  			// defined in variableBlockSchema
   182  			panic(fmt.Sprintf("unhandled block type %q", block.Type))
   183  		}
   184  	}
   185  
   186  	return v, diags
   187  }
   188  
   189  func decodeVariableType(expr hcl.Expression) (cty.Type, *typeexpr.Defaults, VariableParsingMode, hcl.Diagnostics) {
   190  	if exprIsNativeQuotedString(expr) {
   191  		// If a user provides the pre-0.12 form of variable type argument where
   192  		// the string values "string", "list" and "map" are accepted, we
   193  		// provide an error to point the user towards using the type system
   194  		// correctly has a hint.
   195  		// Only the native syntax ends up in this codepath; we handle the
   196  		// JSON syntax (which is, of course, quoted within the type system)
   197  		// in the normal codepath below.
   198  		val, diags := expr.Value(nil)
   199  		if diags.HasErrors() {
   200  			return cty.DynamicPseudoType, nil, VariableParseHCL, diags
   201  		}
   202  		str := val.AsString()
   203  		switch str {
   204  		case "string":
   205  			diags = append(diags, &hcl.Diagnostic{
   206  				Severity: hcl.DiagError,
   207  				Summary:  "Invalid quoted type constraints",
   208  				Detail:   "Durgaform 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\".",
   209  				Subject:  expr.Range().Ptr(),
   210  			})
   211  			return cty.DynamicPseudoType, nil, VariableParseLiteral, diags
   212  		case "list":
   213  			diags = append(diags, &hcl.Diagnostic{
   214  				Severity: hcl.DiagError,
   215  				Summary:  "Invalid quoted type constraints",
   216  				Detail:   "Durgaform 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.",
   217  				Subject:  expr.Range().Ptr(),
   218  			})
   219  			return cty.DynamicPseudoType, nil, VariableParseHCL, diags
   220  		case "map":
   221  			diags = append(diags, &hcl.Diagnostic{
   222  				Severity: hcl.DiagError,
   223  				Summary:  "Invalid quoted type constraints",
   224  				Detail:   "Durgaform 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.",
   225  				Subject:  expr.Range().Ptr(),
   226  			})
   227  			return cty.DynamicPseudoType, nil, VariableParseHCL, diags
   228  		default:
   229  			return cty.DynamicPseudoType, nil, VariableParseHCL, hcl.Diagnostics{{
   230  				Severity: hcl.DiagError,
   231  				Summary:  "Invalid legacy variable type hint",
   232  				Detail:   `To provide a full type expression, remove the surrounding quotes and give the type expression directly.`,
   233  				Subject:  expr.Range().Ptr(),
   234  			}}
   235  		}
   236  	}
   237  
   238  	// First we'll deal with some shorthand forms that the HCL-level type
   239  	// expression parser doesn't include. These both emulate pre-0.12 behavior
   240  	// of allowing a list or map of any element type as long as all of the
   241  	// elements are consistent. This is the same as list(any) or map(any).
   242  	switch hcl.ExprAsKeyword(expr) {
   243  	case "list":
   244  		return cty.List(cty.DynamicPseudoType), nil, VariableParseHCL, nil
   245  	case "map":
   246  		return cty.Map(cty.DynamicPseudoType), nil, VariableParseHCL, nil
   247  	}
   248  
   249  	ty, typeDefaults, diags := typeexpr.TypeConstraintWithDefaults(expr)
   250  	if diags.HasErrors() {
   251  		return cty.DynamicPseudoType, nil, VariableParseHCL, diags
   252  	}
   253  
   254  	switch {
   255  	case ty.IsPrimitiveType():
   256  		// Primitive types use literal parsing.
   257  		return ty, typeDefaults, VariableParseLiteral, diags
   258  	default:
   259  		// Everything else uses HCL parsing
   260  		return ty, typeDefaults, VariableParseHCL, diags
   261  	}
   262  }
   263  
   264  // Required returns true if this variable is required to be set by the caller,
   265  // or false if there is a default value that will be used when it isn't set.
   266  func (v *Variable) Required() bool {
   267  	return v.Default == cty.NilVal
   268  }
   269  
   270  // VariableParsingMode defines how values of a particular variable given by
   271  // text-only mechanisms (command line arguments and environment variables)
   272  // should be parsed to produce the final value.
   273  type VariableParsingMode rune
   274  
   275  // VariableParseLiteral is a variable parsing mode that just takes the given
   276  // string directly as a cty.String value.
   277  const VariableParseLiteral VariableParsingMode = 'L'
   278  
   279  // VariableParseHCL is a variable parsing mode that attempts to parse the given
   280  // string as an HCL expression and returns the result.
   281  const VariableParseHCL VariableParsingMode = 'H'
   282  
   283  // Parse uses the receiving parsing mode to process the given variable value
   284  // string, returning the result along with any diagnostics.
   285  //
   286  // A VariableParsingMode does not know the expected type of the corresponding
   287  // variable, so it's the caller's responsibility to attempt to convert the
   288  // result to the appropriate type and return to the user any diagnostics that
   289  // conversion may produce.
   290  //
   291  // The given name is used to create a synthetic filename in case any diagnostics
   292  // must be generated about the given string value. This should be the name
   293  // of the root module variable whose value will be populated from the given
   294  // string.
   295  //
   296  // If the returned diagnostics has errors, the returned value may not be
   297  // valid.
   298  func (m VariableParsingMode) Parse(name, value string) (cty.Value, hcl.Diagnostics) {
   299  	switch m {
   300  	case VariableParseLiteral:
   301  		return cty.StringVal(value), nil
   302  	case VariableParseHCL:
   303  		fakeFilename := fmt.Sprintf("<value for var.%s>", name)
   304  		expr, diags := hclsyntax.ParseExpression([]byte(value), fakeFilename, hcl.Pos{Line: 1, Column: 1})
   305  		if diags.HasErrors() {
   306  			return cty.DynamicVal, diags
   307  		}
   308  		val, valDiags := expr.Value(nil)
   309  		diags = append(diags, valDiags...)
   310  		return val, diags
   311  	default:
   312  		// Should never happen
   313  		panic(fmt.Errorf("Parse called on invalid VariableParsingMode %#v", m))
   314  	}
   315  }
   316  
   317  // decodeVariableValidationBlock is a wrapper around decodeCheckRuleBlock
   318  // that imposes the additional rule that the condition expression can refer
   319  // only to an input variable of the given name.
   320  func decodeVariableValidationBlock(varName string, block *hcl.Block, override bool) (*CheckRule, hcl.Diagnostics) {
   321  	vv, diags := decodeCheckRuleBlock(block, override)
   322  	if vv.Condition != nil {
   323  		// The validation condition can only refer to the variable itself,
   324  		// to ensure that the variable declaration can't create additional
   325  		// edges in the dependency graph.
   326  		goodRefs := 0
   327  		for _, traversal := range vv.Condition.Variables() {
   328  			ref, moreDiags := addrs.ParseRef(traversal)
   329  			if !moreDiags.HasErrors() {
   330  				if addr, ok := ref.Subject.(addrs.InputVariable); ok {
   331  					if addr.Name == varName {
   332  						goodRefs++
   333  						continue // Reference is valid
   334  					}
   335  				}
   336  			}
   337  			// If we fall out here then the reference is invalid.
   338  			diags = diags.Append(&hcl.Diagnostic{
   339  				Severity: hcl.DiagError,
   340  				Summary:  "Invalid reference in variable validation",
   341  				Detail:   fmt.Sprintf("The condition for variable %q can only refer to the variable itself, using var.%s.", varName, varName),
   342  				Subject:  traversal.SourceRange().Ptr(),
   343  			})
   344  		}
   345  		if goodRefs < 1 {
   346  			diags = diags.Append(&hcl.Diagnostic{
   347  				Severity: hcl.DiagError,
   348  				Summary:  "Invalid variable validation condition",
   349  				Detail:   fmt.Sprintf("The condition for variable %q must refer to var.%s in order to test incoming values.", varName, varName),
   350  				Subject:  vv.Condition.Range().Ptr(),
   351  			})
   352  		}
   353  	}
   354  
   355  	if vv.ErrorMessage != nil {
   356  		// The same applies to the validation error message, except that
   357  		// references are not required. A string literal is a valid error
   358  		// message.
   359  		goodRefs := 0
   360  		for _, traversal := range vv.ErrorMessage.Variables() {
   361  			ref, moreDiags := addrs.ParseRef(traversal)
   362  			if !moreDiags.HasErrors() {
   363  				if addr, ok := ref.Subject.(addrs.InputVariable); ok {
   364  					if addr.Name == varName {
   365  						goodRefs++
   366  						continue // Reference is valid
   367  					}
   368  				}
   369  			}
   370  			// If we fall out here then the reference is invalid.
   371  			diags = diags.Append(&hcl.Diagnostic{
   372  				Severity: hcl.DiagError,
   373  				Summary:  "Invalid reference in variable validation",
   374  				Detail:   fmt.Sprintf("The error message for variable %q can only refer to the variable itself, using var.%s.", varName, varName),
   375  				Subject:  traversal.SourceRange().Ptr(),
   376  			})
   377  		}
   378  	}
   379  
   380  	return vv, diags
   381  }
   382  
   383  // Output represents an "output" block in a module or file.
   384  type Output struct {
   385  	Name        string
   386  	Description string
   387  	Expr        hcl.Expression
   388  	DependsOn   []hcl.Traversal
   389  	Sensitive   bool
   390  
   391  	Preconditions []*CheckRule
   392  
   393  	DescriptionSet bool
   394  	SensitiveSet   bool
   395  
   396  	DeclRange hcl.Range
   397  }
   398  
   399  func decodeOutputBlock(block *hcl.Block, override bool) (*Output, hcl.Diagnostics) {
   400  	var diags hcl.Diagnostics
   401  
   402  	o := &Output{
   403  		Name:      block.Labels[0],
   404  		DeclRange: block.DefRange,
   405  	}
   406  
   407  	schema := outputBlockSchema
   408  	if override {
   409  		schema = schemaForOverrides(schema)
   410  	}
   411  
   412  	content, moreDiags := block.Body.Content(schema)
   413  	diags = append(diags, moreDiags...)
   414  
   415  	if !hclsyntax.ValidIdentifier(o.Name) {
   416  		diags = append(diags, &hcl.Diagnostic{
   417  			Severity: hcl.DiagError,
   418  			Summary:  "Invalid output name",
   419  			Detail:   badIdentifierDetail,
   420  			Subject:  &block.LabelRanges[0],
   421  		})
   422  	}
   423  
   424  	if attr, exists := content.Attributes["description"]; exists {
   425  		valDiags := gohcl.DecodeExpression(attr.Expr, nil, &o.Description)
   426  		diags = append(diags, valDiags...)
   427  		o.DescriptionSet = true
   428  	}
   429  
   430  	if attr, exists := content.Attributes["value"]; exists {
   431  		o.Expr = attr.Expr
   432  	}
   433  
   434  	if attr, exists := content.Attributes["sensitive"]; exists {
   435  		valDiags := gohcl.DecodeExpression(attr.Expr, nil, &o.Sensitive)
   436  		diags = append(diags, valDiags...)
   437  		o.SensitiveSet = true
   438  	}
   439  
   440  	if attr, exists := content.Attributes["depends_on"]; exists {
   441  		deps, depsDiags := decodeDependsOn(attr)
   442  		diags = append(diags, depsDiags...)
   443  		o.DependsOn = append(o.DependsOn, deps...)
   444  	}
   445  
   446  	for _, block := range content.Blocks {
   447  		switch block.Type {
   448  		case "precondition":
   449  			cr, moreDiags := decodeCheckRuleBlock(block, override)
   450  			diags = append(diags, moreDiags...)
   451  			o.Preconditions = append(o.Preconditions, cr)
   452  		case "postcondition":
   453  			diags = append(diags, &hcl.Diagnostic{
   454  				Severity: hcl.DiagError,
   455  				Summary:  "Postconditions are not allowed",
   456  				Detail:   "Output values can only have preconditions, not postconditions.",
   457  				Subject:  block.TypeRange.Ptr(),
   458  			})
   459  		default:
   460  			// The cases above should be exhaustive for all block types
   461  			// defined in the block type schema, so this shouldn't happen.
   462  			panic(fmt.Sprintf("unexpected lifecycle sub-block type %q", block.Type))
   463  		}
   464  	}
   465  
   466  	return o, diags
   467  }
   468  
   469  // Local represents a single entry from a "locals" block in a module or file.
   470  // The "locals" block itself is not represented, because it serves only to
   471  // provide context for us to interpret its contents.
   472  type Local struct {
   473  	Name string
   474  	Expr hcl.Expression
   475  
   476  	DeclRange hcl.Range
   477  }
   478  
   479  func decodeLocalsBlock(block *hcl.Block) ([]*Local, hcl.Diagnostics) {
   480  	attrs, diags := block.Body.JustAttributes()
   481  	if len(attrs) == 0 {
   482  		return nil, diags
   483  	}
   484  
   485  	locals := make([]*Local, 0, len(attrs))
   486  	for name, attr := range attrs {
   487  		if !hclsyntax.ValidIdentifier(name) {
   488  			diags = append(diags, &hcl.Diagnostic{
   489  				Severity: hcl.DiagError,
   490  				Summary:  "Invalid local value name",
   491  				Detail:   badIdentifierDetail,
   492  				Subject:  &attr.NameRange,
   493  			})
   494  		}
   495  
   496  		locals = append(locals, &Local{
   497  			Name:      name,
   498  			Expr:      attr.Expr,
   499  			DeclRange: attr.Range,
   500  		})
   501  	}
   502  	return locals, diags
   503  }
   504  
   505  // Addr returns the address of the local value declared by the receiver,
   506  // relative to its containing module.
   507  func (l *Local) Addr() addrs.LocalValue {
   508  	return addrs.LocalValue{
   509  		Name: l.Name,
   510  	}
   511  }
   512  
   513  var variableBlockSchema = &hcl.BodySchema{
   514  	Attributes: []hcl.AttributeSchema{
   515  		{
   516  			Name: "description",
   517  		},
   518  		{
   519  			Name: "default",
   520  		},
   521  		{
   522  			Name: "type",
   523  		},
   524  		{
   525  			Name: "sensitive",
   526  		},
   527  		{
   528  			Name: "nullable",
   529  		},
   530  	},
   531  	Blocks: []hcl.BlockHeaderSchema{
   532  		{
   533  			Type: "validation",
   534  		},
   535  	},
   536  }
   537  
   538  var outputBlockSchema = &hcl.BodySchema{
   539  	Attributes: []hcl.AttributeSchema{
   540  		{
   541  			Name: "description",
   542  		},
   543  		{
   544  			Name:     "value",
   545  			Required: true,
   546  		},
   547  		{
   548  			Name: "depends_on",
   549  		},
   550  		{
   551  			Name: "sensitive",
   552  		},
   553  	},
   554  	Blocks: []hcl.BlockHeaderSchema{
   555  		{Type: "precondition"},
   556  		{Type: "postcondition"},
   557  	},
   558  }