mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-25 01:00:16 -05:00
lang/exprs: "compilation" of CheckRule
The pattern of having the "compiler" logic in lang/exprs always deal with building suitable exprs.Valuers and configgraph just evaluating whatever it was given has been working out in other contexts like the resource to resource instance journey, so we'll follow that pattern for CheckRule too: If the evaluation of a CheckRule depends on some value that's generated by logic in configgraph then configgraph has a callback to ask the "compiler" layer to dynamically construct zero or more CheckRule objects that are pre-bound to the relevant value. For checks that only rely on the global scope, such as output value preconditions, they are still precompiled into a static slice in the parent object. This also continues the progress towards all of the "expression scopes" living with the "compiler" code instead of in configgraph, so that in future different modules can potentially be bound to differently-shaped scopes as we continue to evolve the language. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This commit is contained in:
@@ -7,6 +7,8 @@ package configgraph
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"iter"
|
||||
"maps"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
@@ -18,73 +20,87 @@ import (
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
)
|
||||
|
||||
// CheckRule represents an author-defined condition that must be true and
|
||||
// an error message to return if it isn't true.
|
||||
//
|
||||
// In many cases the result of a check rule depends on some other value
|
||||
// in a local scope, in which case the type that the check rules belong
|
||||
// to must include a callback function that returns check rules based
|
||||
// on that result rather than predefined inline check rules. The [exprs.Valuer]
|
||||
// objects in a [CheckRule] must be pre-bound to whatever local scope is
|
||||
// appropriate for the context where they are declared.
|
||||
//
|
||||
// If you have an [iter.Seq] of [*CheckRule], or something that you can
|
||||
// conveniently use as one, [checkAllRules] is a useful way to visit all
|
||||
// of them and react consistently to their results.
|
||||
type CheckRule struct {
|
||||
// Condition is the as-yet-unevaluated expression for deciding whether the
|
||||
// check is satisfied. Evaluation of this is delayed to allow providing
|
||||
// a local scope when the logic in the containing object actually evaluates
|
||||
// the check.
|
||||
Condition exprs.Evalable
|
||||
// ConditionValuer produces the boolean result which determines whether
|
||||
// the check passes. The result should be of type [cty.Bool] and should
|
||||
// be [cty.True] if the condition is satisfied or [cty.False] if it is
|
||||
// not.
|
||||
//
|
||||
// The valuer is also allowed to return an unknown value if it isn't
|
||||
// yet possible to decide whether the condition is satisfied. Null
|
||||
// values are not allowed and will cause the check to fail with "error"
|
||||
// status.
|
||||
ConditionValuer exprs.Valuer
|
||||
|
||||
// ErrorMessageRaw is the as-yet-unevaluated expression for producing an
|
||||
// error message when the condition does not pass.
|
||||
ErrorMessageRaw exprs.Evalable
|
||||
|
||||
// ParentScope is the scope where the check rule was declared,
|
||||
// which might need to be wrapped in a local child scope before actually
|
||||
// evaluating the condition and error message.
|
||||
ParentScope exprs.Scope
|
||||
|
||||
// EphemeralAllowed indicates whether the condition and error message are
|
||||
// allowed to be derived from ephemeral values. If not, the relevant
|
||||
// methods will return error diagnostics when ephemeral values emerge.
|
||||
EphemeralAllowed bool
|
||||
// ErrorMessageValuer returns a string value containing an error message
|
||||
// that should be used when the condition is not satified.
|
||||
//
|
||||
// The result is required to be known and non-null. If this valuer
|
||||
// fails to evaluate with error diagnostics then those error diagnostics
|
||||
// will be returned along with a generic error message and the check
|
||||
// will fail with the "error" status.
|
||||
//
|
||||
// This valuer is used only when [ConditionValuer] returns [cty.False].
|
||||
ErrorMessageValuer exprs.Valuer
|
||||
|
||||
// DeclSourceRange is a source range that the module author would consider
|
||||
// to represent the declaration of this check rule, for use in error
|
||||
// messages that describe which rule was responsible for detecting a
|
||||
// failure.
|
||||
DeclSourceRange tfdiags.SourceRange
|
||||
}
|
||||
|
||||
func (r *CheckRule) Check(ctx context.Context, scopeBuilder exprs.ChildScopeBuilder) (checks.Status, tfdiags.Diagnostics) {
|
||||
scope := r.childScope(ctx, scopeBuilder)
|
||||
rawResult, diags := exprs.Evaluate(ctx, r.Condition, scope)
|
||||
func (r *CheckRule) Check(ctx context.Context) (checks.Status, cty.ValueMarks, tfdiags.Diagnostics) {
|
||||
rawResult, diags := r.ConditionValuer.Value(ctx)
|
||||
rawResult, resultMarks := rawResult.Unmark()
|
||||
if diags.HasErrors() {
|
||||
return checks.StatusError, diags
|
||||
resultMarks[exprs.EvalError] = struct{}{}
|
||||
return checks.StatusError, resultMarks, diags
|
||||
}
|
||||
rawResult, err := convert.Convert(rawResult, cty.Bool)
|
||||
if err == nil && rawResult.IsNull() {
|
||||
err = fmt.Errorf("value must not be null")
|
||||
}
|
||||
if err == nil && rawResult.HasMark(marks.Sensitive) {
|
||||
if _, sensitive := resultMarks[marks.Sensitive]; err == nil && sensitive {
|
||||
err = fmt.Errorf("must not be derived from a sensitive value")
|
||||
// TODO: Also annotate the diagnostic with the "caused by sensitive"
|
||||
// annotation, so that the diagnostic renderer can describe where
|
||||
// the sensitive values might have come from.
|
||||
}
|
||||
if err == nil && rawResult.HasMark(marks.Ephemeral) && !r.EphemeralAllowed {
|
||||
err = fmt.Errorf("must not be derived from an ephemeral value")
|
||||
}
|
||||
if err != nil {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid check condition",
|
||||
Detail: fmt.Sprintf("Invalid result for check condition expression: %s.", tfdiags.FormatError(err)),
|
||||
Subject: r.Condition.EvalableSourceRange().ToHCL().Ptr(),
|
||||
Subject: r.ConditionValuer.ValueSourceRange().ToHCL().Ptr(),
|
||||
})
|
||||
return checks.StatusError, diags
|
||||
resultMarks[exprs.EvalError] = struct{}{}
|
||||
return checks.StatusError, resultMarks, diags
|
||||
}
|
||||
if !rawResult.IsKnown() {
|
||||
return checks.StatusUnknown, diags
|
||||
return checks.StatusUnknown, resultMarks, diags
|
||||
}
|
||||
// TODO: Handle "deprecated" marks, adding any deprecation-related
|
||||
// diagnostics into diags.
|
||||
rawResult, _ = rawResult.Unmark() // marks dealt with above
|
||||
if rawResult.True() {
|
||||
return checks.StatusPass, diags
|
||||
return checks.StatusPass, resultMarks, diags
|
||||
}
|
||||
return checks.StatusFail, diags
|
||||
return checks.StatusFail, resultMarks, diags
|
||||
}
|
||||
|
||||
func (r *CheckRule) ErrorMessage(ctx context.Context, scopeBuilder exprs.ChildScopeBuilder) (string, tfdiags.Diagnostics) {
|
||||
scope := r.childScope(ctx, scopeBuilder)
|
||||
rawResult, diags := exprs.Evaluate(ctx, r.ErrorMessageRaw, scope)
|
||||
func (r *CheckRule) ErrorMessage(ctx context.Context) (string, tfdiags.Diagnostics) {
|
||||
rawResult, diags := r.ErrorMessageValuer.Value(ctx)
|
||||
if diags.HasErrors() {
|
||||
return "", diags
|
||||
}
|
||||
@@ -98,9 +114,6 @@ func (r *CheckRule) ErrorMessage(ctx context.Context, scopeBuilder exprs.ChildSc
|
||||
// annotation, so that the diagnostic renderer can describe where
|
||||
// the sensitive values might have come from.
|
||||
}
|
||||
if err == nil && rawResult.HasMark(marks.Ephemeral) && !r.EphemeralAllowed {
|
||||
err = fmt.Errorf("must not be derived from an ephemeral value")
|
||||
}
|
||||
if err == nil && !rawResult.IsKnown() {
|
||||
err = fmt.Errorf("derived from value that is not yet known")
|
||||
// TODO: Also annotate the diagnostic with the "caused by unknown"
|
||||
@@ -112,7 +125,7 @@ func (r *CheckRule) ErrorMessage(ctx context.Context, scopeBuilder exprs.ChildSc
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid error message for check",
|
||||
Detail: fmt.Sprintf("Invalid result for error message expression: %s.", tfdiags.FormatError(err)),
|
||||
Subject: r.Condition.EvalableSourceRange().ToHCL().Ptr(),
|
||||
Subject: r.ErrorMessageValuer.ValueSourceRange().ToHCL().Ptr(),
|
||||
})
|
||||
return "", diags
|
||||
}
|
||||
@@ -124,7 +137,12 @@ func (r *CheckRule) ErrorMessage(ctx context.Context, scopeBuilder exprs.ChildSc
|
||||
|
||||
// ConditionRange returns the source range where the condition expression was declared.
|
||||
func (r *CheckRule) ConditionRange() tfdiags.SourceRange {
|
||||
return r.Condition.EvalableSourceRange()
|
||||
if condRng := r.ConditionValuer.ValueSourceRange(); condRng != nil {
|
||||
return *condRng
|
||||
}
|
||||
// If the condition valuer doesn't have its own range then we'll
|
||||
// use the check rule's overall declaration range as a fallback.
|
||||
return r.DeclRange()
|
||||
}
|
||||
|
||||
// DeclRange returns the source range where this check was declared.
|
||||
@@ -132,6 +150,39 @@ func (r *CheckRule) DeclRange() tfdiags.SourceRange {
|
||||
return r.DeclSourceRange
|
||||
}
|
||||
|
||||
func (r *CheckRule) childScope(ctx context.Context, builder exprs.ChildScopeBuilder) exprs.Scope {
|
||||
return builder.Build(ctx, r.ParentScope)
|
||||
// CheckAllRules deals with the boilerplate of evaluating a series of
|
||||
// [CheckRule] objects and reacting to their results.
|
||||
//
|
||||
// Evaluates each rule in turn and then calls handleResult for each one,
|
||||
// passing the final status, and the error message if and only if the
|
||||
// status is [checks.StatusFail].
|
||||
//
|
||||
// handleResult may choose to return diagnostics to add to the final
|
||||
// aggregate set of diagnostics, but should typically add error diagnostics
|
||||
// only if the status is [checks.StatusFail] because check rules generate
|
||||
// their own error diagnostics for totally-invalid cases that yield
|
||||
// [checks.StatusError].
|
||||
//
|
||||
// The results are a set of all of the cty marks on the condition results
|
||||
// of the rules and an aggregate set of diagnostics mixing any
|
||||
// automatically-generated usage errors with failure-related diagonstics
|
||||
// returned by handleResult. A caller should typically transfer all of
|
||||
// the returned marks to whatever values were being checked to reflect
|
||||
// that the final value was effectively "derived from" the check results.
|
||||
func CheckAllRules(ctx context.Context, rules iter.Seq[*CheckRule], handleResult func(ruleDeclRange tfdiags.SourceRange, status checks.Status, errMsg string) tfdiags.Diagnostics) (cty.ValueMarks, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
marks := cty.NewValueMarks()
|
||||
for rule := range rules {
|
||||
status, moreMarks, moreDiags := rule.Check(ctx)
|
||||
maps.Copy(marks, moreMarks)
|
||||
diags = diags.Append(moreDiags)
|
||||
var errMsg string // empty unless StatusFail
|
||||
if status == checks.StatusFail {
|
||||
errMsg, moreDiags = rule.ErrorMessage(ctx)
|
||||
diags = diags.Append(moreDiags)
|
||||
}
|
||||
moreDiags = handleResult(rule.DeclSourceRange, status, errMsg)
|
||||
diags = diags.Append(moreDiags)
|
||||
}
|
||||
return marks, diags
|
||||
}
|
||||
|
||||
@@ -8,13 +8,13 @@ package configgraph
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"iter"
|
||||
|
||||
"github.com/apparentlymart/go-workgraph/workgraph"
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/ext/typeexpr"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/convert"
|
||||
"github.com/zclconf/go-cty/cty/function"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/checks"
|
||||
@@ -40,15 +40,15 @@ type InputVariable struct {
|
||||
// TODO: Default value
|
||||
// TODO: ForceEphemeral, ForceSensitive
|
||||
|
||||
// ValidationRules are user-defined checks that must succeed for the
|
||||
// Validation rules are user-defined checks that must succeed for the
|
||||
// final value to be considered valid for use in downstream expressions.
|
||||
//
|
||||
// The checking and error message evaluation for these rules must be
|
||||
// performed in a child scope where the raw value is directly exposed
|
||||
// under the same symbol where it would normally appear, because
|
||||
// otherwise checking these rules would depend on the success of these
|
||||
// very rules and so there would be a self-reference error.
|
||||
ValidationRules []CheckRule
|
||||
// CompileValidationRules takes the value of the variable after
|
||||
// type conversion and built-in validation rules have been applied to
|
||||
// it, and returns a sequence of compiled [CheckRule] objects that
|
||||
// test whether the author's configured conditions have been met
|
||||
// for the given value.
|
||||
CompileValidationRules func(ctx context.Context, value cty.Value) iter.Seq[*CheckRule]
|
||||
}
|
||||
|
||||
var _ exprs.Valuer = (*InputVariable)(nil)
|
||||
@@ -80,45 +80,36 @@ func (i *InputVariable) Value(ctx context.Context) (cty.Value, tfdiags.Diagnosti
|
||||
Detail: fmt.Sprintf("Unsuitable value for variable %q: %s.", i.Addr.Variable.Name, tfdiags.FormatError(err)),
|
||||
Subject: MaybeHCLSourceRange(i.ValueSourceRange()),
|
||||
})
|
||||
finalV = exprs.AsEvalError(cty.UnknownVal(i.TargetType.WithoutOptionalAttributesDeep()))
|
||||
finalV = cty.UnknownVal(i.TargetType.WithoutOptionalAttributesDeep())
|
||||
}
|
||||
|
||||
// TODO: Probably need to factor this part out into a separate function
|
||||
// so that we can collect up check results for inclusion in the checks
|
||||
// summary in the plan or state, but for now we're not worrying about
|
||||
// that because it's pretty rarely-used functionality.
|
||||
scopeBuilder := func(ctx context.Context, parent exprs.Scope) exprs.Scope {
|
||||
return &inputVariableValidationScope{
|
||||
wantName: i.Addr.Variable.Name,
|
||||
parentScope: parent,
|
||||
finalVal: finalV,
|
||||
}
|
||||
}
|
||||
for _, rule := range i.ValidationRules {
|
||||
status, moreDiags := rule.Check(ctx, scopeBuilder)
|
||||
diags = diags.Append(moreDiags)
|
||||
if status == checks.StatusFail {
|
||||
errMsg, moreDiags := rule.ErrorMessage(ctx, scopeBuilder)
|
||||
diags = diags.Append(moreDiags)
|
||||
if !moreDiags.HasErrors() {
|
||||
// Once we have our converted and prepared value we can finally compile
|
||||
// the validation rules against it and then check them.
|
||||
validationMarks, moreDiags := CheckAllRules(ctx,
|
||||
i.CompileValidationRules(ctx, finalV),
|
||||
func(ruleDeclRange tfdiags.SourceRange, status checks.Status, errMsg string) tfdiags.Diagnostics {
|
||||
var diags tfdiags.Diagnostics
|
||||
if status == checks.StatusFail {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid value for input variable",
|
||||
Detail: fmt.Sprintf("%s\n\nThis problem was reported by the validation rule at %s.", errMsg, rule.DeclRange().StartString()),
|
||||
Subject: rule.ConditionRange().ToHCL().Ptr(),
|
||||
Detail: fmt.Sprintf("%s\n\nThis problem was reported by the validation rule at %s.", errMsg, ruleDeclRange.StartString()),
|
||||
Subject: i.ValueSourceRange().ToHCL().Ptr(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return diags
|
||||
},
|
||||
)
|
||||
diags = diags.Append(moreDiags)
|
||||
|
||||
if diags.HasErrors() {
|
||||
// If we found any problems then we'll use an unknown result of the
|
||||
// expected type so that downstream expressions will only report
|
||||
// new problems and not consequences of the problems we already
|
||||
// reported.
|
||||
finalV = exprs.AsEvalError(cty.UnknownVal(i.TargetType.WithoutOptionalAttributesDeep()))
|
||||
finalV = exprs.AsEvalError(cty.UnknownVal(i.TargetType.WithoutOptionalAttributesDeep())).WithMarks(validationMarks)
|
||||
}
|
||||
return finalV, diags
|
||||
return finalV.WithMarks(validationMarks), diags
|
||||
}
|
||||
|
||||
// ValueSourceRange implements exprs.Valuer.
|
||||
@@ -149,67 +140,3 @@ func (i *InputVariable) AnnounceAllGraphevalRequests(announce func(workgraph.Req
|
||||
// those. Should our Value method be evaluating those through a
|
||||
// grapheval.Once so that they can have their own RequestInfo values?
|
||||
}
|
||||
|
||||
// inputVariableValidationScope is a specialized [exprs.Scope] implementation
|
||||
// that forces returning a constant value when accessing a specific input
|
||||
// variable directly, but otherwise just passes everything else through from
|
||||
// a parent scope.
|
||||
//
|
||||
// This is used for evaluating validation rules for an [InputVariable], where
|
||||
// we need to be able to evaluate an expression referring to the variable
|
||||
// as part of deciding the final value of the variable and so if we didn't
|
||||
// handle it directly then there would be a self-reference error.
|
||||
type inputVariableValidationScope struct {
|
||||
varTable exprs.SymbolTable
|
||||
wantName string
|
||||
parentScope exprs.Scope
|
||||
finalVal cty.Value
|
||||
}
|
||||
|
||||
var _ exprs.Scope = (*inputVariableValidationScope)(nil)
|
||||
var _ exprs.SymbolTable = (*inputVariableValidationScope)(nil)
|
||||
|
||||
// HandleInvalidStep implements exprs.Scope.
|
||||
func (i *inputVariableValidationScope) HandleInvalidStep(rng tfdiags.SourceRange) tfdiags.Diagnostics {
|
||||
return i.parentScope.HandleInvalidStep(rng)
|
||||
}
|
||||
|
||||
// ResolveAttr implements exprs.Scope.
|
||||
func (i *inputVariableValidationScope) ResolveAttr(ref hcl.TraverseAttr) (exprs.Attribute, tfdiags.Diagnostics) {
|
||||
if i.varTable == nil {
|
||||
// We're currently at the top-level scope where we're looking for
|
||||
// the "var." prefix to represent accessing any input variable at all.
|
||||
attr, diags := i.parentScope.ResolveAttr(ref)
|
||||
if diags.HasErrors() {
|
||||
return attr, diags
|
||||
}
|
||||
nestedTable := exprs.NestedSymbolTableFromAttribute(attr)
|
||||
if nestedTable != nil && ref.Name == "var" {
|
||||
// We'll return another instance of ourselves but with i.varTable
|
||||
// now populated to represent that the next step should try
|
||||
// to look up an input variable.
|
||||
return exprs.NestedSymbolTable(&inputVariableValidationScope{
|
||||
varTable: nestedTable,
|
||||
wantName: i.wantName,
|
||||
parentScope: i.parentScope,
|
||||
}), diags
|
||||
}
|
||||
// If it's anything other than the "var" prefix then we'll just return
|
||||
// whatever the parent scope returned directly, because we don't
|
||||
// need to be involved anymore.
|
||||
return attr, diags
|
||||
}
|
||||
|
||||
// If we get here then we're now nested under the "var." prefix, but
|
||||
// we only need to get involved if the reference is to the variable
|
||||
// we're currently validating.
|
||||
if ref.Name == i.wantName {
|
||||
return exprs.ValueOf(exprs.ConstantValuer(i.finalVal)), nil
|
||||
}
|
||||
return i.varTable.ResolveAttr(ref)
|
||||
}
|
||||
|
||||
// ResolveFunc implements exprs.Scope.
|
||||
func (i *inputVariableValidationScope) ResolveFunc(call *hcl.StaticCall) (function.Function, tfdiags.Diagnostics) {
|
||||
return i.parentScope.ResolveFunc(call)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ package configgraph
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/apparentlymart/go-workgraph/workgraph"
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
@@ -29,7 +30,11 @@ type OutputValue struct {
|
||||
|
||||
// Preconditions are user-defined checks that must succeed before OpenTofu
|
||||
// will evaluate the output value's expression.
|
||||
Preconditions []CheckRule
|
||||
//
|
||||
// Unlike some other uses of [CheckRule], output value preconditions don't
|
||||
// have any special local symbols in scope and so are precompiled as part of
|
||||
// the [OutputValue] they belong to.
|
||||
Preconditions []*CheckRule
|
||||
|
||||
// RawValue produces the "raw" value, as chosen by the caller of the
|
||||
// module, which has not yet been type-converted or validated.
|
||||
@@ -86,30 +91,30 @@ func (o *OutputValue) Value(ctx context.Context) (cty.Value, tfdiags.Diagnostics
|
||||
// would then replace a more general error message that might otherwise
|
||||
// be produced by expression evaluation.
|
||||
//
|
||||
// TODO: Probably need to factor this part out into a separate function
|
||||
// so that we can collect up check results for inclusion in the checks
|
||||
// summary in the plan or state, but for now we're not worrying about
|
||||
// that because it's pretty rarely-used functionality.
|
||||
for _, rule := range o.Preconditions {
|
||||
status, moreDiags := rule.Check(ctx, nil)
|
||||
diags = diags.Append(moreDiags)
|
||||
if status == checks.StatusFail {
|
||||
errMsg, moreDiags := rule.ErrorMessage(ctx, nil)
|
||||
diags = diags.Append(moreDiags)
|
||||
if !moreDiags.HasErrors() {
|
||||
// TODO: We probably need to find some way to collect up check results for
|
||||
// inclusion in the checks summary in the plan or state, but for now we're
|
||||
// not worrying about that because it's pretty rarely-used functionality.
|
||||
preconditionMarks, moreDiags := CheckAllRules(ctx,
|
||||
slices.Values(o.Preconditions),
|
||||
func(ruleDeclRange tfdiags.SourceRange, status checks.Status, errMsg string) tfdiags.Diagnostics {
|
||||
var diags tfdiags.Diagnostics
|
||||
if status == checks.StatusFail {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Output value precondition failed",
|
||||
Detail: fmt.Sprintf("%s\n\nThis problem was reported by the precondition at %s.", errMsg, rule.DeclRange().StartString()),
|
||||
Subject: rule.ConditionRange().ToHCL().Ptr(),
|
||||
Detail: fmt.Sprintf("%s\n\nThis problem was reported by the precondition at %s.", errMsg, ruleDeclRange.StartString()),
|
||||
Subject: MaybeHCLSourceRange(o.ValueSourceRange()),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return diags
|
||||
},
|
||||
)
|
||||
diags = diags.Append(moreDiags)
|
||||
|
||||
if diags.HasErrors() {
|
||||
// If the preconditions caused at least one error then we must
|
||||
// not proceed any further.
|
||||
return exprs.AsEvalError(cty.UnknownVal(o.TargetType.WithoutOptionalAttributesDeep())), diags
|
||||
return exprs.AsEvalError(cty.UnknownVal(o.TargetType.WithoutOptionalAttributesDeep())).WithMarks(preconditionMarks), diags
|
||||
}
|
||||
|
||||
rawV, diags := o.RawValue.Value(ctx)
|
||||
@@ -124,16 +129,16 @@ func (o *OutputValue) Value(ctx context.Context) (cty.Value, tfdiags.Diagnostics
|
||||
Detail: fmt.Sprintf("Unsuitable value for output value %q: %s.", o.Addr.OutputValue.Name, tfdiags.FormatError(err)),
|
||||
Subject: MaybeHCLSourceRange(o.ValueSourceRange()),
|
||||
})
|
||||
finalV = exprs.AsEvalError(cty.UnknownVal(o.TargetType.WithoutOptionalAttributesDeep()))
|
||||
finalV = exprs.AsEvalError(cty.UnknownVal(o.TargetType.WithoutOptionalAttributesDeep())).WithMarks(preconditionMarks)
|
||||
}
|
||||
|
||||
finalV = finalV.WithMarks(preconditionMarks)
|
||||
if o.ForceSensitive {
|
||||
finalV = finalV.Mark(marks.Sensitive)
|
||||
}
|
||||
if o.ForceEphemeral {
|
||||
finalV = finalV.Mark(marks.Ephemeral)
|
||||
}
|
||||
// TODO: deprecation marks
|
||||
|
||||
return finalV, diags
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ package eval
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"iter"
|
||||
"slices"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
@@ -239,11 +241,25 @@ func compileModuleInstanceInputVariables(_ context.Context, configs map[string]*
|
||||
}
|
||||
}
|
||||
ret[addr] = &configgraph.InputVariable{
|
||||
Addr: moduleInstAddr.InputVariable(name),
|
||||
RawValue: configgraph.ValuerOnce(rawValue),
|
||||
TargetType: vc.ConstraintType,
|
||||
TargetDefaults: vc.TypeDefaults,
|
||||
ValidationRules: compileCheckRules(vc.Validations, declScope, vc.Ephemeral),
|
||||
Addr: moduleInstAddr.InputVariable(name),
|
||||
RawValue: configgraph.ValuerOnce(rawValue),
|
||||
TargetType: vc.ConstraintType,
|
||||
TargetDefaults: vc.TypeDefaults,
|
||||
CompileValidationRules: func(ctx context.Context, value cty.Value) iter.Seq[*configgraph.CheckRule] {
|
||||
// For variable validation we need to use a special overlay
|
||||
// scope that resolves the single variable we are validating
|
||||
// to the given constant value but delegates everything else
|
||||
// to the parent scope. This overlay is important because
|
||||
// these checks are run as part of the normal process of
|
||||
// handling a reference to this variable, and so if we used
|
||||
// the normal scope here we'd be depending on our own result.
|
||||
childScope := &inputVariableValidationScope{
|
||||
wantName: name,
|
||||
parentScope: declScope,
|
||||
finalVal: value,
|
||||
}
|
||||
return compileCheckRules(vc.Validations, childScope)
|
||||
},
|
||||
}
|
||||
}
|
||||
return ret
|
||||
@@ -287,7 +303,7 @@ func compileModuleInstanceOutputValues(_ context.Context, configs map[string]*co
|
||||
|
||||
ForceSensitive: vc.Sensitive,
|
||||
ForceEphemeral: vc.Ephemeral,
|
||||
Preconditions: compileCheckRules(vc.Preconditions, declScope, vc.Ephemeral),
|
||||
Preconditions: slices.Collect(compileCheckRules(vc.Preconditions, declScope)),
|
||||
}
|
||||
}
|
||||
return ret
|
||||
@@ -424,20 +440,38 @@ func compileModuleInstanceProviderConfigs(ctx context.Context, configs map[strin
|
||||
}
|
||||
}
|
||||
|
||||
func compileCheckRules(configs []*configs.CheckRule, declScope exprs.Scope, ephemeralAllowed bool) []configgraph.CheckRule {
|
||||
ret := make([]configgraph.CheckRule, 0, len(configs))
|
||||
for _, config := range configs {
|
||||
ret = append(ret, configgraph.CheckRule{
|
||||
Condition: exprs.EvalableHCLExpression(config.Condition),
|
||||
ErrorMessageRaw: exprs.EvalableHCLExpression(config.ErrorMessage),
|
||||
ParentScope: declScope,
|
||||
EphemeralAllowed: ephemeralAllowed,
|
||||
DeclSourceRange: tfdiags.SourceRangeFromHCL(config.DeclRange),
|
||||
})
|
||||
func compileCheckRules(configs []*configs.CheckRule, evalScope exprs.Scope) iter.Seq[*configgraph.CheckRule] {
|
||||
// TODO: Maybe we need to allow the caller to impose additional constraints
|
||||
// on the result of the ConditionValuer here, such as disallowing the
|
||||
// use of ephemeral values outside of ephemeral resource
|
||||
// preconditions/postconditions. If so, perhaps we'd take an additional
|
||||
// argument for an optional callback function that takes the result of
|
||||
// the condition expression and can return additional diagnostics that
|
||||
// make sense for the specific context where the check rules are being used.
|
||||
return func(yield func(*configgraph.CheckRule) bool) {
|
||||
for _, config := range configs {
|
||||
compiled := &configgraph.CheckRule{
|
||||
ConditionValuer: exprs.NewClosure(
|
||||
exprs.EvalableHCLExpression(config.Condition),
|
||||
evalScope,
|
||||
),
|
||||
ErrorMessageValuer: exprs.NewClosure(
|
||||
exprs.EvalableHCLExpression(config.ErrorMessage),
|
||||
evalScope,
|
||||
),
|
||||
DeclSourceRange: tfdiags.SourceRangeFromHCL(config.DeclRange),
|
||||
}
|
||||
if !yield(compiled) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// resourceInstanceGlue is our implementation of [configgraph.ResourceInstanceGlue],
|
||||
// allowing our compiled [configgraph.ResourceInstance] objects to call back
|
||||
// to us for needs that require interacting with outside concerns like
|
||||
// provider plugins, an active plan or apply process, etc.
|
||||
type resourceInstanceGlue struct {
|
||||
validateConfig func(context.Context, cty.Value) tfdiags.Diagnostics
|
||||
getResultValue func(context.Context, cty.Value, configgraph.Maybe[*configgraph.ProviderInstance]) (cty.Value, tfdiags.Diagnostics)
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/opentofu/opentofu/internal/lang/eval/internal/configgraph"
|
||||
"github.com/opentofu/opentofu/internal/lang/exprs"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/function"
|
||||
)
|
||||
|
||||
@@ -370,3 +371,68 @@ func nounForModuleInstanceGlobalSymbol(symbol string) string {
|
||||
return "attribute" // generic fallback that we should avoid using by adding new names above as needed
|
||||
}
|
||||
}
|
||||
|
||||
// inputVariableValidationScope is a specialized [exprs.Scope] implementation
|
||||
// that forces returning a constant value when accessing a specific input
|
||||
// variable directly, but otherwise just passes everything else through from
|
||||
// a parent scope.
|
||||
//
|
||||
// This is used for evaluating validation rules for an [InputVariable], where
|
||||
// we need to be able to evaluate an expression referring to the variable
|
||||
// as part of deciding the final value of the variable and so if we didn't
|
||||
// handle it directly then there would be a self-reference error.
|
||||
type inputVariableValidationScope struct {
|
||||
varTable exprs.SymbolTable
|
||||
wantName string
|
||||
parentScope exprs.Scope
|
||||
finalVal cty.Value
|
||||
}
|
||||
|
||||
var _ exprs.Scope = (*inputVariableValidationScope)(nil)
|
||||
var _ exprs.SymbolTable = (*inputVariableValidationScope)(nil)
|
||||
|
||||
// HandleInvalidStep implements exprs.Scope.
|
||||
func (i *inputVariableValidationScope) HandleInvalidStep(rng tfdiags.SourceRange) tfdiags.Diagnostics {
|
||||
return i.parentScope.HandleInvalidStep(rng)
|
||||
}
|
||||
|
||||
// ResolveAttr implements exprs.Scope.
|
||||
func (i *inputVariableValidationScope) ResolveAttr(ref hcl.TraverseAttr) (exprs.Attribute, tfdiags.Diagnostics) {
|
||||
if i.varTable == nil {
|
||||
// We're currently at the top-level scope where we're looking for
|
||||
// the "var." prefix to represent accessing any input variable at all.
|
||||
attr, diags := i.parentScope.ResolveAttr(ref)
|
||||
if diags.HasErrors() {
|
||||
return attr, diags
|
||||
}
|
||||
nestedTable := exprs.NestedSymbolTableFromAttribute(attr)
|
||||
if nestedTable != nil && ref.Name == "var" {
|
||||
// We'll return another instance of ourselves but with i.varTable
|
||||
// now populated to represent that the next step should try
|
||||
// to look up an input variable.
|
||||
return exprs.NestedSymbolTable(&inputVariableValidationScope{
|
||||
varTable: nestedTable,
|
||||
wantName: i.wantName,
|
||||
parentScope: i.parentScope,
|
||||
finalVal: i.finalVal,
|
||||
}), diags
|
||||
}
|
||||
// If it's anything other than the "var" prefix then we'll just return
|
||||
// whatever the parent scope returned directly, because we don't
|
||||
// need to be involved anymore.
|
||||
return attr, diags
|
||||
}
|
||||
|
||||
// If we get here then we're now nested under the "var." prefix, but
|
||||
// we only need to get involved if the reference is to the variable
|
||||
// we're currently validating.
|
||||
if ref.Name == i.wantName {
|
||||
return exprs.ValueOf(exprs.ConstantValuer(i.finalVal)), nil
|
||||
}
|
||||
return i.varTable.ResolveAttr(ref)
|
||||
}
|
||||
|
||||
// ResolveFunc implements exprs.Scope.
|
||||
func (i *inputVariableValidationScope) ResolveFunc(call *hcl.StaticCall) (function.Function, tfdiags.Diagnostics) {
|
||||
return i.parentScope.ResolveFunc(call)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user