Merge pull request #30401 from hashicorp/f-preconditions-postconditions-rebased
Preconditions and Postconditions
This commit is contained in:
commit
0634c9437a
|
@ -0,0 +1,163 @@
|
||||||
|
package configs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/hashicorp/hcl/v2"
|
||||||
|
"github.com/hashicorp/hcl/v2/gohcl"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CheckRule represents a configuration-defined validation rule, precondition,
|
||||||
|
// or postcondition. Blocks of this sort can appear in a few different places
|
||||||
|
// in configuration, including "validation" blocks for variables,
|
||||||
|
// and "precondition" and "postcondition" blocks for resources.
|
||||||
|
type CheckRule struct {
|
||||||
|
// Condition is an expression that must evaluate to true if the condition
|
||||||
|
// holds or false if it does not. If the expression produces an error then
|
||||||
|
// that's considered to be a bug in the module defining the check.
|
||||||
|
//
|
||||||
|
// The available variables in a condition expression vary depending on what
|
||||||
|
// a check is attached to. For example, validation rules attached to
|
||||||
|
// input variables can only refer to the variable that is being validated.
|
||||||
|
Condition hcl.Expression
|
||||||
|
|
||||||
|
// ErrorMessage is one or more full sentences, which would need to be in
|
||||||
|
// English for consistency with the rest of the error message output but
|
||||||
|
// can in practice be in any language as long as it ends with a period.
|
||||||
|
// The message should describe what is required for the condition to return
|
||||||
|
// true in a way that would make sense to a caller of the module.
|
||||||
|
ErrorMessage string
|
||||||
|
|
||||||
|
DeclRange hcl.Range
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeCheckRuleBlock decodes the contents of the given block as a check rule.
|
||||||
|
//
|
||||||
|
// Unlike most of our "decode..." functions, this one can be applied to blocks
|
||||||
|
// of various types as long as their body structures are "check-shaped". The
|
||||||
|
// function takes the containing block only because some error messages will
|
||||||
|
// refer to its location, and the returned object's DeclRange will be the
|
||||||
|
// block's header.
|
||||||
|
func decodeCheckRuleBlock(block *hcl.Block, override bool) (*CheckRule, hcl.Diagnostics) {
|
||||||
|
var diags hcl.Diagnostics
|
||||||
|
cr := &CheckRule{
|
||||||
|
DeclRange: block.DefRange,
|
||||||
|
}
|
||||||
|
|
||||||
|
if override {
|
||||||
|
// For now we'll just forbid overriding check blocks, to simplify
|
||||||
|
// the initial design. If we can find a clear use-case for overriding
|
||||||
|
// checks in override files and there's a way to define it that
|
||||||
|
// isn't confusing then we could relax this.
|
||||||
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: fmt.Sprintf("Can't override %s blocks", block.Type),
|
||||||
|
Detail: fmt.Sprintf("Override files cannot override %q blocks.", block.Type),
|
||||||
|
Subject: cr.DeclRange.Ptr(),
|
||||||
|
})
|
||||||
|
return cr, diags
|
||||||
|
}
|
||||||
|
|
||||||
|
content, moreDiags := block.Body.Content(checkRuleBlockSchema)
|
||||||
|
diags = append(diags, moreDiags...)
|
||||||
|
|
||||||
|
if attr, exists := content.Attributes["condition"]; exists {
|
||||||
|
cr.Condition = attr.Expr
|
||||||
|
|
||||||
|
if len(cr.Condition.Variables()) == 0 {
|
||||||
|
// A condition expression that doesn't refer to any variable is
|
||||||
|
// pointless, because its result would always be a constant.
|
||||||
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: fmt.Sprintf("Invalid %s expression", block.Type),
|
||||||
|
Detail: "The condition expression must refer to at least one object from elsewhere in the configuration, or else its result would not be checking anything.",
|
||||||
|
Subject: cr.Condition.Range().Ptr(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if attr, exists := content.Attributes["error_message"]; exists {
|
||||||
|
moreDiags := gohcl.DecodeExpression(attr.Expr, nil, &cr.ErrorMessage)
|
||||||
|
diags = append(diags, moreDiags...)
|
||||||
|
if !moreDiags.HasErrors() {
|
||||||
|
const errSummary = "Invalid validation error message"
|
||||||
|
switch {
|
||||||
|
case cr.ErrorMessage == "":
|
||||||
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: errSummary,
|
||||||
|
Detail: "An empty string is not a valid nor useful error message.",
|
||||||
|
Subject: attr.Expr.Range().Ptr(),
|
||||||
|
})
|
||||||
|
case !looksLikeSentences(cr.ErrorMessage):
|
||||||
|
// Because we're going to include this string verbatim as part
|
||||||
|
// of a bigger error message written in our usual style in
|
||||||
|
// English, we'll require the given error message to conform
|
||||||
|
// to that. We might relax this in future if e.g. we start
|
||||||
|
// presenting these error messages in a different way, or if
|
||||||
|
// Terraform starts supporting producing error messages in
|
||||||
|
// other human languages, etc.
|
||||||
|
// For pragmatism we also allow sentences ending with
|
||||||
|
// exclamation points, but we don't mention it explicitly here
|
||||||
|
// because that's not really consistent with the Terraform UI
|
||||||
|
// writing style.
|
||||||
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: errSummary,
|
||||||
|
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.",
|
||||||
|
Subject: attr.Expr.Range().Ptr(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cr, diags
|
||||||
|
}
|
||||||
|
|
||||||
|
// looksLikeSentence is a simple heuristic that encourages writing error
|
||||||
|
// messages that will be presentable when included as part of a larger
|
||||||
|
// Terraform error diagnostic whose other text is written in the Terraform
|
||||||
|
// UI writing style.
|
||||||
|
//
|
||||||
|
// This is intentionally not a very strong validation since we're assuming
|
||||||
|
// that module authors want to write good messages and might just need a nudge
|
||||||
|
// about Terraform's specific style, rather than that they are going to try
|
||||||
|
// to work around these rules to write a lower-quality message.
|
||||||
|
func looksLikeSentences(s string) bool {
|
||||||
|
if len(s) < 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
runes := []rune(s) // HCL guarantees that all strings are valid UTF-8
|
||||||
|
first := runes[0]
|
||||||
|
last := runes[len(runes)-1]
|
||||||
|
|
||||||
|
// If the first rune is a letter then it must be an uppercase letter.
|
||||||
|
// (This will only see the first rune in a multi-rune combining sequence,
|
||||||
|
// but the first rune is generally the letter if any are, and if not then
|
||||||
|
// we'll just ignore it because we're primarily expecting English messages
|
||||||
|
// right now anyway, for consistency with all of Terraform's other output.)
|
||||||
|
if unicode.IsLetter(first) && !unicode.IsUpper(first) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// The string must be at least one full sentence, which implies having
|
||||||
|
// sentence-ending punctuation.
|
||||||
|
// (This assumes that if a sentence ends with quotes then the period
|
||||||
|
// will be outside the quotes, which is consistent with Terraform's UI
|
||||||
|
// writing style.)
|
||||||
|
return last == '.' || last == '?' || last == '!'
|
||||||
|
}
|
||||||
|
|
||||||
|
var checkRuleBlockSchema = &hcl.BodySchema{
|
||||||
|
Attributes: []hcl.AttributeSchema{
|
||||||
|
{
|
||||||
|
Name: "condition",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "error_message",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
|
@ -209,6 +209,55 @@ func checkModuleExperiments(m *Module) hcl.Diagnostics {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !m.ActiveExperiments.Has(experiments.PreconditionsPostconditions) {
|
||||||
|
for _, r := range m.ManagedResources {
|
||||||
|
for _, c := range r.Preconditions {
|
||||||
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: "Preconditions are experimental",
|
||||||
|
Detail: "The resource preconditions feature is currently an opt-in experiment, subject to change in future releases based on feedback.\n\nActivate the feature for this module by adding preconditions_postconditions to the list of active experiments.",
|
||||||
|
Subject: c.DeclRange.Ptr(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, c := range r.Postconditions {
|
||||||
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: "Postconditions are experimental",
|
||||||
|
Detail: "The resource preconditions feature is currently an opt-in experiment, subject to change in future releases based on feedback.\n\nActivate the feature for this module by adding preconditions_postconditions to the list of active experiments.",
|
||||||
|
Subject: c.DeclRange.Ptr(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, r := range m.DataResources {
|
||||||
|
for _, c := range r.Preconditions {
|
||||||
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: "Preconditions are experimental",
|
||||||
|
Detail: "The resource preconditions feature is currently an opt-in experiment, subject to change in future releases based on feedback.\n\nActivate the feature for this module by adding preconditions_postconditions to the list of active experiments.",
|
||||||
|
Subject: c.DeclRange.Ptr(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, c := range r.Postconditions {
|
||||||
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: "Postconditions are experimental",
|
||||||
|
Detail: "The resource preconditions feature is currently an opt-in experiment, subject to change in future releases based on feedback.\n\nActivate the feature for this module by adding preconditions_postconditions to the list of active experiments.",
|
||||||
|
Subject: c.DeclRange.Ptr(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, o := range m.Outputs {
|
||||||
|
for _, c := range o.Preconditions {
|
||||||
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: "Preconditions are experimental",
|
||||||
|
Detail: "The output value preconditions feature is currently an opt-in experiment, subject to change in future releases based on feedback.\n\nActivate the feature for this module by adding preconditions_postconditions to the list of active experiments.",
|
||||||
|
Subject: c.DeclRange.Ptr(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return diags
|
return diags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ package configs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"unicode"
|
|
||||||
|
|
||||||
"github.com/hashicorp/hcl/v2"
|
"github.com/hashicorp/hcl/v2"
|
||||||
"github.com/hashicorp/hcl/v2/gohcl"
|
"github.com/hashicorp/hcl/v2/gohcl"
|
||||||
|
@ -30,7 +29,7 @@ type Variable struct {
|
||||||
ConstraintType cty.Type
|
ConstraintType cty.Type
|
||||||
|
|
||||||
ParsingMode VariableParsingMode
|
ParsingMode VariableParsingMode
|
||||||
Validations []*VariableValidation
|
Validations []*CheckRule
|
||||||
Sensitive bool
|
Sensitive bool
|
||||||
|
|
||||||
DescriptionSet bool
|
DescriptionSet bool
|
||||||
|
@ -308,53 +307,12 @@ func (m VariableParsingMode) Parse(name, value string) (cty.Value, hcl.Diagnosti
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// VariableValidation represents a configuration-defined validation rule
|
// decodeVariableValidationBlock is a wrapper around decodeCheckRuleBlock
|
||||||
// for a particular input variable, given as a "validation" block inside
|
// that imposes the additional rule that the condition expression can refer
|
||||||
// a "variable" block.
|
// only to an input variable of the given name.
|
||||||
type VariableValidation struct {
|
func decodeVariableValidationBlock(varName string, block *hcl.Block, override bool) (*CheckRule, hcl.Diagnostics) {
|
||||||
// Condition is an expression that refers to the variable being tested
|
vv, diags := decodeCheckRuleBlock(block, override)
|
||||||
// and contains no other references. The expression must return true
|
if vv.Condition != nil {
|
||||||
// to indicate that the value is valid or false to indicate that it is
|
|
||||||
// invalid. If the expression produces an error, that's considered a bug
|
|
||||||
// in the module defining the validation rule, not an error in the caller.
|
|
||||||
Condition hcl.Expression
|
|
||||||
|
|
||||||
// ErrorMessage is one or more full sentences, which would need to be in
|
|
||||||
// English for consistency with the rest of the error message output but
|
|
||||||
// can in practice be in any language as long as it ends with a period.
|
|
||||||
// The message should describe what is required for the condition to return
|
|
||||||
// true in a way that would make sense to a caller of the module.
|
|
||||||
ErrorMessage string
|
|
||||||
|
|
||||||
DeclRange hcl.Range
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeVariableValidationBlock(varName string, block *hcl.Block, override bool) (*VariableValidation, hcl.Diagnostics) {
|
|
||||||
var diags hcl.Diagnostics
|
|
||||||
vv := &VariableValidation{
|
|
||||||
DeclRange: block.DefRange,
|
|
||||||
}
|
|
||||||
|
|
||||||
if override {
|
|
||||||
// For now we'll just forbid overriding validation blocks, to simplify
|
|
||||||
// the initial design. If we can find a clear use-case for overriding
|
|
||||||
// validations in override files and there's a way to define it that
|
|
||||||
// isn't confusing then we could relax this.
|
|
||||||
diags = diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Can't override variable validation rules",
|
|
||||||
Detail: "Variable \"validation\" blocks cannot be used in override files.",
|
|
||||||
Subject: vv.DeclRange.Ptr(),
|
|
||||||
})
|
|
||||||
return vv, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
content, moreDiags := block.Body.Content(variableValidationBlockSchema)
|
|
||||||
diags = append(diags, moreDiags...)
|
|
||||||
|
|
||||||
if attr, exists := content.Attributes["condition"]; exists {
|
|
||||||
vv.Condition = attr.Expr
|
|
||||||
|
|
||||||
// The validation condition can only refer to the variable itself,
|
// The validation condition can only refer to the variable itself,
|
||||||
// to ensure that the variable declaration can't create additional
|
// to ensure that the variable declaration can't create additional
|
||||||
// edges in the dependency graph.
|
// edges in the dependency graph.
|
||||||
|
@ -382,83 +340,14 @@ func decodeVariableValidationBlock(varName string, block *hcl.Block, override bo
|
||||||
Severity: hcl.DiagError,
|
Severity: hcl.DiagError,
|
||||||
Summary: "Invalid variable validation condition",
|
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),
|
Detail: fmt.Sprintf("The condition for variable %q must refer to var.%s in order to test incoming values.", varName, varName),
|
||||||
Subject: attr.Expr.Range().Ptr(),
|
Subject: vv.Condition.Range().Ptr(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if attr, exists := content.Attributes["error_message"]; exists {
|
|
||||||
moreDiags := gohcl.DecodeExpression(attr.Expr, nil, &vv.ErrorMessage)
|
|
||||||
diags = append(diags, moreDiags...)
|
|
||||||
if !moreDiags.HasErrors() {
|
|
||||||
const errSummary = "Invalid validation error message"
|
|
||||||
switch {
|
|
||||||
case vv.ErrorMessage == "":
|
|
||||||
diags = diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: errSummary,
|
|
||||||
Detail: "An empty string is not a valid nor useful error message.",
|
|
||||||
Subject: attr.Expr.Range().Ptr(),
|
|
||||||
})
|
|
||||||
case !looksLikeSentences(vv.ErrorMessage):
|
|
||||||
// Because we're going to include this string verbatim as part
|
|
||||||
// of a bigger error message written in our usual style in
|
|
||||||
// English, we'll require the given error message to conform
|
|
||||||
// to that. We might relax this in future if e.g. we start
|
|
||||||
// presenting these error messages in a different way, or if
|
|
||||||
// Terraform starts supporting producing error messages in
|
|
||||||
// other human languages, etc.
|
|
||||||
// For pragmatism we also allow sentences ending with
|
|
||||||
// exclamation points, but we don't mention it explicitly here
|
|
||||||
// because that's not really consistent with the Terraform UI
|
|
||||||
// writing style.
|
|
||||||
diags = diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: errSummary,
|
|
||||||
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.",
|
|
||||||
Subject: attr.Expr.Range().Ptr(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return vv, diags
|
return vv, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
// looksLikeSentence is a simple heuristic that encourages writing error
|
|
||||||
// messages that will be presentable when included as part of a larger
|
|
||||||
// Terraform error diagnostic whose other text is written in the Terraform
|
|
||||||
// UI writing style.
|
|
||||||
//
|
|
||||||
// This is intentionally not a very strong validation since we're assuming
|
|
||||||
// that module authors want to write good messages and might just need a nudge
|
|
||||||
// about Terraform's specific style, rather than that they are going to try
|
|
||||||
// to work around these rules to write a lower-quality message.
|
|
||||||
func looksLikeSentences(s string) bool {
|
|
||||||
if len(s) < 1 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
runes := []rune(s) // HCL guarantees that all strings are valid UTF-8
|
|
||||||
first := runes[0]
|
|
||||||
last := runes[len(runes)-1]
|
|
||||||
|
|
||||||
// If the first rune is a letter then it must be an uppercase letter.
|
|
||||||
// (This will only see the first rune in a multi-rune combining sequence,
|
|
||||||
// but the first rune is generally the letter if any are, and if not then
|
|
||||||
// we'll just ignore it because we're primarily expecting English messages
|
|
||||||
// right now anyway, for consistency with all of Terraform's other output.)
|
|
||||||
if unicode.IsLetter(first) && !unicode.IsUpper(first) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// The string must be at least one full sentence, which implies having
|
|
||||||
// sentence-ending punctuation.
|
|
||||||
// (This assumes that if a sentence ends with quotes then the period
|
|
||||||
// will be outside the quotes, which is consistent with Terraform's UI
|
|
||||||
// writing style.)
|
|
||||||
return last == '.' || last == '?' || last == '!'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output represents an "output" block in a module or file.
|
// Output represents an "output" block in a module or file.
|
||||||
type Output struct {
|
type Output struct {
|
||||||
Name string
|
Name string
|
||||||
|
@ -467,6 +356,8 @@ type Output struct {
|
||||||
DependsOn []hcl.Traversal
|
DependsOn []hcl.Traversal
|
||||||
Sensitive bool
|
Sensitive bool
|
||||||
|
|
||||||
|
Preconditions []*CheckRule
|
||||||
|
|
||||||
DescriptionSet bool
|
DescriptionSet bool
|
||||||
SensitiveSet bool
|
SensitiveSet bool
|
||||||
|
|
||||||
|
@ -520,6 +411,26 @@ func decodeOutputBlock(block *hcl.Block, override bool) (*Output, hcl.Diagnostic
|
||||||
o.DependsOn = append(o.DependsOn, deps...)
|
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
|
return o, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -592,19 +503,6 @@ var variableBlockSchema = &hcl.BodySchema{
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var variableValidationBlockSchema = &hcl.BodySchema{
|
|
||||||
Attributes: []hcl.AttributeSchema{
|
|
||||||
{
|
|
||||||
Name: "condition",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "error_message",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var outputBlockSchema = &hcl.BodySchema{
|
var outputBlockSchema = &hcl.BodySchema{
|
||||||
Attributes: []hcl.AttributeSchema{
|
Attributes: []hcl.AttributeSchema{
|
||||||
{
|
{
|
||||||
|
@ -621,4 +519,8 @@ var outputBlockSchema = &hcl.BodySchema{
|
||||||
Name: "sensitive",
|
Name: "sensitive",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Blocks: []hcl.BlockHeaderSchema{
|
||||||
|
{Type: "precondition"},
|
||||||
|
{Type: "postcondition"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -142,14 +142,14 @@ func (p *Parser) loadConfigFile(path string, override bool) (*File, hcl.Diagnost
|
||||||
}
|
}
|
||||||
|
|
||||||
case "resource":
|
case "resource":
|
||||||
cfg, cfgDiags := decodeResourceBlock(block)
|
cfg, cfgDiags := decodeResourceBlock(block, override)
|
||||||
diags = append(diags, cfgDiags...)
|
diags = append(diags, cfgDiags...)
|
||||||
if cfg != nil {
|
if cfg != nil {
|
||||||
file.ManagedResources = append(file.ManagedResources, cfg)
|
file.ManagedResources = append(file.ManagedResources, cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
case "data":
|
case "data":
|
||||||
cfg, cfgDiags := decodeDataBlock(block)
|
cfg, cfgDiags := decodeDataBlock(block, override)
|
||||||
diags = append(diags, cfgDiags...)
|
diags = append(diags, cfgDiags...)
|
||||||
if cfg != nil {
|
if cfg != nil {
|
||||||
file.DataResources = append(file.DataResources, cfg)
|
file.DataResources = append(file.DataResources, cfg)
|
||||||
|
|
|
@ -97,7 +97,7 @@ func TestParserLoadConfigFileFailureMessages(t *testing.T) {
|
||||||
{
|
{
|
||||||
"invalid-files/data-resource-lifecycle.tf",
|
"invalid-files/data-resource-lifecycle.tf",
|
||||||
hcl.DiagError,
|
hcl.DiagError,
|
||||||
"Unsupported lifecycle block",
|
"Invalid data resource lifecycle argument",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"invalid-files/variable-type-unknown.tf",
|
"invalid-files/variable-type-unknown.tf",
|
||||||
|
|
|
@ -22,6 +22,9 @@ type Resource struct {
|
||||||
ProviderConfigRef *ProviderConfigRef
|
ProviderConfigRef *ProviderConfigRef
|
||||||
Provider addrs.Provider
|
Provider addrs.Provider
|
||||||
|
|
||||||
|
Preconditions []*CheckRule
|
||||||
|
Postconditions []*CheckRule
|
||||||
|
|
||||||
DependsOn []hcl.Traversal
|
DependsOn []hcl.Traversal
|
||||||
|
|
||||||
// Managed is populated only for Mode = addrs.ManagedResourceMode,
|
// Managed is populated only for Mode = addrs.ManagedResourceMode,
|
||||||
|
@ -81,7 +84,7 @@ func (r *Resource) ProviderConfigAddr() addrs.LocalProviderConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeResourceBlock(block *hcl.Block) (*Resource, hcl.Diagnostics) {
|
func decodeResourceBlock(block *hcl.Block, override bool) (*Resource, hcl.Diagnostics) {
|
||||||
var diags hcl.Diagnostics
|
var diags hcl.Diagnostics
|
||||||
r := &Resource{
|
r := &Resource{
|
||||||
Mode: addrs.ManagedResourceMode,
|
Mode: addrs.ManagedResourceMode,
|
||||||
|
@ -237,6 +240,24 @@ func decodeResourceBlock(block *hcl.Block) (*Resource, hcl.Diagnostics) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, block := range lcContent.Blocks {
|
||||||
|
switch block.Type {
|
||||||
|
case "precondition", "postcondition":
|
||||||
|
cr, moreDiags := decodeCheckRuleBlock(block, override)
|
||||||
|
diags = append(diags, moreDiags...)
|
||||||
|
switch block.Type {
|
||||||
|
case "precondition":
|
||||||
|
r.Preconditions = append(r.Preconditions, cr)
|
||||||
|
case "postcondition":
|
||||||
|
r.Postconditions = append(r.Postconditions, cr)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// The cases above should be exhaustive for all block types
|
||||||
|
// defined in the lifecycle schema, so this shouldn't happen.
|
||||||
|
panic(fmt.Sprintf("unexpected lifecycle sub-block type %q", block.Type))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case "connection":
|
case "connection":
|
||||||
if seenConnection != nil {
|
if seenConnection != nil {
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
diags = append(diags, &hcl.Diagnostic{
|
||||||
|
@ -307,7 +328,7 @@ func decodeResourceBlock(block *hcl.Block) (*Resource, hcl.Diagnostics) {
|
||||||
return r, diags
|
return r, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeDataBlock(block *hcl.Block) (*Resource, hcl.Diagnostics) {
|
func decodeDataBlock(block *hcl.Block, override bool) (*Resource, hcl.Diagnostics) {
|
||||||
var diags hcl.Diagnostics
|
var diags hcl.Diagnostics
|
||||||
r := &Resource{
|
r := &Resource{
|
||||||
Mode: addrs.DataResourceMode,
|
Mode: addrs.DataResourceMode,
|
||||||
|
@ -368,6 +389,7 @@ func decodeDataBlock(block *hcl.Block) (*Resource, hcl.Diagnostics) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var seenEscapeBlock *hcl.Block
|
var seenEscapeBlock *hcl.Block
|
||||||
|
var seenLifecycle *hcl.Block
|
||||||
for _, block := range content.Blocks {
|
for _, block := range content.Blocks {
|
||||||
switch block.Type {
|
switch block.Type {
|
||||||
|
|
||||||
|
@ -391,21 +413,59 @@ func decodeDataBlock(block *hcl.Block) (*Resource, hcl.Diagnostics) {
|
||||||
// will see a blend of both.
|
// will see a blend of both.
|
||||||
r.Config = hcl.MergeBodies([]hcl.Body{r.Config, block.Body})
|
r.Config = hcl.MergeBodies([]hcl.Body{r.Config, block.Body})
|
||||||
|
|
||||||
// The rest of these are just here to reserve block type names for future use.
|
|
||||||
case "lifecycle":
|
case "lifecycle":
|
||||||
|
if seenLifecycle != nil {
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
diags = append(diags, &hcl.Diagnostic{
|
||||||
Severity: hcl.DiagError,
|
Severity: hcl.DiagError,
|
||||||
Summary: "Unsupported lifecycle block",
|
Summary: "Duplicate lifecycle block",
|
||||||
Detail: "Data resources do not have lifecycle settings, so a lifecycle block is not allowed.",
|
Detail: fmt.Sprintf("This resource already has a lifecycle block at %s.", seenLifecycle.DefRange),
|
||||||
Subject: &block.DefRange,
|
Subject: block.DefRange.Ptr(),
|
||||||
})
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seenLifecycle = block
|
||||||
|
|
||||||
|
lcContent, lcDiags := block.Body.Content(resourceLifecycleBlockSchema)
|
||||||
|
diags = append(diags, lcDiags...)
|
||||||
|
|
||||||
|
// All of the attributes defined for resource lifecycle are for
|
||||||
|
// managed resources only, so we can emit a common error message
|
||||||
|
// for any given attributes that HCL accepted.
|
||||||
|
for name, attr := range lcContent.Attributes {
|
||||||
|
diags = append(diags, &hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: "Invalid data resource lifecycle argument",
|
||||||
|
Detail: fmt.Sprintf("The lifecycle argument %q is defined only for managed resources (\"resource\" blocks), and is not valid for data resources.", name),
|
||||||
|
Subject: attr.NameRange.Ptr(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, block := range lcContent.Blocks {
|
||||||
|
switch block.Type {
|
||||||
|
case "precondition", "postcondition":
|
||||||
|
cr, moreDiags := decodeCheckRuleBlock(block, override)
|
||||||
|
diags = append(diags, moreDiags...)
|
||||||
|
switch block.Type {
|
||||||
|
case "precondition":
|
||||||
|
r.Preconditions = append(r.Preconditions, cr)
|
||||||
|
case "postcondition":
|
||||||
|
r.Postconditions = append(r.Postconditions, cr)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// The cases above should be exhaustive for all block types
|
||||||
|
// defined in the lifecycle schema, so this shouldn't happen.
|
||||||
|
panic(fmt.Sprintf("unexpected lifecycle sub-block type %q", block.Type))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
// Any other block types are ones we're reserving for future use,
|
||||||
|
// but don't have any defined meaning today.
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
diags = append(diags, &hcl.Diagnostic{
|
||||||
Severity: hcl.DiagError,
|
Severity: hcl.DiagError,
|
||||||
Summary: "Reserved block type name in data block",
|
Summary: "Reserved block type name in data block",
|
||||||
Detail: fmt.Sprintf("The block type name %q is reserved for use by Terraform in a future version.", block.Type),
|
Detail: fmt.Sprintf("The block type name %q is reserved for use by Terraform in a future version.", block.Type),
|
||||||
Subject: &block.TypeRange,
|
Subject: block.TypeRange.Ptr(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -551,13 +611,17 @@ var resourceBlockSchema = &hcl.BodySchema{
|
||||||
var dataBlockSchema = &hcl.BodySchema{
|
var dataBlockSchema = &hcl.BodySchema{
|
||||||
Attributes: commonResourceAttributes,
|
Attributes: commonResourceAttributes,
|
||||||
Blocks: []hcl.BlockHeaderSchema{
|
Blocks: []hcl.BlockHeaderSchema{
|
||||||
{Type: "lifecycle"}, // reserved for future use
|
{Type: "lifecycle"},
|
||||||
{Type: "locals"}, // reserved for future use
|
{Type: "locals"}, // reserved for future use
|
||||||
{Type: "_"}, // meta-argument escaping block
|
{Type: "_"}, // meta-argument escaping block
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var resourceLifecycleBlockSchema = &hcl.BodySchema{
|
var resourceLifecycleBlockSchema = &hcl.BodySchema{
|
||||||
|
// We tell HCL that these elements are all valid for both "resource"
|
||||||
|
// and "data" lifecycle blocks, but the rules are actually more restrictive
|
||||||
|
// than that. We deal with that after decoding so that we can return
|
||||||
|
// more specific error messages than HCL would typically return itself.
|
||||||
Attributes: []hcl.AttributeSchema{
|
Attributes: []hcl.AttributeSchema{
|
||||||
{
|
{
|
||||||
Name: "create_before_destroy",
|
Name: "create_before_destroy",
|
||||||
|
@ -569,4 +633,8 @@ var resourceLifecycleBlockSchema = &hcl.BodySchema{
|
||||||
Name: "ignore_changes",
|
Name: "ignore_changes",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Blocks: []hcl.BlockHeaderSchema{
|
||||||
|
{Type: "precondition"},
|
||||||
|
{Type: "postcondition"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
34
internal/configs/testdata/error-files/precondition-postcondition-constant.tf
vendored
Normal file
34
internal/configs/testdata/error-files/precondition-postcondition-constant.tf
vendored
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
resource "test" "test" {
|
||||||
|
lifecycle {
|
||||||
|
precondition {
|
||||||
|
condition = true # ERROR: Invalid precondition expression
|
||||||
|
error_message = "Must be true."
|
||||||
|
}
|
||||||
|
postcondition {
|
||||||
|
condition = true # ERROR: Invalid postcondition expression
|
||||||
|
error_message = "Must be true."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data "test" "test" {
|
||||||
|
lifecycle {
|
||||||
|
precondition {
|
||||||
|
condition = true # ERROR: Invalid precondition expression
|
||||||
|
error_message = "Must be true."
|
||||||
|
}
|
||||||
|
postcondition {
|
||||||
|
condition = true # ERROR: Invalid postcondition expression
|
||||||
|
error_message = "Must be true."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "test" {
|
||||||
|
value = ""
|
||||||
|
|
||||||
|
precondition {
|
||||||
|
condition = true # ERROR: Invalid precondition expression
|
||||||
|
error_message = "Must be true."
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
data "test" "foo" {
|
|
||||||
lifecycle {}
|
|
||||||
}
|
|
|
@ -1,5 +1,7 @@
|
||||||
data "example" "example" {
|
data "example" "example" {
|
||||||
lifecycle {
|
lifecycle {
|
||||||
# This block intentionally left blank
|
# The lifecycle arguments are not valid for data resources:
|
||||||
|
# only the precondition and postcondition blocks are allowed.
|
||||||
|
ignore_changes = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
resource "test" "test" {
|
||||||
|
lifecycle {
|
||||||
|
precondition { # ERROR: Preconditions are experimental
|
||||||
|
condition = path.module != ""
|
||||||
|
error_message = "Must be true."
|
||||||
|
}
|
||||||
|
postcondition { # ERROR: Postconditions are experimental
|
||||||
|
condition = path.module != ""
|
||||||
|
error_message = "Must be true."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data "test" "test" {
|
||||||
|
lifecycle {
|
||||||
|
precondition { # ERROR: Preconditions are experimental
|
||||||
|
condition = path.module != ""
|
||||||
|
error_message = "Must be true."
|
||||||
|
}
|
||||||
|
postcondition { # ERROR: Postconditions are experimental
|
||||||
|
condition = path.module != ""
|
||||||
|
error_message = "Must be true."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "test" {
|
||||||
|
value = ""
|
||||||
|
|
||||||
|
precondition { # ERROR: Preconditions are experimental
|
||||||
|
condition = path.module != ""
|
||||||
|
error_message = "Must be true."
|
||||||
|
}
|
||||||
|
}
|
38
internal/configs/testdata/warning-files/preconditions-postconditions-experiment.tf
vendored
Normal file
38
internal/configs/testdata/warning-files/preconditions-postconditions-experiment.tf
vendored
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
terraform {
|
||||||
|
experiments = [preconditions_postconditions] # WARNING: Experimental feature "preconditions_postconditions" is active
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "test" "test" {
|
||||||
|
lifecycle {
|
||||||
|
precondition {
|
||||||
|
condition = path.module != ""
|
||||||
|
error_message = "Must be true."
|
||||||
|
}
|
||||||
|
postcondition {
|
||||||
|
condition = path.module != ""
|
||||||
|
error_message = "Must be true."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data "test" "test" {
|
||||||
|
lifecycle {
|
||||||
|
precondition {
|
||||||
|
condition = path.module != ""
|
||||||
|
error_message = "Must be true."
|
||||||
|
}
|
||||||
|
postcondition {
|
||||||
|
condition = path.module != ""
|
||||||
|
error_message = "Must be true."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "test" {
|
||||||
|
value = ""
|
||||||
|
|
||||||
|
precondition {
|
||||||
|
condition = path.module != ""
|
||||||
|
error_message = "Must be true."
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ const (
|
||||||
ModuleVariableOptionalAttrs = Experiment("module_variable_optional_attrs")
|
ModuleVariableOptionalAttrs = Experiment("module_variable_optional_attrs")
|
||||||
SuppressProviderSensitiveAttrs = Experiment("provider_sensitive_attrs")
|
SuppressProviderSensitiveAttrs = Experiment("provider_sensitive_attrs")
|
||||||
ConfigDrivenMove = Experiment("config_driven_move")
|
ConfigDrivenMove = Experiment("config_driven_move")
|
||||||
|
PreconditionsPostconditions = Experiment("preconditions_postconditions")
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -26,6 +27,7 @@ func init() {
|
||||||
registerConcludedExperiment(SuppressProviderSensitiveAttrs, "Provider-defined sensitive attributes are now redacted by default, without enabling an experiment.")
|
registerConcludedExperiment(SuppressProviderSensitiveAttrs, "Provider-defined sensitive attributes are now redacted by default, without enabling an experiment.")
|
||||||
registerConcludedExperiment(ConfigDrivenMove, "Declarations of moved resource instances using \"moved\" blocks can now be used by default, without enabling an experiment.")
|
registerConcludedExperiment(ConfigDrivenMove, "Declarations of moved resource instances using \"moved\" blocks can now be used by default, without enabling an experiment.")
|
||||||
registerCurrentExperiment(ModuleVariableOptionalAttrs)
|
registerCurrentExperiment(ModuleVariableOptionalAttrs)
|
||||||
|
registerCurrentExperiment(PreconditionsPostconditions)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrent takes an experiment name and returns the experiment value
|
// GetCurrent takes an experiment name and returns the experiment value
|
||||||
|
|
|
@ -296,7 +296,7 @@ func (s *Scope) evalContext(refs []*addrs.Reference, selfAddr addrs.Referenceabl
|
||||||
// this codepath doesn't really "know about". If the "self"
|
// this codepath doesn't really "know about". If the "self"
|
||||||
// object starts being supported in more contexts later then
|
// object starts being supported in more contexts later then
|
||||||
// we'll need to adjust this message.
|
// we'll need to adjust this message.
|
||||||
Detail: `The "self" object is not available in this context. This object can be used only in resource provisioner and connection blocks.`,
|
Detail: `The "self" object is not available in this context. This object can be used only in resource provisioner, connection, and postcondition blocks.`,
|
||||||
Subject: ref.SourceRange.ToHCL().Ptr(),
|
Subject: ref.SourceRange.ToHCL().Ptr(),
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/hashicorp/terraform/internal/addrs"
|
"github.com/hashicorp/terraform/internal/addrs"
|
||||||
"github.com/hashicorp/terraform/internal/configs/configschema"
|
"github.com/hashicorp/terraform/internal/configs/configschema"
|
||||||
"github.com/hashicorp/terraform/internal/lang/marks"
|
"github.com/hashicorp/terraform/internal/lang/marks"
|
||||||
|
@ -736,3 +737,182 @@ resource "test_object" "b" {
|
||||||
t.Fatal("expected cycle error from apply")
|
t.Fatal("expected cycle error from apply")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContext2Apply_resourcePostcondition(t *testing.T) {
|
||||||
|
m := testModuleInline(t, map[string]string{
|
||||||
|
"main.tf": `
|
||||||
|
terraform {
|
||||||
|
experiments = [preconditions_postconditions]
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "boop" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "test_resource" "a" {
|
||||||
|
value = var.boop
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "test_resource" "b" {
|
||||||
|
value = test_resource.a.output
|
||||||
|
lifecycle {
|
||||||
|
postcondition {
|
||||||
|
condition = self.output != ""
|
||||||
|
error_message = "Output must not be blank."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "test_resource" "c" {
|
||||||
|
value = test_resource.b.output
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
|
||||||
|
p := testProvider("test")
|
||||||
|
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
|
||||||
|
ResourceTypes: map[string]*configschema.Block{
|
||||||
|
"test_resource": {
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"value": {
|
||||||
|
Type: cty.String,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"output": {
|
||||||
|
Type: cty.String,
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
|
||||||
|
m := req.ProposedNewState.AsValueMap()
|
||||||
|
m["output"] = cty.UnknownVal(cty.String)
|
||||||
|
|
||||||
|
resp.PlannedState = cty.ObjectVal(m)
|
||||||
|
resp.LegacyTypeSystem = true
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
ctx := testContext2(t, &ContextOpts{
|
||||||
|
Providers: map[addrs.Provider]providers.Factory{
|
||||||
|
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("condition pass", func(t *testing.T) {
|
||||||
|
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
|
||||||
|
Mode: plans.NormalMode,
|
||||||
|
SetVariables: InputValues{
|
||||||
|
"boop": &InputValue{
|
||||||
|
Value: cty.StringVal("boop"),
|
||||||
|
SourceType: ValueFromCLIArg,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assertNoErrors(t, diags)
|
||||||
|
if len(plan.Changes.Resources) != 3 {
|
||||||
|
t.Fatalf("unexpected plan changes: %#v", plan.Changes)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
|
||||||
|
m := req.PlannedState.AsValueMap()
|
||||||
|
m["output"] = cty.StringVal(fmt.Sprintf("new-%s", m["value"].AsString()))
|
||||||
|
|
||||||
|
resp.NewState = cty.ObjectVal(m)
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
state, diags := ctx.Apply(plan, m)
|
||||||
|
assertNoErrors(t, diags)
|
||||||
|
|
||||||
|
wantResourceAttrs := map[string]struct{ value, output string }{
|
||||||
|
"a": {"boop", "new-boop"},
|
||||||
|
"b": {"new-boop", "new-new-boop"},
|
||||||
|
"c": {"new-new-boop", "new-new-new-boop"},
|
||||||
|
}
|
||||||
|
for name, attrs := range wantResourceAttrs {
|
||||||
|
addr := mustResourceInstanceAddr(fmt.Sprintf("test_resource.%s", name))
|
||||||
|
r := state.ResourceInstance(addr)
|
||||||
|
rd, err := r.Current.Decode(cty.Object(map[string]cty.Type{
|
||||||
|
"value": cty.String,
|
||||||
|
"output": cty.String,
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error decoding test_resource.a: %s", err)
|
||||||
|
}
|
||||||
|
want := cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"value": cty.StringVal(attrs.value),
|
||||||
|
"output": cty.StringVal(attrs.output),
|
||||||
|
})
|
||||||
|
if !cmp.Equal(want, rd.Value, valueComparer) {
|
||||||
|
t.Errorf("wrong attrs for %s\n%s", addr, cmp.Diff(want, rd.Value, valueComparer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("condition fail", func(t *testing.T) {
|
||||||
|
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
|
||||||
|
Mode: plans.NormalMode,
|
||||||
|
SetVariables: InputValues{
|
||||||
|
"boop": &InputValue{
|
||||||
|
Value: cty.StringVal("boop"),
|
||||||
|
SourceType: ValueFromCLIArg,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assertNoErrors(t, diags)
|
||||||
|
if len(plan.Changes.Resources) != 3 {
|
||||||
|
t.Fatalf("unexpected plan changes: %#v", plan.Changes)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
|
||||||
|
m := req.PlannedState.AsValueMap()
|
||||||
|
|
||||||
|
// For the resource with a constraint, fudge the output to make the
|
||||||
|
// condition fail.
|
||||||
|
if value := m["value"].AsString(); value == "new-boop" {
|
||||||
|
m["output"] = cty.StringVal("")
|
||||||
|
} else {
|
||||||
|
m["output"] = cty.StringVal(fmt.Sprintf("new-%s", value))
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.NewState = cty.ObjectVal(m)
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
state, diags := ctx.Apply(plan, m)
|
||||||
|
if !diags.HasErrors() {
|
||||||
|
t.Fatal("succeeded; want errors")
|
||||||
|
}
|
||||||
|
if got, want := diags.Err().Error(), "Resource postcondition failed: Output must not be blank."; got != want {
|
||||||
|
t.Fatalf("wrong error:\ngot: %s\nwant: %q", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resources a and b should still be recorded in state
|
||||||
|
wantResourceAttrs := map[string]struct{ value, output string }{
|
||||||
|
"a": {"boop", "new-boop"},
|
||||||
|
"b": {"new-boop", ""},
|
||||||
|
}
|
||||||
|
for name, attrs := range wantResourceAttrs {
|
||||||
|
addr := mustResourceInstanceAddr(fmt.Sprintf("test_resource.%s", name))
|
||||||
|
r := state.ResourceInstance(addr)
|
||||||
|
rd, err := r.Current.Decode(cty.Object(map[string]cty.Type{
|
||||||
|
"value": cty.String,
|
||||||
|
"output": cty.String,
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error decoding test_resource.a: %s", err)
|
||||||
|
}
|
||||||
|
want := cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"value": cty.StringVal(attrs.value),
|
||||||
|
"output": cty.StringVal(attrs.output),
|
||||||
|
})
|
||||||
|
if !cmp.Equal(want, rd.Value, valueComparer) {
|
||||||
|
t.Errorf("wrong attrs for %s\n%s", addr, cmp.Diff(want, rd.Value, valueComparer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resource c should not be in state
|
||||||
|
if state.ResourceInstance(mustResourceInstanceAddr("test_resource.c")) != nil {
|
||||||
|
t.Error("test_resource.c should not exist in state, but is")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -2178,3 +2178,438 @@ func TestContext2Plan_moduleExpandOrphansResourceInstance(t *testing.T) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContext2Plan_resourcePreconditionPostcondition(t *testing.T) {
|
||||||
|
m := testModuleInline(t, map[string]string{
|
||||||
|
"main.tf": `
|
||||||
|
terraform {
|
||||||
|
experiments = [preconditions_postconditions]
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "boop" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "test_resource" "a" {
|
||||||
|
value = var.boop
|
||||||
|
lifecycle {
|
||||||
|
precondition {
|
||||||
|
condition = var.boop == "boop"
|
||||||
|
error_message = "Wrong boop."
|
||||||
|
}
|
||||||
|
postcondition {
|
||||||
|
condition = self.output != ""
|
||||||
|
error_message = "Output must not be blank."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
|
||||||
|
p := testProvider("test")
|
||||||
|
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
|
||||||
|
ResourceTypes: map[string]*configschema.Block{
|
||||||
|
"test_resource": {
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"value": {
|
||||||
|
Type: cty.String,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"output": {
|
||||||
|
Type: cty.String,
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := testContext2(t, &ContextOpts{
|
||||||
|
Providers: map[addrs.Provider]providers.Factory{
|
||||||
|
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("conditions pass", func(t *testing.T) {
|
||||||
|
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
|
||||||
|
m := req.ProposedNewState.AsValueMap()
|
||||||
|
m["output"] = cty.StringVal("bar")
|
||||||
|
|
||||||
|
resp.PlannedState = cty.ObjectVal(m)
|
||||||
|
resp.LegacyTypeSystem = true
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
|
||||||
|
Mode: plans.NormalMode,
|
||||||
|
SetVariables: InputValues{
|
||||||
|
"boop": &InputValue{
|
||||||
|
Value: cty.StringVal("boop"),
|
||||||
|
SourceType: ValueFromCLIArg,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assertNoErrors(t, diags)
|
||||||
|
for _, res := range plan.Changes.Resources {
|
||||||
|
switch res.Addr.String() {
|
||||||
|
case "test_resource.a":
|
||||||
|
if res.Action != plans.Create {
|
||||||
|
t.Fatalf("unexpected %s change for %s", res.Action, res.Addr)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatalf("unexpected %s change for %s", res.Action, res.Addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("precondition fail", func(t *testing.T) {
|
||||||
|
_, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
|
||||||
|
Mode: plans.NormalMode,
|
||||||
|
SetVariables: InputValues{
|
||||||
|
"boop": &InputValue{
|
||||||
|
Value: cty.StringVal("nope"),
|
||||||
|
SourceType: ValueFromCLIArg,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if !diags.HasErrors() {
|
||||||
|
t.Fatal("succeeded; want errors")
|
||||||
|
}
|
||||||
|
if got, want := diags.Err().Error(), "Resource precondition failed: Wrong boop."; got != want {
|
||||||
|
t.Fatalf("wrong error:\ngot: %s\nwant: %q", got, want)
|
||||||
|
}
|
||||||
|
if p.PlanResourceChangeCalled {
|
||||||
|
t.Errorf("Provider's PlanResourceChange was called; should'nt've been")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("postcondition fail", func(t *testing.T) {
|
||||||
|
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
|
||||||
|
m := req.ProposedNewState.AsValueMap()
|
||||||
|
m["output"] = cty.StringVal("")
|
||||||
|
|
||||||
|
resp.PlannedState = cty.ObjectVal(m)
|
||||||
|
resp.LegacyTypeSystem = true
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
_, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
|
||||||
|
Mode: plans.NormalMode,
|
||||||
|
SetVariables: InputValues{
|
||||||
|
"boop": &InputValue{
|
||||||
|
Value: cty.StringVal("boop"),
|
||||||
|
SourceType: ValueFromCLIArg,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if !diags.HasErrors() {
|
||||||
|
t.Fatal("succeeded; want errors")
|
||||||
|
}
|
||||||
|
if got, want := diags.Err().Error(), "Resource postcondition failed: Output must not be blank."; got != want {
|
||||||
|
t.Fatalf("wrong error:\ngot: %s\nwant: %q", got, want)
|
||||||
|
}
|
||||||
|
if !p.PlanResourceChangeCalled {
|
||||||
|
t.Errorf("Provider's PlanResourceChangeCalled wasn't called; should've been")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContext2Plan_dataSourcePreconditionPostcondition(t *testing.T) {
|
||||||
|
m := testModuleInline(t, map[string]string{
|
||||||
|
"main.tf": `
|
||||||
|
terraform {
|
||||||
|
experiments = [preconditions_postconditions]
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "boop" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
data "test_data_source" "a" {
|
||||||
|
foo = var.boop
|
||||||
|
lifecycle {
|
||||||
|
precondition {
|
||||||
|
condition = var.boop == "boop"
|
||||||
|
error_message = "Wrong boop."
|
||||||
|
}
|
||||||
|
postcondition {
|
||||||
|
condition = length(self.results) > 0
|
||||||
|
error_message = "Results cannot be empty."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "test_resource" "a" {
|
||||||
|
value = data.test_data_source.a.results[0]
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
|
||||||
|
p := testProvider("test")
|
||||||
|
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
|
||||||
|
ResourceTypes: map[string]*configschema.Block{
|
||||||
|
"test_resource": {
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"value": {
|
||||||
|
Type: cty.String,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
DataSources: map[string]*configschema.Block{
|
||||||
|
"test_data_source": {
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"foo": {
|
||||||
|
Type: cty.String,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"results": {
|
||||||
|
Type: cty.List(cty.String),
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := testContext2(t, &ContextOpts{
|
||||||
|
Providers: map[addrs.Provider]providers.Factory{
|
||||||
|
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("conditions pass", func(t *testing.T) {
|
||||||
|
p.ReadDataSourceResponse = &providers.ReadDataSourceResponse{
|
||||||
|
State: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"foo": cty.StringVal("boop"),
|
||||||
|
"results": cty.ListVal([]cty.Value{cty.StringVal("boop")}),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
|
||||||
|
Mode: plans.NormalMode,
|
||||||
|
SetVariables: InputValues{
|
||||||
|
"boop": &InputValue{
|
||||||
|
Value: cty.StringVal("boop"),
|
||||||
|
SourceType: ValueFromCLIArg,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assertNoErrors(t, diags)
|
||||||
|
for _, res := range plan.Changes.Resources {
|
||||||
|
switch res.Addr.String() {
|
||||||
|
case "test_resource.a":
|
||||||
|
if res.Action != plans.Create {
|
||||||
|
t.Fatalf("unexpected %s change for %s", res.Action, res.Addr)
|
||||||
|
}
|
||||||
|
case "data.test_data_source.a":
|
||||||
|
if res.Action != plans.Read {
|
||||||
|
t.Fatalf("unexpected %s change for %s", res.Action, res.Addr)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatalf("unexpected %s change for %s", res.Action, res.Addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("precondition fail", func(t *testing.T) {
|
||||||
|
_, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
|
||||||
|
Mode: plans.NormalMode,
|
||||||
|
SetVariables: InputValues{
|
||||||
|
"boop": &InputValue{
|
||||||
|
Value: cty.StringVal("nope"),
|
||||||
|
SourceType: ValueFromCLIArg,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if !diags.HasErrors() {
|
||||||
|
t.Fatal("succeeded; want errors")
|
||||||
|
}
|
||||||
|
if got, want := diags.Err().Error(), "Resource precondition failed: Wrong boop."; got != want {
|
||||||
|
t.Fatalf("wrong error:\ngot: %s\nwant: %q", got, want)
|
||||||
|
}
|
||||||
|
if p.ReadDataSourceCalled {
|
||||||
|
t.Errorf("Provider's ReadResource was called; should'nt've been")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("postcondition fail", func(t *testing.T) {
|
||||||
|
p.ReadDataSourceResponse = &providers.ReadDataSourceResponse{
|
||||||
|
State: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"foo": cty.StringVal("boop"),
|
||||||
|
"results": cty.ListValEmpty(cty.String),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
_, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
|
||||||
|
Mode: plans.NormalMode,
|
||||||
|
SetVariables: InputValues{
|
||||||
|
"boop": &InputValue{
|
||||||
|
Value: cty.StringVal("boop"),
|
||||||
|
SourceType: ValueFromCLIArg,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if !diags.HasErrors() {
|
||||||
|
t.Fatal("succeeded; want errors")
|
||||||
|
}
|
||||||
|
if got, want := diags.Err().Error(), "Resource postcondition failed: Results cannot be empty."; got != want {
|
||||||
|
t.Fatalf("wrong error:\ngot: %s\nwant: %q", got, want)
|
||||||
|
}
|
||||||
|
if !p.ReadDataSourceCalled {
|
||||||
|
t.Errorf("Provider's ReadDataSource wasn't called; should've been")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContext2Plan_outputPrecondition(t *testing.T) {
|
||||||
|
m := testModuleInline(t, map[string]string{
|
||||||
|
"main.tf": `
|
||||||
|
terraform {
|
||||||
|
experiments = [preconditions_postconditions]
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "boop" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
output "a" {
|
||||||
|
value = var.boop
|
||||||
|
precondition {
|
||||||
|
condition = var.boop == "boop"
|
||||||
|
error_message = "Wrong boop."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
|
||||||
|
p := testProvider("test")
|
||||||
|
|
||||||
|
ctx := testContext2(t, &ContextOpts{
|
||||||
|
Providers: map[addrs.Provider]providers.Factory{
|
||||||
|
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("condition pass", func(t *testing.T) {
|
||||||
|
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
|
||||||
|
Mode: plans.NormalMode,
|
||||||
|
SetVariables: InputValues{
|
||||||
|
"boop": &InputValue{
|
||||||
|
Value: cty.StringVal("boop"),
|
||||||
|
SourceType: ValueFromCLIArg,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assertNoErrors(t, diags)
|
||||||
|
addr := addrs.RootModuleInstance.OutputValue("a")
|
||||||
|
outputPlan := plan.Changes.OutputValue(addr)
|
||||||
|
if outputPlan == nil {
|
||||||
|
t.Fatalf("no plan for %s at all", addr)
|
||||||
|
}
|
||||||
|
if got, want := outputPlan.Addr, addr; !got.Equal(want) {
|
||||||
|
t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want)
|
||||||
|
}
|
||||||
|
if got, want := outputPlan.Action, plans.Create; got != want {
|
||||||
|
t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("condition fail", func(t *testing.T) {
|
||||||
|
_, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
|
||||||
|
Mode: plans.NormalMode,
|
||||||
|
SetVariables: InputValues{
|
||||||
|
"boop": &InputValue{
|
||||||
|
Value: cty.StringVal("nope"),
|
||||||
|
SourceType: ValueFromCLIArg,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if !diags.HasErrors() {
|
||||||
|
t.Fatal("succeeded; want errors")
|
||||||
|
}
|
||||||
|
if got, want := diags.Err().Error(), "Module output value precondition failed: Wrong boop."; got != want {
|
||||||
|
t.Fatalf("wrong error:\ngot: %s\nwant: %q", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContext2Plan_preconditionErrors(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
condition string
|
||||||
|
wantSummary string
|
||||||
|
wantDetail string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"data.test_data_source",
|
||||||
|
"Invalid reference",
|
||||||
|
`The "data" object must be followed by two attribute names`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"self.value",
|
||||||
|
`Invalid "self" reference`,
|
||||||
|
"only in resource provisioner, connection, and postcondition blocks",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data.foo.bar",
|
||||||
|
"Reference to undeclared resource",
|
||||||
|
`A data resource "foo" "bar" has not been declared in the root module`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"test_resource.b.value",
|
||||||
|
"Invalid condition result",
|
||||||
|
"Condition expression must return either true or false",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"test_resource.c.value",
|
||||||
|
"Invalid condition result",
|
||||||
|
"Invalid validation condition result value: a bool is required",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
p := testProvider("test")
|
||||||
|
ctx := testContext2(t, &ContextOpts{
|
||||||
|
Providers: map[addrs.Provider]providers.Factory{
|
||||||
|
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.condition, func(t *testing.T) {
|
||||||
|
main := fmt.Sprintf(`
|
||||||
|
terraform {
|
||||||
|
experiments = [preconditions_postconditions]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "test_resource" "a" {
|
||||||
|
value = var.boop
|
||||||
|
lifecycle {
|
||||||
|
precondition {
|
||||||
|
condition = %s
|
||||||
|
error_message = "Not relevant."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "test_resource" "b" {
|
||||||
|
value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "test_resource" "c" {
|
||||||
|
value = "bar"
|
||||||
|
}
|
||||||
|
`, tc.condition)
|
||||||
|
m := testModuleInline(t, map[string]string{"main.tf": main})
|
||||||
|
|
||||||
|
_, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
|
||||||
|
if !diags.HasErrors() {
|
||||||
|
t.Fatal("succeeded; want errors")
|
||||||
|
}
|
||||||
|
diag := diags[0]
|
||||||
|
if got, want := diag.Description().Summary, tc.wantSummary; got != want {
|
||||||
|
t.Errorf("unexpected summary\n got: %s\nwant: %s", got, want)
|
||||||
|
}
|
||||||
|
if got, want := diag.Description().Detail, tc.wantDetail; !strings.Contains(got, want) {
|
||||||
|
t.Errorf("unexpected summary\ngot: %s\nwant to contain %q", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,115 @@
|
||||||
|
package terraform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/hashicorp/hcl/v2"
|
||||||
|
"github.com/zclconf/go-cty/cty"
|
||||||
|
"github.com/zclconf/go-cty/cty/convert"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/internal/addrs"
|
||||||
|
"github.com/hashicorp/terraform/internal/configs"
|
||||||
|
"github.com/hashicorp/terraform/internal/instances"
|
||||||
|
"github.com/hashicorp/terraform/internal/lang"
|
||||||
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||||
|
)
|
||||||
|
|
||||||
|
type checkType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
checkInvalid checkType = 0
|
||||||
|
checkResourcePrecondition checkType = 1
|
||||||
|
checkResourcePostcondition checkType = 2
|
||||||
|
checkOutputPrecondition checkType = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c checkType) FailureSummary() string {
|
||||||
|
switch c {
|
||||||
|
case checkResourcePrecondition:
|
||||||
|
return "Resource precondition failed"
|
||||||
|
case checkResourcePostcondition:
|
||||||
|
return "Resource postcondition failed"
|
||||||
|
case checkOutputPrecondition:
|
||||||
|
return "Module output value precondition failed"
|
||||||
|
default:
|
||||||
|
// This should not happen
|
||||||
|
return "Failed condition for invalid check type"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// evalCheckRules ensures that all of the given check rules pass against
|
||||||
|
// the given HCL evaluation context.
|
||||||
|
//
|
||||||
|
// If any check rules produce an unknown result then they will be silently
|
||||||
|
// ignored on the assumption that the same checks will be run again later
|
||||||
|
// with fewer unknown values in the EvalContext.
|
||||||
|
//
|
||||||
|
// If any of the rules do not pass, the returned diagnostics will contain
|
||||||
|
// errors. Otherwise, it will either be empty or contain only warnings.
|
||||||
|
func evalCheckRules(typ checkType, rules []*configs.CheckRule, ctx EvalContext, self addrs.Referenceable, keyData instances.RepetitionData) (diags tfdiags.Diagnostics) {
|
||||||
|
if len(rules) == 0 {
|
||||||
|
// Nothing to do
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rule := range rules {
|
||||||
|
const errInvalidCondition = "Invalid condition result"
|
||||||
|
var ruleDiags tfdiags.Diagnostics
|
||||||
|
|
||||||
|
refs, moreDiags := lang.ReferencesInExpr(rule.Condition)
|
||||||
|
ruleDiags = ruleDiags.Append(moreDiags)
|
||||||
|
scope := ctx.EvaluationScope(self, keyData)
|
||||||
|
hclCtx, moreDiags := scope.EvalContext(refs)
|
||||||
|
ruleDiags = ruleDiags.Append(moreDiags)
|
||||||
|
|
||||||
|
result, hclDiags := rule.Condition.Value(hclCtx)
|
||||||
|
ruleDiags = ruleDiags.Append(hclDiags)
|
||||||
|
diags = diags.Append(ruleDiags)
|
||||||
|
|
||||||
|
if ruleDiags.HasErrors() {
|
||||||
|
log.Printf("[TRACE] evalCheckRules: %s: %s", typ.FailureSummary(), ruleDiags.Err().Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if !result.IsKnown() {
|
||||||
|
continue // We'll wait until we've learned more, then.
|
||||||
|
}
|
||||||
|
if result.IsNull() {
|
||||||
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: errInvalidCondition,
|
||||||
|
Detail: "Condition expression must return either true or false, not null.",
|
||||||
|
Subject: rule.Condition.Range().Ptr(),
|
||||||
|
Expression: rule.Condition,
|
||||||
|
EvalContext: hclCtx,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
result, err = convert.Convert(result, cty.Bool)
|
||||||
|
if err != nil {
|
||||||
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: errInvalidCondition,
|
||||||
|
Detail: fmt.Sprintf("Invalid validation condition result value: %s.", tfdiags.FormatError(err)),
|
||||||
|
Subject: rule.Condition.Range().Ptr(),
|
||||||
|
Expression: rule.Condition,
|
||||||
|
EvalContext: hclCtx,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.False() {
|
||||||
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: typ.FailureSummary(),
|
||||||
|
Detail: rule.ErrorMessage,
|
||||||
|
Subject: rule.Condition.Range().Ptr(),
|
||||||
|
Expression: rule.Condition,
|
||||||
|
EvalContext: hclCtx,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return diags
|
||||||
|
}
|
|
@ -58,7 +58,7 @@ func (d *evaluationStateData) staticValidateReference(ref *addrs.Reference, self
|
||||||
// this codepath doesn't really "know about". If the "self"
|
// this codepath doesn't really "know about". If the "self"
|
||||||
// object starts being supported in more contexts later then
|
// object starts being supported in more contexts later then
|
||||||
// we'll need to adjust this message.
|
// we'll need to adjust this message.
|
||||||
Detail: `The "self" object is not available in this context. This object can be used only in resource provisioner and connection blocks.`,
|
Detail: `The "self" object is not available in this context. This object can be used only in resource provisioner, connection, and postcondition blocks.`,
|
||||||
Subject: ref.SourceRange.ToHCL().Ptr(),
|
Subject: ref.SourceRange.ToHCL().Ptr(),
|
||||||
})
|
})
|
||||||
return diags
|
return diags
|
||||||
|
|
|
@ -237,8 +237,11 @@ func referencesForOutput(c *configs.Output) []*addrs.Reference {
|
||||||
refs := make([]*addrs.Reference, 0, l)
|
refs := make([]*addrs.Reference, 0, l)
|
||||||
refs = append(refs, impRefs...)
|
refs = append(refs, impRefs...)
|
||||||
refs = append(refs, expRefs...)
|
refs = append(refs, expRefs...)
|
||||||
|
for _, check := range c.Preconditions {
|
||||||
|
checkRefs, _ := lang.ReferencesInExpr(check.Condition)
|
||||||
|
refs = append(refs, checkRefs...)
|
||||||
|
}
|
||||||
return refs
|
return refs
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GraphNodeReferencer
|
// GraphNodeReferencer
|
||||||
|
@ -267,6 +270,16 @@ func (n *NodeApplyableOutput) Execute(ctx EvalContext, op walkOperation) (diags
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkDiags := evalCheckRules(
|
||||||
|
checkOutputPrecondition,
|
||||||
|
n.Config.Preconditions,
|
||||||
|
ctx, nil, EvalDataForNoInstanceKey,
|
||||||
|
)
|
||||||
|
diags = diags.Append(checkDiags)
|
||||||
|
if diags.HasErrors() {
|
||||||
|
return diags // failed preconditions prevent further evaluation
|
||||||
|
}
|
||||||
|
|
||||||
// If there was no change recorded, or the recorded change was not wholly
|
// If there was no change recorded, or the recorded change was not wholly
|
||||||
// known, then we need to re-evaluate the output
|
// known, then we need to re-evaluate the output
|
||||||
if !changeRecorded || !val.IsWhollyKnown() {
|
if !changeRecorded || !val.IsWhollyKnown() {
|
||||||
|
|
|
@ -172,6 +172,16 @@ func (n *NodeAbstractResource) References() []*addrs.Reference {
|
||||||
result = append(result, refs...)
|
result = append(result, refs...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, check := range c.Preconditions {
|
||||||
|
refs, _ := lang.ReferencesInExpr(check.Condition)
|
||||||
|
result = append(result, refs...)
|
||||||
|
}
|
||||||
|
for _, check := range c.Postconditions {
|
||||||
|
refs, _ := lang.ReferencesInExpr(check.Condition)
|
||||||
|
result = append(result, refs...)
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/hashicorp/terraform/internal/addrs"
|
"github.com/hashicorp/terraform/internal/addrs"
|
||||||
"github.com/hashicorp/terraform/internal/configs"
|
"github.com/hashicorp/terraform/internal/configs"
|
||||||
"github.com/hashicorp/terraform/internal/configs/configschema"
|
"github.com/hashicorp/terraform/internal/configs/configschema"
|
||||||
|
"github.com/hashicorp/terraform/internal/instances"
|
||||||
"github.com/hashicorp/terraform/internal/plans"
|
"github.com/hashicorp/terraform/internal/plans"
|
||||||
"github.com/hashicorp/terraform/internal/plans/objchange"
|
"github.com/hashicorp/terraform/internal/plans/objchange"
|
||||||
"github.com/hashicorp/terraform/internal/providers"
|
"github.com/hashicorp/terraform/internal/providers"
|
||||||
|
@ -638,16 +639,17 @@ func (n *NodeAbstractResourceInstance) plan(
|
||||||
plannedChange *plans.ResourceInstanceChange,
|
plannedChange *plans.ResourceInstanceChange,
|
||||||
currentState *states.ResourceInstanceObject,
|
currentState *states.ResourceInstanceObject,
|
||||||
createBeforeDestroy bool,
|
createBeforeDestroy bool,
|
||||||
forceReplace []addrs.AbsResourceInstance) (*plans.ResourceInstanceChange, *states.ResourceInstanceObject, tfdiags.Diagnostics) {
|
forceReplace []addrs.AbsResourceInstance) (*plans.ResourceInstanceChange, *states.ResourceInstanceObject, instances.RepetitionData, tfdiags.Diagnostics) {
|
||||||
var diags tfdiags.Diagnostics
|
var diags tfdiags.Diagnostics
|
||||||
var state *states.ResourceInstanceObject
|
var state *states.ResourceInstanceObject
|
||||||
var plan *plans.ResourceInstanceChange
|
var plan *plans.ResourceInstanceChange
|
||||||
|
var keyData instances.RepetitionData
|
||||||
|
|
||||||
config := *n.Config
|
config := *n.Config
|
||||||
resource := n.Addr.Resource.Resource
|
resource := n.Addr.Resource.Resource
|
||||||
provider, providerSchema, err := getProvider(ctx, n.ResolvedProvider)
|
provider, providerSchema, err := getProvider(ctx, n.ResolvedProvider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return plan, state, diags.Append(err)
|
return plan, state, keyData, diags.Append(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if plannedChange != nil {
|
if plannedChange != nil {
|
||||||
|
@ -657,7 +659,7 @@ func (n *NodeAbstractResourceInstance) plan(
|
||||||
|
|
||||||
if providerSchema == nil {
|
if providerSchema == nil {
|
||||||
diags = diags.Append(fmt.Errorf("provider schema is unavailable for %s", n.Addr))
|
diags = diags.Append(fmt.Errorf("provider schema is unavailable for %s", n.Addr))
|
||||||
return plan, state, diags
|
return plan, state, keyData, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluate the configuration
|
// Evaluate the configuration
|
||||||
|
@ -665,22 +667,33 @@ func (n *NodeAbstractResourceInstance) plan(
|
||||||
if schema == nil {
|
if schema == nil {
|
||||||
// Should be caught during validation, so we don't bother with a pretty error here
|
// Should be caught during validation, so we don't bother with a pretty error here
|
||||||
diags = diags.Append(fmt.Errorf("provider does not support resource type %q", resource.Type))
|
diags = diags.Append(fmt.Errorf("provider does not support resource type %q", resource.Type))
|
||||||
return plan, state, diags
|
return plan, state, keyData, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
forEach, _ := evaluateForEachExpression(n.Config.ForEach, ctx)
|
forEach, _ := evaluateForEachExpression(n.Config.ForEach, ctx)
|
||||||
|
|
||||||
keyData := EvalDataForInstanceKey(n.ResourceInstanceAddr().Resource.Key, forEach)
|
keyData = EvalDataForInstanceKey(n.ResourceInstanceAddr().Resource.Key, forEach)
|
||||||
|
|
||||||
|
checkDiags := evalCheckRules(
|
||||||
|
checkResourcePrecondition,
|
||||||
|
n.Config.Preconditions,
|
||||||
|
ctx, nil, keyData,
|
||||||
|
)
|
||||||
|
diags = diags.Append(checkDiags)
|
||||||
|
if diags.HasErrors() {
|
||||||
|
return plan, state, keyData, diags // failed preconditions prevent further evaluation
|
||||||
|
}
|
||||||
|
|
||||||
origConfigVal, _, configDiags := ctx.EvaluateBlock(config.Config, schema, nil, keyData)
|
origConfigVal, _, configDiags := ctx.EvaluateBlock(config.Config, schema, nil, keyData)
|
||||||
diags = diags.Append(configDiags)
|
diags = diags.Append(configDiags)
|
||||||
if configDiags.HasErrors() {
|
if configDiags.HasErrors() {
|
||||||
return plan, state, diags
|
return plan, state, keyData, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
metaConfigVal, metaDiags := n.providerMetas(ctx)
|
metaConfigVal, metaDiags := n.providerMetas(ctx)
|
||||||
diags = diags.Append(metaDiags)
|
diags = diags.Append(metaDiags)
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
return plan, state, diags
|
return plan, state, keyData, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
var priorVal cty.Value
|
var priorVal cty.Value
|
||||||
|
@ -723,7 +736,7 @@ func (n *NodeAbstractResourceInstance) plan(
|
||||||
)
|
)
|
||||||
diags = diags.Append(validateResp.Diagnostics.InConfigBody(config.Config, n.Addr.String()))
|
diags = diags.Append(validateResp.Diagnostics.InConfigBody(config.Config, n.Addr.String()))
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
return plan, state, diags
|
return plan, state, keyData, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignore_changes is meant to only apply to the configuration, so it must
|
// ignore_changes is meant to only apply to the configuration, so it must
|
||||||
|
@ -736,7 +749,7 @@ func (n *NodeAbstractResourceInstance) plan(
|
||||||
configValIgnored, ignoreChangeDiags := n.processIgnoreChanges(priorVal, origConfigVal)
|
configValIgnored, ignoreChangeDiags := n.processIgnoreChanges(priorVal, origConfigVal)
|
||||||
diags = diags.Append(ignoreChangeDiags)
|
diags = diags.Append(ignoreChangeDiags)
|
||||||
if ignoreChangeDiags.HasErrors() {
|
if ignoreChangeDiags.HasErrors() {
|
||||||
return plan, state, diags
|
return plan, state, keyData, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create an unmarked version of our config val and our prior val.
|
// Create an unmarked version of our config val and our prior val.
|
||||||
|
@ -752,7 +765,7 @@ func (n *NodeAbstractResourceInstance) plan(
|
||||||
return h.PreDiff(n.Addr, states.CurrentGen, priorVal, proposedNewVal)
|
return h.PreDiff(n.Addr, states.CurrentGen, priorVal, proposedNewVal)
|
||||||
}))
|
}))
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
return plan, state, diags
|
return plan, state, keyData, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := provider.PlanResourceChange(providers.PlanResourceChangeRequest{
|
resp := provider.PlanResourceChange(providers.PlanResourceChangeRequest{
|
||||||
|
@ -765,7 +778,7 @@ func (n *NodeAbstractResourceInstance) plan(
|
||||||
})
|
})
|
||||||
diags = diags.Append(resp.Diagnostics.InConfigBody(config.Config, n.Addr.String()))
|
diags = diags.Append(resp.Diagnostics.InConfigBody(config.Config, n.Addr.String()))
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
return plan, state, diags
|
return plan, state, keyData, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
plannedNewVal := resp.PlannedState
|
plannedNewVal := resp.PlannedState
|
||||||
|
@ -793,7 +806,7 @@ func (n *NodeAbstractResourceInstance) plan(
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
return plan, state, diags
|
return plan, state, keyData, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
if errs := objchange.AssertPlanValid(schema, unmarkedPriorVal, unmarkedConfigVal, plannedNewVal); len(errs) > 0 {
|
if errs := objchange.AssertPlanValid(schema, unmarkedPriorVal, unmarkedConfigVal, plannedNewVal); len(errs) > 0 {
|
||||||
|
@ -823,7 +836,7 @@ func (n *NodeAbstractResourceInstance) plan(
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
return plan, state, diags
|
return plan, state, keyData, diags
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -840,7 +853,7 @@ func (n *NodeAbstractResourceInstance) plan(
|
||||||
plannedNewVal, ignoreChangeDiags = n.processIgnoreChanges(unmarkedPriorVal, plannedNewVal)
|
plannedNewVal, ignoreChangeDiags = n.processIgnoreChanges(unmarkedPriorVal, plannedNewVal)
|
||||||
diags = diags.Append(ignoreChangeDiags)
|
diags = diags.Append(ignoreChangeDiags)
|
||||||
if ignoreChangeDiags.HasErrors() {
|
if ignoreChangeDiags.HasErrors() {
|
||||||
return plan, state, diags
|
return plan, state, keyData, diags
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -907,7 +920,7 @@ func (n *NodeAbstractResourceInstance) plan(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
return plan, state, diags
|
return plan, state, keyData, diags
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1001,7 +1014,7 @@ func (n *NodeAbstractResourceInstance) plan(
|
||||||
// append these new diagnostics if there's at least one error inside.
|
// append these new diagnostics if there's at least one error inside.
|
||||||
if resp.Diagnostics.HasErrors() {
|
if resp.Diagnostics.HasErrors() {
|
||||||
diags = diags.Append(resp.Diagnostics.InConfigBody(config.Config, n.Addr.String()))
|
diags = diags.Append(resp.Diagnostics.InConfigBody(config.Config, n.Addr.String()))
|
||||||
return plan, state, diags
|
return plan, state, keyData, diags
|
||||||
}
|
}
|
||||||
plannedNewVal = resp.PlannedState
|
plannedNewVal = resp.PlannedState
|
||||||
plannedPrivate = resp.PlannedPrivate
|
plannedPrivate = resp.PlannedPrivate
|
||||||
|
@ -1021,7 +1034,7 @@ func (n *NodeAbstractResourceInstance) plan(
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
return plan, state, diags
|
return plan, state, keyData, diags
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1064,7 +1077,7 @@ func (n *NodeAbstractResourceInstance) plan(
|
||||||
return h.PostDiff(n.Addr, states.CurrentGen, action, priorVal, plannedNewVal)
|
return h.PostDiff(n.Addr, states.CurrentGen, action, priorVal, plannedNewVal)
|
||||||
}))
|
}))
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
return plan, state, diags
|
return plan, state, keyData, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update our return plan
|
// Update our return plan
|
||||||
|
@ -1098,7 +1111,7 @@ func (n *NodeAbstractResourceInstance) plan(
|
||||||
Private: plannedPrivate,
|
Private: plannedPrivate,
|
||||||
}
|
}
|
||||||
|
|
||||||
return plan, state, diags
|
return plan, state, keyData, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *NodeAbstractResource) processIgnoreChanges(prior, config cty.Value) (cty.Value, tfdiags.Diagnostics) {
|
func (n *NodeAbstractResource) processIgnoreChanges(prior, config cty.Value) (cty.Value, tfdiags.Diagnostics) {
|
||||||
|
@ -1470,16 +1483,17 @@ func (n *NodeAbstractResourceInstance) providerMetas(ctx EvalContext) (cty.Value
|
||||||
// value, but it still matches the previous state, then we can record a NoNop
|
// value, but it still matches the previous state, then we can record a NoNop
|
||||||
// change. If the states don't match then we record a Read change so that the
|
// change. If the states don't match then we record a Read change so that the
|
||||||
// new value is applied to the state.
|
// new value is applied to the state.
|
||||||
func (n *NodeAbstractResourceInstance) planDataSource(ctx EvalContext, currentState *states.ResourceInstanceObject) (*plans.ResourceInstanceChange, *states.ResourceInstanceObject, tfdiags.Diagnostics) {
|
func (n *NodeAbstractResourceInstance) planDataSource(ctx EvalContext, currentState *states.ResourceInstanceObject) (*plans.ResourceInstanceChange, *states.ResourceInstanceObject, instances.RepetitionData, tfdiags.Diagnostics) {
|
||||||
var diags tfdiags.Diagnostics
|
var diags tfdiags.Diagnostics
|
||||||
|
var keyData instances.RepetitionData
|
||||||
var configVal cty.Value
|
var configVal cty.Value
|
||||||
|
|
||||||
_, providerSchema, err := getProvider(ctx, n.ResolvedProvider)
|
_, providerSchema, err := getProvider(ctx, n.ResolvedProvider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, diags.Append(err)
|
return nil, nil, keyData, diags.Append(err)
|
||||||
}
|
}
|
||||||
if providerSchema == nil {
|
if providerSchema == nil {
|
||||||
return nil, nil, diags.Append(fmt.Errorf("provider schema not available for %s", n.Addr))
|
return nil, nil, keyData, diags.Append(fmt.Errorf("provider schema not available for %s", n.Addr))
|
||||||
}
|
}
|
||||||
|
|
||||||
config := *n.Config
|
config := *n.Config
|
||||||
|
@ -1487,7 +1501,7 @@ func (n *NodeAbstractResourceInstance) planDataSource(ctx EvalContext, currentSt
|
||||||
if schema == nil {
|
if schema == nil {
|
||||||
// Should be caught during validation, so we don't bother with a pretty error here
|
// Should be caught during validation, so we don't bother with a pretty error here
|
||||||
diags = diags.Append(fmt.Errorf("provider %q does not support data source %q", n.ResolvedProvider, n.Addr.ContainingResource().Resource.Type))
|
diags = diags.Append(fmt.Errorf("provider %q does not support data source %q", n.ResolvedProvider, n.Addr.ContainingResource().Resource.Type))
|
||||||
return nil, nil, diags
|
return nil, nil, keyData, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
objTy := schema.ImpliedType()
|
objTy := schema.ImpliedType()
|
||||||
|
@ -1497,13 +1511,26 @@ func (n *NodeAbstractResourceInstance) planDataSource(ctx EvalContext, currentSt
|
||||||
}
|
}
|
||||||
|
|
||||||
forEach, _ := evaluateForEachExpression(config.ForEach, ctx)
|
forEach, _ := evaluateForEachExpression(config.ForEach, ctx)
|
||||||
keyData := EvalDataForInstanceKey(n.ResourceInstanceAddr().Resource.Key, forEach)
|
keyData = EvalDataForInstanceKey(n.ResourceInstanceAddr().Resource.Key, forEach)
|
||||||
|
|
||||||
|
checkDiags := evalCheckRules(
|
||||||
|
checkResourcePrecondition,
|
||||||
|
n.Config.Preconditions,
|
||||||
|
ctx, nil, keyData,
|
||||||
|
)
|
||||||
|
diags = diags.Append(checkDiags)
|
||||||
|
if diags.HasErrors() {
|
||||||
|
diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) {
|
||||||
|
return h.PostApply(n.Addr, states.CurrentGen, priorVal, diags.Err())
|
||||||
|
}))
|
||||||
|
return nil, nil, keyData, diags // failed preconditions prevent further evaluation
|
||||||
|
}
|
||||||
|
|
||||||
var configDiags tfdiags.Diagnostics
|
var configDiags tfdiags.Diagnostics
|
||||||
configVal, _, configDiags = ctx.EvaluateBlock(config.Config, schema, nil, keyData)
|
configVal, _, configDiags = ctx.EvaluateBlock(config.Config, schema, nil, keyData)
|
||||||
diags = diags.Append(configDiags)
|
diags = diags.Append(configDiags)
|
||||||
if configDiags.HasErrors() {
|
if configDiags.HasErrors() {
|
||||||
return nil, nil, diags
|
return nil, nil, keyData, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
unmarkedConfigVal, configMarkPaths := configVal.UnmarkDeepWithPaths()
|
unmarkedConfigVal, configMarkPaths := configVal.UnmarkDeepWithPaths()
|
||||||
|
@ -1529,7 +1556,7 @@ func (n *NodeAbstractResourceInstance) planDataSource(ctx EvalContext, currentSt
|
||||||
return h.PreDiff(n.Addr, states.CurrentGen, priorVal, proposedNewVal)
|
return h.PreDiff(n.Addr, states.CurrentGen, priorVal, proposedNewVal)
|
||||||
}))
|
}))
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
return nil, nil, diags
|
return nil, nil, keyData, diags
|
||||||
}
|
}
|
||||||
proposedNewVal = proposedNewVal.MarkWithPaths(configMarkPaths)
|
proposedNewVal = proposedNewVal.MarkWithPaths(configMarkPaths)
|
||||||
|
|
||||||
|
@ -1555,7 +1582,7 @@ func (n *NodeAbstractResourceInstance) planDataSource(ctx EvalContext, currentSt
|
||||||
return h.PostDiff(n.Addr, states.CurrentGen, plans.Read, priorVal, proposedNewVal)
|
return h.PostDiff(n.Addr, states.CurrentGen, plans.Read, priorVal, proposedNewVal)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return plannedChange, plannedNewState, diags
|
return plannedChange, plannedNewState, keyData, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
// While this isn't a "diff", continue to call this for data sources.
|
// While this isn't a "diff", continue to call this for data sources.
|
||||||
|
@ -1563,14 +1590,14 @@ func (n *NodeAbstractResourceInstance) planDataSource(ctx EvalContext, currentSt
|
||||||
return h.PreDiff(n.Addr, states.CurrentGen, priorVal, configVal)
|
return h.PreDiff(n.Addr, states.CurrentGen, priorVal, configVal)
|
||||||
}))
|
}))
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
return nil, nil, diags
|
return nil, nil, keyData, diags
|
||||||
}
|
}
|
||||||
// We have a complete configuration with no dependencies to wait on, so we
|
// We have a complete configuration with no dependencies to wait on, so we
|
||||||
// can read the data source into the state.
|
// can read the data source into the state.
|
||||||
newVal, readDiags := n.readDataSource(ctx, configVal)
|
newVal, readDiags := n.readDataSource(ctx, configVal)
|
||||||
diags = diags.Append(readDiags)
|
diags = diags.Append(readDiags)
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
return nil, nil, diags
|
return nil, nil, keyData, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we have a prior value, we can check for any irregularities in the response
|
// if we have a prior value, we can check for any irregularities in the response
|
||||||
|
@ -1603,7 +1630,7 @@ func (n *NodeAbstractResourceInstance) planDataSource(ctx EvalContext, currentSt
|
||||||
diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) {
|
diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) {
|
||||||
return h.PostDiff(n.Addr, states.CurrentGen, plans.Update, priorVal, newVal)
|
return h.PostDiff(n.Addr, states.CurrentGen, plans.Update, priorVal, newVal)
|
||||||
}))
|
}))
|
||||||
return nil, plannedNewState, diags
|
return nil, plannedNewState, keyData, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
// forcePlanReadData determines if we need to override the usual behavior of
|
// forcePlanReadData determines if we need to override the usual behavior of
|
||||||
|
@ -1649,15 +1676,16 @@ func (n *NodeAbstractResourceInstance) forcePlanReadData(ctx EvalContext) bool {
|
||||||
|
|
||||||
// apply deals with the main part of the data resource lifecycle: either
|
// apply deals with the main part of the data resource lifecycle: either
|
||||||
// actually reading from the data source or generating a plan to do so.
|
// actually reading from the data source or generating a plan to do so.
|
||||||
func (n *NodeAbstractResourceInstance) applyDataSource(ctx EvalContext, planned *plans.ResourceInstanceChange) (*states.ResourceInstanceObject, tfdiags.Diagnostics) {
|
func (n *NodeAbstractResourceInstance) applyDataSource(ctx EvalContext, planned *plans.ResourceInstanceChange) (*states.ResourceInstanceObject, instances.RepetitionData, tfdiags.Diagnostics) {
|
||||||
var diags tfdiags.Diagnostics
|
var diags tfdiags.Diagnostics
|
||||||
|
var keyData instances.RepetitionData
|
||||||
|
|
||||||
_, providerSchema, err := getProvider(ctx, n.ResolvedProvider)
|
_, providerSchema, err := getProvider(ctx, n.ResolvedProvider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, diags.Append(err)
|
return nil, keyData, diags.Append(err)
|
||||||
}
|
}
|
||||||
if providerSchema == nil {
|
if providerSchema == nil {
|
||||||
return nil, diags.Append(fmt.Errorf("provider schema not available for %s", n.Addr))
|
return nil, keyData, diags.Append(fmt.Errorf("provider schema not available for %s", n.Addr))
|
||||||
}
|
}
|
||||||
|
|
||||||
if planned != nil && planned.Action != plans.Read {
|
if planned != nil && planned.Action != plans.Read {
|
||||||
|
@ -1667,14 +1695,14 @@ func (n *NodeAbstractResourceInstance) applyDataSource(ctx EvalContext, planned
|
||||||
"invalid action %s for %s: only Read is supported (this is a bug in Terraform; please report it!)",
|
"invalid action %s for %s: only Read is supported (this is a bug in Terraform; please report it!)",
|
||||||
planned.Action, n.Addr,
|
planned.Action, n.Addr,
|
||||||
))
|
))
|
||||||
return nil, diags
|
return nil, keyData, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) {
|
diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) {
|
||||||
return h.PreApply(n.Addr, states.CurrentGen, planned.Action, planned.Before, planned.After)
|
return h.PreApply(n.Addr, states.CurrentGen, planned.Action, planned.Before, planned.After)
|
||||||
}))
|
}))
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
return nil, diags
|
return nil, keyData, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
config := *n.Config
|
config := *n.Config
|
||||||
|
@ -1682,22 +1710,35 @@ func (n *NodeAbstractResourceInstance) applyDataSource(ctx EvalContext, planned
|
||||||
if schema == nil {
|
if schema == nil {
|
||||||
// Should be caught during validation, so we don't bother with a pretty error here
|
// Should be caught during validation, so we don't bother with a pretty error here
|
||||||
diags = diags.Append(fmt.Errorf("provider %q does not support data source %q", n.ResolvedProvider, n.Addr.ContainingResource().Resource.Type))
|
diags = diags.Append(fmt.Errorf("provider %q does not support data source %q", n.ResolvedProvider, n.Addr.ContainingResource().Resource.Type))
|
||||||
return nil, diags
|
return nil, keyData, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
forEach, _ := evaluateForEachExpression(config.ForEach, ctx)
|
forEach, _ := evaluateForEachExpression(config.ForEach, ctx)
|
||||||
keyData := EvalDataForInstanceKey(n.Addr.Resource.Key, forEach)
|
keyData = EvalDataForInstanceKey(n.Addr.Resource.Key, forEach)
|
||||||
|
|
||||||
|
checkDiags := evalCheckRules(
|
||||||
|
checkResourcePrecondition,
|
||||||
|
n.Config.Preconditions,
|
||||||
|
ctx, nil, keyData,
|
||||||
|
)
|
||||||
|
diags = diags.Append(checkDiags)
|
||||||
|
if diags.HasErrors() {
|
||||||
|
diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) {
|
||||||
|
return h.PostApply(n.Addr, states.CurrentGen, planned.Before, diags.Err())
|
||||||
|
}))
|
||||||
|
return nil, keyData, diags // failed preconditions prevent further evaluation
|
||||||
|
}
|
||||||
|
|
||||||
configVal, _, configDiags := ctx.EvaluateBlock(config.Config, schema, nil, keyData)
|
configVal, _, configDiags := ctx.EvaluateBlock(config.Config, schema, nil, keyData)
|
||||||
diags = diags.Append(configDiags)
|
diags = diags.Append(configDiags)
|
||||||
if configDiags.HasErrors() {
|
if configDiags.HasErrors() {
|
||||||
return nil, diags
|
return nil, keyData, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
newVal, readDiags := n.readDataSource(ctx, configVal)
|
newVal, readDiags := n.readDataSource(ctx, configVal)
|
||||||
diags = diags.Append(readDiags)
|
diags = diags.Append(readDiags)
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
return nil, diags
|
return nil, keyData, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
state := &states.ResourceInstanceObject{
|
state := &states.ResourceInstanceObject{
|
||||||
|
@ -1709,7 +1750,7 @@ func (n *NodeAbstractResourceInstance) applyDataSource(ctx EvalContext, planned
|
||||||
return h.PostApply(n.Addr, states.CurrentGen, newVal, diags.Err())
|
return h.PostApply(n.Addr, states.CurrentGen, newVal, diags.Err())
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return state, diags
|
return state, keyData, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
// evalApplyProvisioners determines if provisioners need to be run, and if so
|
// evalApplyProvisioners determines if provisioners need to be run, and if so
|
||||||
|
@ -1981,22 +2022,23 @@ func (n *NodeAbstractResourceInstance) apply(
|
||||||
state *states.ResourceInstanceObject,
|
state *states.ResourceInstanceObject,
|
||||||
change *plans.ResourceInstanceChange,
|
change *plans.ResourceInstanceChange,
|
||||||
applyConfig *configs.Resource,
|
applyConfig *configs.Resource,
|
||||||
createBeforeDestroy bool) (*states.ResourceInstanceObject, tfdiags.Diagnostics) {
|
createBeforeDestroy bool) (*states.ResourceInstanceObject, instances.RepetitionData, tfdiags.Diagnostics) {
|
||||||
|
|
||||||
var diags tfdiags.Diagnostics
|
var diags tfdiags.Diagnostics
|
||||||
|
var keyData instances.RepetitionData
|
||||||
if state == nil {
|
if state == nil {
|
||||||
state = &states.ResourceInstanceObject{}
|
state = &states.ResourceInstanceObject{}
|
||||||
}
|
}
|
||||||
|
|
||||||
provider, providerSchema, err := getProvider(ctx, n.ResolvedProvider)
|
provider, providerSchema, err := getProvider(ctx, n.ResolvedProvider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, diags.Append(err)
|
return nil, keyData, diags.Append(err)
|
||||||
}
|
}
|
||||||
schema, _ := providerSchema.SchemaForResourceType(n.Addr.Resource.Resource.Mode, n.Addr.Resource.Resource.Type)
|
schema, _ := providerSchema.SchemaForResourceType(n.Addr.Resource.Resource.Mode, n.Addr.Resource.Resource.Type)
|
||||||
if schema == nil {
|
if schema == nil {
|
||||||
// Should be caught during validation, so we don't bother with a pretty error here
|
// Should be caught during validation, so we don't bother with a pretty error here
|
||||||
diags = diags.Append(fmt.Errorf("provider does not support resource type %q", n.Addr.Resource.Resource.Type))
|
diags = diags.Append(fmt.Errorf("provider does not support resource type %q", n.Addr.Resource.Resource.Type))
|
||||||
return nil, diags
|
return nil, keyData, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[INFO] Starting apply for %s", n.Addr)
|
log.Printf("[INFO] Starting apply for %s", n.Addr)
|
||||||
|
@ -2005,11 +2047,11 @@ func (n *NodeAbstractResourceInstance) apply(
|
||||||
if applyConfig != nil {
|
if applyConfig != nil {
|
||||||
var configDiags tfdiags.Diagnostics
|
var configDiags tfdiags.Diagnostics
|
||||||
forEach, _ := evaluateForEachExpression(applyConfig.ForEach, ctx)
|
forEach, _ := evaluateForEachExpression(applyConfig.ForEach, ctx)
|
||||||
keyData := EvalDataForInstanceKey(n.ResourceInstanceAddr().Resource.Key, forEach)
|
keyData = EvalDataForInstanceKey(n.ResourceInstanceAddr().Resource.Key, forEach)
|
||||||
configVal, _, configDiags = ctx.EvaluateBlock(applyConfig.Config, schema, nil, keyData)
|
configVal, _, configDiags = ctx.EvaluateBlock(applyConfig.Config, schema, nil, keyData)
|
||||||
diags = diags.Append(configDiags)
|
diags = diags.Append(configDiags)
|
||||||
if configDiags.HasErrors() {
|
if configDiags.HasErrors() {
|
||||||
return nil, diags
|
return nil, keyData, diags
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2018,13 +2060,13 @@ func (n *NodeAbstractResourceInstance) apply(
|
||||||
"configuration for %s still contains unknown values during apply (this is a bug in Terraform; please report it!)",
|
"configuration for %s still contains unknown values during apply (this is a bug in Terraform; please report it!)",
|
||||||
n.Addr,
|
n.Addr,
|
||||||
))
|
))
|
||||||
return nil, diags
|
return nil, keyData, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
metaConfigVal, metaDiags := n.providerMetas(ctx)
|
metaConfigVal, metaDiags := n.providerMetas(ctx)
|
||||||
diags = diags.Append(metaDiags)
|
diags = diags.Append(metaDiags)
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
return nil, diags
|
return nil, keyData, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[DEBUG] %s: applying the planned %s change", n.Addr, change.Action)
|
log.Printf("[DEBUG] %s: applying the planned %s change", n.Addr, change.Action)
|
||||||
|
@ -2052,7 +2094,7 @@ func (n *NodeAbstractResourceInstance) apply(
|
||||||
Status: state.Status,
|
Status: state.Status,
|
||||||
Value: change.After,
|
Value: change.After,
|
||||||
}
|
}
|
||||||
return newState, diags
|
return newState, keyData, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := provider.ApplyResourceChange(providers.ApplyResourceChangeRequest{
|
resp := provider.ApplyResourceChange(providers.ApplyResourceChangeRequest{
|
||||||
|
@ -2123,7 +2165,7 @@ func (n *NodeAbstractResourceInstance) apply(
|
||||||
// Bail early in this particular case, because an object that doesn't
|
// Bail early in this particular case, because an object that doesn't
|
||||||
// conform to the schema can't be saved in the state anyway -- the
|
// conform to the schema can't be saved in the state anyway -- the
|
||||||
// serializer will reject it.
|
// serializer will reject it.
|
||||||
return nil, diags
|
return nil, keyData, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
// After this point we have a type-conforming result object and so we
|
// After this point we have a type-conforming result object and so we
|
||||||
|
@ -2249,7 +2291,7 @@ func (n *NodeAbstractResourceInstance) apply(
|
||||||
// prior state as the new value, making this effectively a no-op. If
|
// prior state as the new value, making this effectively a no-op. If
|
||||||
// the item really _has_ been deleted then our next refresh will detect
|
// the item really _has_ been deleted then our next refresh will detect
|
||||||
// that and fix it up.
|
// that and fix it up.
|
||||||
return state.DeepCopy(), diags
|
return state.DeepCopy(), keyData, diags
|
||||||
|
|
||||||
case diags.HasErrors() && !newVal.IsNull():
|
case diags.HasErrors() && !newVal.IsNull():
|
||||||
// if we have an error, make sure we restore the object status in the new state
|
// if we have an error, make sure we restore the object status in the new state
|
||||||
|
@ -2266,7 +2308,7 @@ func (n *NodeAbstractResourceInstance) apply(
|
||||||
newState.Dependencies = state.Dependencies
|
newState.Dependencies = state.Dependencies
|
||||||
}
|
}
|
||||||
|
|
||||||
return newState, diags
|
return newState, keyData, diags
|
||||||
|
|
||||||
case !newVal.IsNull():
|
case !newVal.IsNull():
|
||||||
// Non error case with a new state
|
// Non error case with a new state
|
||||||
|
@ -2276,11 +2318,11 @@ func (n *NodeAbstractResourceInstance) apply(
|
||||||
Private: resp.Private,
|
Private: resp.Private,
|
||||||
CreateBeforeDestroy: createBeforeDestroy,
|
CreateBeforeDestroy: createBeforeDestroy,
|
||||||
}
|
}
|
||||||
return newState, diags
|
return newState, keyData, diags
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Non error case, were the object was deleted
|
// Non error case, were the object was deleted
|
||||||
return nil, diags
|
return nil, keyData, diags
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -160,7 +160,7 @@ func (n *NodeApplyableResourceInstance) dataResourceExecute(ctx EvalContext) (di
|
||||||
// In this particular call to applyDataSource we include our planned
|
// In this particular call to applyDataSource we include our planned
|
||||||
// change, which signals that we expect this read to complete fully
|
// change, which signals that we expect this read to complete fully
|
||||||
// with no unknown values; it'll produce an error if not.
|
// with no unknown values; it'll produce an error if not.
|
||||||
state, applyDiags := n.applyDataSource(ctx, change)
|
state, repeatData, applyDiags := n.applyDataSource(ctx, change)
|
||||||
diags = diags.Append(applyDiags)
|
diags = diags.Append(applyDiags)
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
return diags
|
return diags
|
||||||
|
@ -174,6 +174,19 @@ func (n *NodeApplyableResourceInstance) dataResourceExecute(ctx EvalContext) (di
|
||||||
diags = diags.Append(n.writeChange(ctx, nil, ""))
|
diags = diags.Append(n.writeChange(ctx, nil, ""))
|
||||||
|
|
||||||
diags = diags.Append(updateStateHook(ctx))
|
diags = diags.Append(updateStateHook(ctx))
|
||||||
|
|
||||||
|
// Post-conditions might block further progress. We intentionally do this
|
||||||
|
// _after_ writing the state/diff because we want to check against
|
||||||
|
// the result of the operation, and to fail on future operations
|
||||||
|
// until the user makes the condition succeed.
|
||||||
|
checkDiags := evalCheckRules(
|
||||||
|
checkResourcePostcondition,
|
||||||
|
n.Config.Postconditions,
|
||||||
|
ctx, n.ResourceInstanceAddr().Resource,
|
||||||
|
repeatData,
|
||||||
|
)
|
||||||
|
diags = diags.Append(checkDiags)
|
||||||
|
|
||||||
return diags
|
return diags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,7 +251,7 @@ func (n *NodeApplyableResourceInstance) managedResourceExecute(ctx EvalContext)
|
||||||
|
|
||||||
// Make a new diff, in case we've learned new values in the state
|
// Make a new diff, in case we've learned new values in the state
|
||||||
// during apply which we can now incorporate.
|
// during apply which we can now incorporate.
|
||||||
diffApply, _, planDiags := n.plan(ctx, diff, state, false, n.forceReplace)
|
diffApply, _, _, planDiags := n.plan(ctx, diff, state, false, n.forceReplace)
|
||||||
diags = diags.Append(planDiags)
|
diags = diags.Append(planDiags)
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
return diags
|
return diags
|
||||||
|
@ -269,7 +282,7 @@ func (n *NodeApplyableResourceInstance) managedResourceExecute(ctx EvalContext)
|
||||||
return diags
|
return diags
|
||||||
}
|
}
|
||||||
|
|
||||||
state, applyDiags := n.apply(ctx, state, diffApply, n.Config, n.CreateBeforeDestroy())
|
state, repeatData, applyDiags := n.apply(ctx, state, diffApply, n.Config, n.CreateBeforeDestroy())
|
||||||
diags = diags.Append(applyDiags)
|
diags = diags.Append(applyDiags)
|
||||||
|
|
||||||
// We clear the change out here so that future nodes don't see a change
|
// We clear the change out here so that future nodes don't see a change
|
||||||
|
@ -339,6 +352,18 @@ func (n *NodeApplyableResourceInstance) managedResourceExecute(ctx EvalContext)
|
||||||
|
|
||||||
diags = diags.Append(n.postApplyHook(ctx, state, diags.Err()))
|
diags = diags.Append(n.postApplyHook(ctx, state, diags.Err()))
|
||||||
diags = diags.Append(updateStateHook(ctx))
|
diags = diags.Append(updateStateHook(ctx))
|
||||||
|
|
||||||
|
// Post-conditions might block further progress. We intentionally do this
|
||||||
|
// _after_ writing the state because we want to check against
|
||||||
|
// the result of the operation, and to fail on future operations
|
||||||
|
// until the user makes the condition succeed.
|
||||||
|
checkDiags := evalCheckRules(
|
||||||
|
checkResourcePostcondition,
|
||||||
|
n.Config.Postconditions,
|
||||||
|
ctx, addr, repeatData,
|
||||||
|
)
|
||||||
|
diags = diags.Append(checkDiags)
|
||||||
|
|
||||||
return diags
|
return diags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -210,7 +210,7 @@ func (n *NodeDestroyResourceInstance) managedResourceExecute(ctx EvalContext) (d
|
||||||
// Managed resources need to be destroyed, while data sources
|
// Managed resources need to be destroyed, while data sources
|
||||||
// are only removed from state.
|
// are only removed from state.
|
||||||
// we pass a nil configuration to apply because we are destroying
|
// we pass a nil configuration to apply because we are destroying
|
||||||
s, d := n.apply(ctx, state, changeApply, nil, false)
|
s, _, d := n.apply(ctx, state, changeApply, nil, false)
|
||||||
state, diags = s, diags.Append(d)
|
state, diags = s, diags.Append(d)
|
||||||
// we don't return immediately here on error, so that the state can be
|
// we don't return immediately here on error, so that the state can be
|
||||||
// finalized
|
// finalized
|
||||||
|
|
|
@ -249,7 +249,7 @@ func (n *NodeDestroyDeposedResourceInstanceObject) Execute(ctx EvalContext, op w
|
||||||
}
|
}
|
||||||
|
|
||||||
// we pass a nil configuration to apply because we are destroying
|
// we pass a nil configuration to apply because we are destroying
|
||||||
state, applyDiags := n.apply(ctx, state, change, nil, false)
|
state, _, applyDiags := n.apply(ctx, state, change, nil, false)
|
||||||
diags = diags.Append(applyDiags)
|
diags = diags.Append(applyDiags)
|
||||||
// don't return immediately on errors, we need to handle the state
|
// don't return immediately on errors, we need to handle the state
|
||||||
|
|
||||||
|
|
|
@ -95,7 +95,7 @@ func (n *NodePlannableResourceInstance) dataResourceExecute(ctx EvalContext) (di
|
||||||
return diags
|
return diags
|
||||||
}
|
}
|
||||||
|
|
||||||
change, state, planDiags := n.planDataSource(ctx, state)
|
change, state, repeatData, planDiags := n.planDataSource(ctx, state)
|
||||||
diags = diags.Append(planDiags)
|
diags = diags.Append(planDiags)
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
return diags
|
return diags
|
||||||
|
@ -113,6 +113,18 @@ func (n *NodePlannableResourceInstance) dataResourceExecute(ctx EvalContext) (di
|
||||||
}
|
}
|
||||||
|
|
||||||
diags = diags.Append(n.writeChange(ctx, change, ""))
|
diags = diags.Append(n.writeChange(ctx, change, ""))
|
||||||
|
|
||||||
|
// Post-conditions might block further progress. We intentionally do this
|
||||||
|
// _after_ writing the state/diff because we want to check against
|
||||||
|
// the result of the operation, and to fail on future operations
|
||||||
|
// until the user makes the condition succeed.
|
||||||
|
checkDiags := evalCheckRules(
|
||||||
|
checkResourcePostcondition,
|
||||||
|
n.Config.Postconditions,
|
||||||
|
ctx, addr.Resource, repeatData,
|
||||||
|
)
|
||||||
|
diags = diags.Append(checkDiags)
|
||||||
|
|
||||||
return diags
|
return diags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,7 +205,7 @@ func (n *NodePlannableResourceInstance) managedResourceExecute(ctx EvalContext)
|
||||||
|
|
||||||
// Plan the instance, unless we're in the refresh-only mode
|
// Plan the instance, unless we're in the refresh-only mode
|
||||||
if !n.skipPlanChanges {
|
if !n.skipPlanChanges {
|
||||||
change, instancePlanState, planDiags := n.plan(
|
change, instancePlanState, repeatData, planDiags := n.plan(
|
||||||
ctx, change, instanceRefreshState, n.ForceCreateBeforeDestroy, n.forceReplace,
|
ctx, change, instanceRefreshState, n.ForceCreateBeforeDestroy, n.forceReplace,
|
||||||
)
|
)
|
||||||
diags = diags.Append(planDiags)
|
diags = diags.Append(planDiags)
|
||||||
|
@ -240,6 +252,19 @@ func (n *NodePlannableResourceInstance) managedResourceExecute(ctx EvalContext)
|
||||||
return diags
|
return diags
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Post-conditions might block completion. We intentionally do this
|
||||||
|
// _after_ writing the state/diff because we want to check against
|
||||||
|
// the result of the operation, and to fail on future operations
|
||||||
|
// until the user makes the condition succeed.
|
||||||
|
// (Note that some preconditions will end up being skipped during
|
||||||
|
// planning, because their conditions depend on values not yet known.)
|
||||||
|
checkDiags := evalCheckRules(
|
||||||
|
checkResourcePostcondition,
|
||||||
|
n.Config.Postconditions,
|
||||||
|
ctx, addr.Resource, repeatData,
|
||||||
|
)
|
||||||
|
diags = diags.Append(checkDiags)
|
||||||
} else {
|
} else {
|
||||||
// Even if we don't plan changes, we do still need to at least update
|
// Even if we don't plan changes, we do still need to at least update
|
||||||
// the working state to reflect the refresh result. If not, then e.g.
|
// the working state to reflect the refresh result. If not, then e.g.
|
||||||
|
|
|
@ -79,7 +79,7 @@ func TestNodeRootVariableExecute(t *testing.T) {
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
Type: cty.Number,
|
Type: cty.Number,
|
||||||
ConstraintType: cty.Number,
|
ConstraintType: cty.Number,
|
||||||
Validations: []*configs.VariableValidation{
|
Validations: []*configs.CheckRule{
|
||||||
{
|
{
|
||||||
Condition: fakeHCLExpressionFunc(func(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
Condition: fakeHCLExpressionFunc(func(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
||||||
// This returns true only if the given variable value
|
// This returns true only if the given variable value
|
||||||
|
|
|
@ -0,0 +1,391 @@
|
||||||
|
---
|
||||||
|
page_title: Preconditions and Postconditions - Configuration Language
|
||||||
|
---
|
||||||
|
|
||||||
|
# Preconditions and Postconditions
|
||||||
|
|
||||||
|
Terraform providers can automatically detect and report problems related to
|
||||||
|
the remote system they are interacting with, but they typically do so using
|
||||||
|
language that describes implementation details of the target system, which
|
||||||
|
can sometimes make it hard to find the root cause of the problem in your
|
||||||
|
Terraform configuration.
|
||||||
|
|
||||||
|
Preconditions and postconditions allow you to optionally describe the
|
||||||
|
assumptions you are making as a module author, so that Terraform can detect
|
||||||
|
situations where those assumptions don't hold and potentially return an
|
||||||
|
error earlier or an error with better context about where the problem
|
||||||
|
originated.
|
||||||
|
|
||||||
|
Preconditions and postconditions both follow a similar structure, and differ
|
||||||
|
only in when Terraform evaluates them: Terraform checks a precondition prior
|
||||||
|
to evaluating the object it is associated with, and a postcondition _after_
|
||||||
|
evaluating the object. That means that preconditions are useful for stating
|
||||||
|
assumptions about data from elsewhere that the resource configuration relies
|
||||||
|
on, while postconditions are more useful for stating assumptions about the
|
||||||
|
result of the resource itself.
|
||||||
|
|
||||||
|
The following example shows some different possible uses of preconditions and
|
||||||
|
postconditions.
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
variable "aws_ami_id" {
|
||||||
|
type = string
|
||||||
|
|
||||||
|
# Input variable validation can check that the AMI ID is syntactically valid.
|
||||||
|
validation {
|
||||||
|
condition = can(regex("^ami-", var.aws_ami_id))
|
||||||
|
error_message = "The AMI ID must have the prefix \"ami-\"."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data "aws_ami" "example" {
|
||||||
|
id = var.aws_ami_id
|
||||||
|
|
||||||
|
lifecycle {
|
||||||
|
# A data resource with a postcondition can ensure that the selected AMI
|
||||||
|
# meets this module's expectations, by reacting to the dynamically-loaded
|
||||||
|
# AMI attributes.
|
||||||
|
postcondition {
|
||||||
|
condition = self.tags["Component"] == "nomad-server"
|
||||||
|
error_message = "The selected AMI must be tagged with the Component value \"nomad-server\"."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_instance" "example" {
|
||||||
|
instance_type = "t2.micro"
|
||||||
|
ami = "ami-abc123"
|
||||||
|
|
||||||
|
lifecycle {
|
||||||
|
# A resource with a precondition can ensure that the selected AMI
|
||||||
|
# is set up correctly to work with the instance configuration.
|
||||||
|
precondition {
|
||||||
|
condition = data.aws_ami.example.architecture == "x86_64"
|
||||||
|
error_message = "The selected AMI must be for the x86_64 architecture."
|
||||||
|
}
|
||||||
|
|
||||||
|
# A resource with a postcondition can react to server-decided values
|
||||||
|
# during the apply step and halt work immediately if the result doesn't
|
||||||
|
# meet expectations.
|
||||||
|
postcondition {
|
||||||
|
condition = self.private_dns != ""
|
||||||
|
error_message = "EC2 instance must be in a VPC that has private DNS hostnames enabled."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data "aws_ebs_volume" "example" {
|
||||||
|
# We can use data resources that refer to other resources in order to
|
||||||
|
# load extra data that isn't directly exported by a resource.
|
||||||
|
#
|
||||||
|
# This example reads the details about the root storage volume for
|
||||||
|
# the EC2 instance declared by aws_instance.example, using the exported ID.
|
||||||
|
|
||||||
|
filter {
|
||||||
|
name = "volume-id"
|
||||||
|
values = [aws_instance.example.root_block_device.volume_id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "api_base_url" {
|
||||||
|
value = "https://${aws_instance.example.private_dns}:8433/"
|
||||||
|
|
||||||
|
# An output value with a precondition can check the object that the
|
||||||
|
# output value is describing to make sure it meets expectations before
|
||||||
|
# any caller of this module can use it.
|
||||||
|
precondition {
|
||||||
|
condition = data.aws_ebs_volume.example.encrypted
|
||||||
|
error_message = "The server's root volume is not encrypted."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The input variable validation rule, preconditions, and postconditions in the
|
||||||
|
above example declare explicitly some assumptions and guarantees that the
|
||||||
|
module developer is making in the design of this module:
|
||||||
|
|
||||||
|
* The caller of the module must provide a syntactically-valid AMI ID in the
|
||||||
|
`aws_ami_id` input variable.
|
||||||
|
|
||||||
|
This would detect if the caller accidentally assigned an AMI name to the
|
||||||
|
argument, instead of an AMI ID.
|
||||||
|
|
||||||
|
* The AMI ID must refer to an AMI that exists and that has been tagged as
|
||||||
|
being intended for the component "nomad-server".
|
||||||
|
|
||||||
|
This would detect if the caller accidentally provided an AMI intended for
|
||||||
|
some other system component, which might otherwise be detected only after
|
||||||
|
booting the EC2 instance and noticing that the expected network service
|
||||||
|
isn't running. Terraform can therefore detect that problem earlier and
|
||||||
|
return a more actionable error message for it.
|
||||||
|
|
||||||
|
* The AMI ID must refer to an AMI which contains an operating system for the
|
||||||
|
`x86_64` architecture.
|
||||||
|
|
||||||
|
This would detect if the caller accidentally built an AMI for a different
|
||||||
|
architecture, which might therefore not be able to run the software this
|
||||||
|
virtual machine is intended to host.
|
||||||
|
|
||||||
|
* The EC2 instance must be allocated a private DNS hostname.
|
||||||
|
|
||||||
|
In AWS, EC2 instances are assigned private DNS hostnames only if they
|
||||||
|
belong to a virtual network configured in a certain way. This would
|
||||||
|
detect if the selected virtual network is not configured correctly,
|
||||||
|
giving explicit feedback to prompt the user to debug the network settings.
|
||||||
|
|
||||||
|
* The EC2 instance will have an encrypted root volume.
|
||||||
|
|
||||||
|
This ensures that the root volume is encrypted even though the software
|
||||||
|
running in this EC2 instance would probably still operate as expected
|
||||||
|
on an unencrypted volume. Therefore Terraform can draw attention to the
|
||||||
|
problem immediately, before any other components rely on the
|
||||||
|
insecurely-configured component.
|
||||||
|
|
||||||
|
Writing explicit preconditions and postconditions is always optional, but it
|
||||||
|
can be helpful to users and future maintainers of a Terraform module by
|
||||||
|
capturing assumptions that might otherwise be only implied, and by allowing
|
||||||
|
Terraform to check those assumptions and halt more quickly if they don't
|
||||||
|
hold in practice for a particular set of input variables.
|
||||||
|
|
||||||
|
## Precondition and Postcondition Locations
|
||||||
|
|
||||||
|
Terraform supports preconditions and postconditions in a number of different
|
||||||
|
locations in a module:
|
||||||
|
|
||||||
|
* The `lifecycle` block inside a `resource` or `data` block can include both
|
||||||
|
`precondition` and `postcondition` blocks associated with the containing
|
||||||
|
resource.
|
||||||
|
|
||||||
|
Terraform evaluates resource preconditions before evaluating the resource's
|
||||||
|
configuration arguments. Resource preconditions can take precedence over
|
||||||
|
argument evaluation errors.
|
||||||
|
|
||||||
|
Terraform evaluates resource postconditions after planning and after
|
||||||
|
applying changes to a managed resource, or after reading from a data
|
||||||
|
resource. Resource postcondition failures will therefore prevent applying
|
||||||
|
changes to other resources that depend on the failing resource.
|
||||||
|
|
||||||
|
* An `output` block declaring an output value can include a `precondition`
|
||||||
|
block.
|
||||||
|
|
||||||
|
Terraform evaluates output value preconditions before evaluating the
|
||||||
|
`value` expression to finalize the result. Output value preconditions
|
||||||
|
can take precedence over potential errors in the `value` expression.
|
||||||
|
|
||||||
|
Output value preconditions can be particularly useful in a root module,
|
||||||
|
to prevent saving an invalid new output value in the state and to preserve
|
||||||
|
the value from the previous apply, if any.
|
||||||
|
|
||||||
|
Output value preconditions can serve a symmetrical purpose to input
|
||||||
|
variable `validation` blocks: whereas input variable validation checks
|
||||||
|
assumptions the module makes about its inputs, output value preconditions
|
||||||
|
check guarantees that the module makes about its outputs.
|
||||||
|
|
||||||
|
## Condition Expressions
|
||||||
|
|
||||||
|
`precondition` and `postcondition` blocks both require an argument named
|
||||||
|
`condition`, whose value is a boolean expression which should return `true`
|
||||||
|
if the intended assumption holds or `false` if it does not.
|
||||||
|
|
||||||
|
Preconditions and postconditions can both refer to any other objects in the
|
||||||
|
same module, as long as the references don't create any cyclic dependencies.
|
||||||
|
|
||||||
|
Resource postconditions can additionally refer to attributes of each instance
|
||||||
|
of the resource where they are configured, using the special symbol `self`.
|
||||||
|
For example, `self.private_dns` refers to the `private_dns` attribute of
|
||||||
|
each instance of the containing resource.
|
||||||
|
|
||||||
|
Condition expressions are otherwise just normal Terraform expressions, and
|
||||||
|
so you can use any of Terraform's built-in functions or language operators
|
||||||
|
as long as the expression is valid and returns a boolean result.
|
||||||
|
|
||||||
|
### Common Condition Expression Features
|
||||||
|
|
||||||
|
Because condition expressions must produce boolean results, they can often
|
||||||
|
use built-in functions and language features that are less common elsewhere
|
||||||
|
in the Terraform language. The following language features are particularly
|
||||||
|
useful when writing condition expressions:
|
||||||
|
|
||||||
|
* You can use the built-in function `contains` to test whether a given
|
||||||
|
value is one of a set of predefined valid values:
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
condition = contains(["STAGE", "PROD"], var.environment)
|
||||||
|
```
|
||||||
|
|
||||||
|
* You can use the boolean operators `&&` (AND), `||` (OR), and `!` (NOT) to
|
||||||
|
combine multiple simpler conditions together:
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
condition = var.name != "" && lower(var.name) == var.name
|
||||||
|
```
|
||||||
|
|
||||||
|
* You can require a non-empty list or map by testing the collection's length:
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
condition = length(var.items) != 0
|
||||||
|
```
|
||||||
|
|
||||||
|
This is a better approach than directly comparing with another collection
|
||||||
|
using `==` or `!=`, because the comparison operators can only return `true`
|
||||||
|
if both operands have exactly the same type, which is often ambiguous
|
||||||
|
for empty collections.
|
||||||
|
|
||||||
|
* You can use `for` expressions which produce lists of boolean results
|
||||||
|
themselves in conjunction with the functions `alltrue` and `anytrue` to
|
||||||
|
test whether a condition holds for all or for any elements of a collection:
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
condition = alltrue([
|
||||||
|
for v in var.instances : contains(["t2.micro", "m3.medium"], v.type)
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|
||||||
|
* You can use the `can` function to concisely use the validity of an expression
|
||||||
|
as a condition. It returns `true` if its given expression evaluates
|
||||||
|
successfully and `false` if it returns any error, so you can use various
|
||||||
|
other functions that typically return errors as a part of your condition
|
||||||
|
expressions.
|
||||||
|
|
||||||
|
For example, you can use `can` with `regex` to test if a string matches
|
||||||
|
a particular pattern, because `regex` returns an error when given a
|
||||||
|
non-matching string:
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
condition = can(regex("^[a-z]+$", var.name)
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also use `can` with the type conversion functions to test whether
|
||||||
|
a value is convertible to a type or type constraint:
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
# This remote output value must have a value that can
|
||||||
|
# be used as a string, which includes strings themselves
|
||||||
|
# but also allows numbers and boolean values.
|
||||||
|
condition = can(tostring(data.terraform_remote_state.example.outputs["name"]))
|
||||||
|
```
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
# This remote output value must be convertible to a list
|
||||||
|
# type of with element type.
|
||||||
|
condition = can(tolist(data.terraform_remote_state.example.outputs["items"]))
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also use `can` with attribute access or index operators to
|
||||||
|
concisely test whether a collection or structural value has a particular
|
||||||
|
element or index:
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
# var.example must have an attribute named "foo"
|
||||||
|
condition = can(var.example.foo)
|
||||||
|
```
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
# var.example must be a sequence with at least one element
|
||||||
|
condition = can(var.example[0])
|
||||||
|
# (although it would typically be clearer to write this as a
|
||||||
|
# test like length(var.example) > 0 to better represent the
|
||||||
|
# intent of the condition.)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Early Evaluation
|
||||||
|
|
||||||
|
Terraform will evaluate conditions as early as possible.
|
||||||
|
|
||||||
|
If the condition expression depends on a resource attribute that won't be known
|
||||||
|
until the apply phase then Terraform will delay checking the condition until
|
||||||
|
the apply phase, but Terraform can check all other expressions during the
|
||||||
|
planning phase, and therefore block applying a plan that would violate the
|
||||||
|
conditions.
|
||||||
|
|
||||||
|
In the earlier example on this page, Terraform would typically be able to
|
||||||
|
detect invalid AMI tags during the planning phase, as long as `var.aws_ami_id`
|
||||||
|
is not itself derived from another resource. However, Terraform will not
|
||||||
|
detect a non-encrypted root volume until the EC2 instance was already created
|
||||||
|
during the apply step, because that condition depends on the root volume's
|
||||||
|
assigned ID, which AWS decides only when the EC2 instance is actually started.
|
||||||
|
|
||||||
|
For conditions which Terraform must defer to the apply phase, a _precondition_
|
||||||
|
will prevent taking whatever action was planned for a related resource, whereas
|
||||||
|
a _postcondition_ will merely halt processing after that action was already
|
||||||
|
taken, preventing any downstream actions that rely on it but not undoing the
|
||||||
|
action.
|
||||||
|
|
||||||
|
Terraform typically has less information during the initial creation of a
|
||||||
|
full configuration than when applying subsequent changes to that configuration.
|
||||||
|
Conditions checked only during apply during initial creation may therefore
|
||||||
|
be checked during planning on subsequent updates, detecting problems sooner
|
||||||
|
in that case.
|
||||||
|
|
||||||
|
## Error Messages
|
||||||
|
|
||||||
|
Each `precondition` or `postcondition` block must include an argument
|
||||||
|
`error_message`, which provides some custom error sentences that Terraform
|
||||||
|
will include as part of error messages when it detects an unmet condition.
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: Resource postcondition failed
|
||||||
|
|
||||||
|
with data.aws_ami.example,
|
||||||
|
on ec2.tf line 19, in data "aws_ami" "example":
|
||||||
|
72: condition = self.tags["Component"] == "nomad-server"
|
||||||
|
|----------------
|
||||||
|
| self.tags["Component"] is "consul-server"
|
||||||
|
|
||||||
|
The selected AMI must be tagged with the Component value "nomad-server".
|
||||||
|
```
|
||||||
|
|
||||||
|
The `error_message` argument must always be a literal string, and should
|
||||||
|
typically be written as a full sentence in a style similar to Terraform's own
|
||||||
|
error messages. Terraform will show the given message alongside the name
|
||||||
|
of the resource that detected the problem and any outside values used as part
|
||||||
|
of the condition expression.
|
||||||
|
|
||||||
|
## Preconditions or Postconditions?
|
||||||
|
|
||||||
|
Because preconditions can refer to the result attributes of other resources
|
||||||
|
in the same module, it's typically true that a particular check could be
|
||||||
|
implemented either as a postcondition of the resource producing the data
|
||||||
|
or as a precondition of a resource or output value using the data.
|
||||||
|
|
||||||
|
To decide which is most appropriate for a particular situation, consider
|
||||||
|
whether the check is representing either an assumption or a guarantee:
|
||||||
|
|
||||||
|
* An _assumption_ is a condition that must be true in order for the
|
||||||
|
configuration of a particular resource to be usable. In the earlier
|
||||||
|
example on this page, the `aws_instance` configuration had the _assumption_
|
||||||
|
that the given AMI will always be for the `x86_64` CPU architecture.
|
||||||
|
|
||||||
|
Assumptions should typically be written as preconditions, so that future
|
||||||
|
maintainers can find them close to the other expressions that rely on
|
||||||
|
that condition, and thus know more about what different variations that
|
||||||
|
resource is intended to allow.
|
||||||
|
|
||||||
|
* A _guarantee_ is a characteristic or behavior of an object that the rest of
|
||||||
|
the configuration ought to be able to rely on. In the earlier example on
|
||||||
|
this page, the `aws_instance` configuration had the _guarantee_ that the
|
||||||
|
EC2 instance will be running in a network that assigns it a private DNS
|
||||||
|
record.
|
||||||
|
|
||||||
|
Guarantees should typically be written as postconditions, so that
|
||||||
|
future maintainers can find them close to the resource configuration that
|
||||||
|
is responsible for implementing those guarantees and more easily see
|
||||||
|
which behaviors are important to preserve when changing the configuration.
|
||||||
|
|
||||||
|
In practice though, the distinction between these two is subjective: is the
|
||||||
|
AMI being tagged as Component `"nomad-server"` a guarantee about the AMI or
|
||||||
|
an assumption made by the EC2 instance? To decide, it might help to consider
|
||||||
|
which resource or output value would be most helpful to report in a resulting
|
||||||
|
error message, because Terraform will always report errors in the location
|
||||||
|
where the condition was declared.
|
||||||
|
|
||||||
|
The decision between the two may also be a matter of convenience. If a
|
||||||
|
particular resource has many dependencies that _all_ make an assumption about
|
||||||
|
that resource then it can be pragmatic to declare that just once as a
|
||||||
|
post-condition of the resource, rather than many times as preconditions on
|
||||||
|
each of the dependencies.
|
||||||
|
|
||||||
|
It may sometimes be helpful to declare the same or similar conditions as both
|
||||||
|
preconditions _and_ postconditions, particularly if the postcondition is
|
||||||
|
in a different module than the precondition, so that they can verify one
|
||||||
|
another as the two modules evolve independently.
|
Loading…
Reference in New Issue