mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-24 08:00:17 -06:00
64fb36dc54
Signed-off-by: ollevche <ollevche@gmail.com> Signed-off-by: Oleksandr Levchenkov <ollevche@gmail.com> Co-authored-by: Janos <86970079+janosdebugs@users.noreply.github.com> Co-authored-by: Ronny Orot <ronny.orot@gmail.com>
583 lines
18 KiB
Go
583 lines
18 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package configs
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/ext/typeexpr"
|
|
"github.com/hashicorp/hcl/v2/gohcl"
|
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
|
"github.com/zclconf/go-cty/cty"
|
|
"github.com/zclconf/go-cty/cty/convert"
|
|
|
|
"github.com/opentofu/opentofu/internal/addrs"
|
|
)
|
|
|
|
// A consistent detail message for all "not a valid identifier" diagnostics.
|
|
const badIdentifierDetail = "A name must start with a letter or underscore and may contain only letters, digits, underscores, and dashes."
|
|
|
|
// Variable represents a "variable" block in a module or file.
|
|
type Variable struct {
|
|
Name string
|
|
Description string
|
|
Default cty.Value
|
|
|
|
// Type is the concrete type of the variable value.
|
|
Type cty.Type
|
|
// ConstraintType is used for decoding and type conversions, and may
|
|
// contain nested ObjectWithOptionalAttr types.
|
|
ConstraintType cty.Type
|
|
TypeDefaults *typeexpr.Defaults
|
|
|
|
ParsingMode VariableParsingMode
|
|
Validations []*CheckRule
|
|
Sensitive bool
|
|
|
|
DescriptionSet bool
|
|
SensitiveSet bool
|
|
|
|
// Nullable indicates that null is a valid value for this variable. Setting
|
|
// Nullable to false means that the module can expect this variable to
|
|
// never be null.
|
|
Nullable bool
|
|
NullableSet bool
|
|
|
|
DeclRange hcl.Range
|
|
}
|
|
|
|
func decodeVariableBlock(block *hcl.Block, override bool) (*Variable, hcl.Diagnostics) {
|
|
v := &Variable{
|
|
Name: block.Labels[0],
|
|
DeclRange: block.DefRange,
|
|
}
|
|
|
|
// Unless we're building an override, we'll set some defaults
|
|
// which we might override with attributes below. We leave these
|
|
// as zero-value in the override case so we can recognize whether
|
|
// or not they are set when we merge.
|
|
if !override {
|
|
v.Type = cty.DynamicPseudoType
|
|
v.ConstraintType = cty.DynamicPseudoType
|
|
v.ParsingMode = VariableParseLiteral
|
|
}
|
|
|
|
content, diags := block.Body.Content(variableBlockSchema)
|
|
|
|
if !hclsyntax.ValidIdentifier(v.Name) {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid variable name",
|
|
Detail: badIdentifierDetail,
|
|
Subject: &block.LabelRanges[0],
|
|
})
|
|
}
|
|
|
|
// Don't allow declaration of variables that would conflict with the
|
|
// reserved attribute and block type names in a "module" block, since
|
|
// these won't be usable for child modules.
|
|
for _, attr := range moduleBlockSchema.Attributes {
|
|
if attr.Name == v.Name {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid variable name",
|
|
Detail: fmt.Sprintf("The variable name %q is reserved due to its special meaning inside module blocks.", attr.Name),
|
|
Subject: &block.LabelRanges[0],
|
|
})
|
|
}
|
|
}
|
|
for _, blockS := range moduleBlockSchema.Blocks {
|
|
if blockS.Type == v.Name {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid variable name",
|
|
Detail: fmt.Sprintf("The variable name %q is reserved due to its special meaning inside module blocks.", blockS.Type),
|
|
Subject: &block.LabelRanges[0],
|
|
})
|
|
}
|
|
}
|
|
|
|
if attr, exists := content.Attributes["description"]; exists {
|
|
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Description)
|
|
diags = append(diags, valDiags...)
|
|
v.DescriptionSet = true
|
|
}
|
|
|
|
if attr, exists := content.Attributes["type"]; exists {
|
|
ty, tyDefaults, parseMode, tyDiags := decodeVariableType(attr.Expr)
|
|
diags = append(diags, tyDiags...)
|
|
v.ConstraintType = ty
|
|
v.TypeDefaults = tyDefaults
|
|
v.Type = ty.WithoutOptionalAttributesDeep()
|
|
v.ParsingMode = parseMode
|
|
}
|
|
|
|
if attr, exists := content.Attributes["sensitive"]; exists {
|
|
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Sensitive)
|
|
diags = append(diags, valDiags...)
|
|
v.SensitiveSet = true
|
|
}
|
|
|
|
if attr, exists := content.Attributes["nullable"]; exists {
|
|
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Nullable)
|
|
diags = append(diags, valDiags...)
|
|
v.NullableSet = true
|
|
} else {
|
|
// The current default is true, which is subject to change in a future
|
|
// language edition.
|
|
v.Nullable = true
|
|
}
|
|
|
|
if attr, exists := content.Attributes["default"]; exists {
|
|
val, valDiags := attr.Expr.Value(nil)
|
|
diags = append(diags, valDiags...)
|
|
|
|
// Convert the default to the expected type so we can catch invalid
|
|
// defaults early and allow later code to assume validity.
|
|
// Note that this depends on us having already processed any "type"
|
|
// attribute above.
|
|
// However, we can't do this if we're in an override file where
|
|
// the type might not be set; we'll catch that during merge.
|
|
if v.ConstraintType != cty.NilType {
|
|
var err error
|
|
// If the type constraint has defaults, we must apply those
|
|
// defaults to the variable default value before type conversion,
|
|
// unless the default value is null. Null is excluded from the
|
|
// type default application process as a special case, to allow
|
|
// nullable variables to have a null default value.
|
|
if v.TypeDefaults != nil && !val.IsNull() {
|
|
val = v.TypeDefaults.Apply(val)
|
|
}
|
|
val, err = convert.Convert(val, v.ConstraintType)
|
|
if err != nil {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid default value for variable",
|
|
Detail: fmt.Sprintf("This default value is not compatible with the variable's type constraint: %s.", err),
|
|
Subject: attr.Expr.Range().Ptr(),
|
|
})
|
|
val = cty.DynamicVal
|
|
}
|
|
}
|
|
|
|
if !v.Nullable && val.IsNull() {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid default value for variable",
|
|
Detail: "A null default value is not valid when nullable=false.",
|
|
Subject: attr.Expr.Range().Ptr(),
|
|
})
|
|
}
|
|
|
|
v.Default = val
|
|
}
|
|
|
|
for _, block := range content.Blocks {
|
|
switch block.Type {
|
|
|
|
case "validation":
|
|
vv, moreDiags := decodeVariableValidationBlock(v.Name, block, override)
|
|
diags = append(diags, moreDiags...)
|
|
v.Validations = append(v.Validations, vv)
|
|
|
|
default:
|
|
// The above cases should be exhaustive for all block types
|
|
// defined in variableBlockSchema
|
|
panic(fmt.Sprintf("unhandled block type %q", block.Type))
|
|
}
|
|
}
|
|
|
|
return v, diags
|
|
}
|
|
|
|
func decodeVariableType(expr hcl.Expression) (cty.Type, *typeexpr.Defaults, VariableParsingMode, hcl.Diagnostics) {
|
|
if exprIsNativeQuotedString(expr) {
|
|
// If a user provides the pre-0.12 form of variable type argument where
|
|
// the string values "string", "list" and "map" are accepted, we
|
|
// provide an error to point the user towards using the type system
|
|
// correctly has a hint.
|
|
// Only the native syntax ends up in this codepath; we handle the
|
|
// JSON syntax (which is, of course, quoted within the type system)
|
|
// in the normal codepath below.
|
|
val, diags := expr.Value(nil)
|
|
if diags.HasErrors() {
|
|
return cty.DynamicPseudoType, nil, VariableParseHCL, diags
|
|
}
|
|
str := val.AsString()
|
|
switch str {
|
|
case "string":
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid quoted type constraints",
|
|
Detail: "OpenTofu 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 OpenTofu. Remove the quotes around \"string\".",
|
|
Subject: expr.Range().Ptr(),
|
|
})
|
|
return cty.DynamicPseudoType, nil, VariableParseLiteral, diags
|
|
case "list":
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid quoted type constraints",
|
|
Detail: "OpenTofu 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 OpenTofu. Remove the quotes around \"list\" and write list(string) instead to explicitly indicate that the list elements are strings.",
|
|
Subject: expr.Range().Ptr(),
|
|
})
|
|
return cty.DynamicPseudoType, nil, VariableParseHCL, diags
|
|
case "map":
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid quoted type constraints",
|
|
Detail: "OpenTofu 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 OpenTofu. Remove the quotes around \"map\" and write map(string) instead to explicitly indicate that the map elements are strings.",
|
|
Subject: expr.Range().Ptr(),
|
|
})
|
|
return cty.DynamicPseudoType, nil, VariableParseHCL, diags
|
|
default:
|
|
return cty.DynamicPseudoType, nil, VariableParseHCL, hcl.Diagnostics{{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid legacy variable type hint",
|
|
Detail: `To provide a full type expression, remove the surrounding quotes and give the type expression directly.`,
|
|
Subject: expr.Range().Ptr(),
|
|
}}
|
|
}
|
|
}
|
|
|
|
// First we'll deal with some shorthand forms that the HCL-level type
|
|
// expression parser doesn't include. These both emulate pre-0.12 behavior
|
|
// of allowing a list or map of any element type as long as all of the
|
|
// elements are consistent. This is the same as list(any) or map(any).
|
|
switch hcl.ExprAsKeyword(expr) {
|
|
case "list":
|
|
return cty.List(cty.DynamicPseudoType), nil, VariableParseHCL, nil
|
|
case "map":
|
|
return cty.Map(cty.DynamicPseudoType), nil, VariableParseHCL, nil
|
|
}
|
|
|
|
ty, typeDefaults, diags := typeexpr.TypeConstraintWithDefaults(expr)
|
|
if diags.HasErrors() {
|
|
return cty.DynamicPseudoType, nil, VariableParseHCL, diags
|
|
}
|
|
|
|
switch {
|
|
case ty.IsPrimitiveType():
|
|
// Primitive types use literal parsing.
|
|
return ty, typeDefaults, VariableParseLiteral, diags
|
|
default:
|
|
// Everything else uses HCL parsing
|
|
return ty, typeDefaults, VariableParseHCL, diags
|
|
}
|
|
}
|
|
|
|
func (v *Variable) Addr() addrs.InputVariable {
|
|
return addrs.InputVariable{Name: v.Name}
|
|
}
|
|
|
|
// Required returns true if this variable is required to be set by the caller,
|
|
// or false if there is a default value that will be used when it isn't set.
|
|
func (v *Variable) Required() bool {
|
|
return v.Default == cty.NilVal
|
|
}
|
|
|
|
// VariableParsingMode defines how values of a particular variable given by
|
|
// text-only mechanisms (command line arguments and environment variables)
|
|
// should be parsed to produce the final value.
|
|
type VariableParsingMode rune
|
|
|
|
// VariableParseLiteral is a variable parsing mode that just takes the given
|
|
// string directly as a cty.String value.
|
|
const VariableParseLiteral VariableParsingMode = 'L'
|
|
|
|
// VariableParseHCL is a variable parsing mode that attempts to parse the given
|
|
// string as an HCL expression and returns the result.
|
|
const VariableParseHCL VariableParsingMode = 'H'
|
|
|
|
// Parse uses the receiving parsing mode to process the given variable value
|
|
// string, returning the result along with any diagnostics.
|
|
//
|
|
// A VariableParsingMode does not know the expected type of the corresponding
|
|
// variable, so it's the caller's responsibility to attempt to convert the
|
|
// result to the appropriate type and return to the user any diagnostics that
|
|
// conversion may produce.
|
|
//
|
|
// The given name is used to create a synthetic filename in case any diagnostics
|
|
// must be generated about the given string value. This should be the name
|
|
// of the root module variable whose value will be populated from the given
|
|
// string.
|
|
//
|
|
// If the returned diagnostics has errors, the returned value may not be
|
|
// valid.
|
|
func (m VariableParsingMode) Parse(name, value string) (cty.Value, hcl.Diagnostics) {
|
|
switch m {
|
|
case VariableParseLiteral:
|
|
return cty.StringVal(value), nil
|
|
case VariableParseHCL:
|
|
fakeFilename := fmt.Sprintf("<value for var.%s>", name)
|
|
expr, diags := hclsyntax.ParseExpression([]byte(value), fakeFilename, hcl.Pos{Line: 1, Column: 1})
|
|
if diags.HasErrors() {
|
|
return cty.DynamicVal, diags
|
|
}
|
|
val, valDiags := expr.Value(nil)
|
|
diags = append(diags, valDiags...)
|
|
return val, diags
|
|
default:
|
|
// Should never happen
|
|
panic(fmt.Errorf("Parse called on invalid VariableParsingMode %#v", m))
|
|
}
|
|
}
|
|
|
|
// decodeVariableValidationBlock is a wrapper around decodeCheckRuleBlock
|
|
// that imposes the additional rule that the condition expression can refer
|
|
// only to an input variable of the given name.
|
|
func decodeVariableValidationBlock(varName string, block *hcl.Block, override bool) (*CheckRule, hcl.Diagnostics) {
|
|
vv, diags := decodeCheckRuleBlock(block, override)
|
|
if vv.Condition != nil {
|
|
// The validation condition can only refer to the variable itself,
|
|
// to ensure that the variable declaration can't create additional
|
|
// edges in the dependency graph.
|
|
goodRefs := 0
|
|
for _, traversal := range vv.Condition.Variables() {
|
|
ref, moreDiags := addrs.ParseRef(traversal)
|
|
if !moreDiags.HasErrors() {
|
|
if addr, ok := ref.Subject.(addrs.InputVariable); ok {
|
|
if addr.Name == varName {
|
|
goodRefs++
|
|
continue // Reference is valid
|
|
}
|
|
}
|
|
}
|
|
// If we fall out here then the reference is invalid.
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid reference in variable validation",
|
|
Detail: fmt.Sprintf("The condition for variable %q can only refer to the variable itself, using var.%s.", varName, varName),
|
|
Subject: traversal.SourceRange().Ptr(),
|
|
})
|
|
}
|
|
if goodRefs < 1 {
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid variable validation condition",
|
|
Detail: fmt.Sprintf("The condition for variable %q must refer to var.%s in order to test incoming values.", varName, varName),
|
|
Subject: vv.Condition.Range().Ptr(),
|
|
})
|
|
}
|
|
}
|
|
|
|
if vv.ErrorMessage != nil {
|
|
// The same applies to the validation error message, except that
|
|
// references are not required. A string literal is a valid error
|
|
// message.
|
|
goodRefs := 0
|
|
for _, traversal := range vv.ErrorMessage.Variables() {
|
|
ref, moreDiags := addrs.ParseRef(traversal)
|
|
if !moreDiags.HasErrors() {
|
|
if addr, ok := ref.Subject.(addrs.InputVariable); ok {
|
|
if addr.Name == varName {
|
|
goodRefs++
|
|
continue // Reference is valid
|
|
}
|
|
}
|
|
}
|
|
// If we fall out here then the reference is invalid.
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid reference in variable validation",
|
|
Detail: fmt.Sprintf("The error message for variable %q can only refer to the variable itself, using var.%s.", varName, varName),
|
|
Subject: traversal.SourceRange().Ptr(),
|
|
})
|
|
}
|
|
}
|
|
|
|
return vv, diags
|
|
}
|
|
|
|
// Output represents an "output" block in a module or file.
|
|
type Output struct {
|
|
Name string
|
|
Description string
|
|
Expr hcl.Expression
|
|
DependsOn []hcl.Traversal
|
|
Sensitive bool
|
|
|
|
Preconditions []*CheckRule
|
|
|
|
DescriptionSet bool
|
|
SensitiveSet bool
|
|
|
|
DeclRange hcl.Range
|
|
|
|
// IsOverridden indicates if the output is being overridden. It's used in
|
|
// testing framework to not evaluate expression and use OverrideValue instead.
|
|
IsOverridden bool
|
|
// OverrideValue is only valid if IsOverridden is set to true. The value
|
|
// should be used instead of evaluated expression. It's possible to have no
|
|
// OverrideValue even with IsOverridden is set to true.
|
|
OverrideValue *cty.Value
|
|
}
|
|
|
|
func decodeOutputBlock(block *hcl.Block, override bool) (*Output, hcl.Diagnostics) {
|
|
var diags hcl.Diagnostics
|
|
|
|
o := &Output{
|
|
Name: block.Labels[0],
|
|
DeclRange: block.DefRange,
|
|
}
|
|
|
|
schema := outputBlockSchema
|
|
if override {
|
|
schema = schemaForOverrides(schema)
|
|
}
|
|
|
|
content, moreDiags := block.Body.Content(schema)
|
|
diags = append(diags, moreDiags...)
|
|
|
|
if !hclsyntax.ValidIdentifier(o.Name) {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid output name",
|
|
Detail: badIdentifierDetail,
|
|
Subject: &block.LabelRanges[0],
|
|
})
|
|
}
|
|
|
|
if attr, exists := content.Attributes["description"]; exists {
|
|
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &o.Description)
|
|
diags = append(diags, valDiags...)
|
|
o.DescriptionSet = true
|
|
}
|
|
|
|
if attr, exists := content.Attributes["value"]; exists {
|
|
o.Expr = attr.Expr
|
|
}
|
|
|
|
if attr, exists := content.Attributes["sensitive"]; exists {
|
|
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &o.Sensitive)
|
|
diags = append(diags, valDiags...)
|
|
o.SensitiveSet = true
|
|
}
|
|
|
|
if attr, exists := content.Attributes["depends_on"]; exists {
|
|
deps, depsDiags := decodeDependsOn(attr)
|
|
diags = append(diags, depsDiags...)
|
|
o.DependsOn = append(o.DependsOn, deps...)
|
|
}
|
|
|
|
for _, block := range content.Blocks {
|
|
switch block.Type {
|
|
case "precondition":
|
|
cr, moreDiags := decodeCheckRuleBlock(block, override)
|
|
diags = append(diags, moreDiags...)
|
|
o.Preconditions = append(o.Preconditions, cr)
|
|
case "postcondition":
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Postconditions are not allowed",
|
|
Detail: "Output values can only have preconditions, not postconditions.",
|
|
Subject: block.TypeRange.Ptr(),
|
|
})
|
|
default:
|
|
// The cases above should be exhaustive for all block types
|
|
// defined in the block type schema, so this shouldn't happen.
|
|
panic(fmt.Sprintf("unexpected lifecycle sub-block type %q", block.Type))
|
|
}
|
|
}
|
|
|
|
return o, diags
|
|
}
|
|
|
|
func (o *Output) Addr() addrs.OutputValue {
|
|
return addrs.OutputValue{Name: o.Name}
|
|
}
|
|
|
|
// Local represents a single entry from a "locals" block in a module or file.
|
|
// The "locals" block itself is not represented, because it serves only to
|
|
// provide context for us to interpret its contents.
|
|
type Local struct {
|
|
Name string
|
|
Expr hcl.Expression
|
|
|
|
DeclRange hcl.Range
|
|
}
|
|
|
|
func decodeLocalsBlock(block *hcl.Block) ([]*Local, hcl.Diagnostics) {
|
|
attrs, diags := block.Body.JustAttributes()
|
|
if len(attrs) == 0 {
|
|
return nil, diags
|
|
}
|
|
|
|
locals := make([]*Local, 0, len(attrs))
|
|
for name, attr := range attrs {
|
|
if !hclsyntax.ValidIdentifier(name) {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid local value name",
|
|
Detail: badIdentifierDetail,
|
|
Subject: &attr.NameRange,
|
|
})
|
|
}
|
|
|
|
locals = append(locals, &Local{
|
|
Name: name,
|
|
Expr: attr.Expr,
|
|
DeclRange: attr.Range,
|
|
})
|
|
}
|
|
return locals, diags
|
|
}
|
|
|
|
// Addr returns the address of the local value declared by the receiver,
|
|
// relative to its containing module.
|
|
func (l *Local) Addr() addrs.LocalValue {
|
|
return addrs.LocalValue{
|
|
Name: l.Name,
|
|
}
|
|
}
|
|
|
|
var variableBlockSchema = &hcl.BodySchema{
|
|
Attributes: []hcl.AttributeSchema{
|
|
{
|
|
Name: "description",
|
|
},
|
|
{
|
|
Name: "default",
|
|
},
|
|
{
|
|
Name: "type",
|
|
},
|
|
{
|
|
Name: "sensitive",
|
|
},
|
|
{
|
|
Name: "nullable",
|
|
},
|
|
},
|
|
Blocks: []hcl.BlockHeaderSchema{
|
|
{
|
|
Type: "validation",
|
|
},
|
|
},
|
|
}
|
|
|
|
var outputBlockSchema = &hcl.BodySchema{
|
|
Attributes: []hcl.AttributeSchema{
|
|
{
|
|
Name: "description",
|
|
},
|
|
{
|
|
Name: "value",
|
|
Required: true,
|
|
},
|
|
{
|
|
Name: "depends_on",
|
|
},
|
|
{
|
|
Name: "sensitive",
|
|
},
|
|
},
|
|
Blocks: []hcl.BlockHeaderSchema{
|
|
{Type: "precondition"},
|
|
{Type: "postcondition"},
|
|
},
|
|
}
|