Merge pull request #30658 from hashicorp/alisdair/preconditions-postconditions-refresh-only
core: Eval pre/postconditions in refresh-only mode
This commit is contained in:
commit
6cd0876596
|
@ -2283,6 +2283,34 @@ resource "test_resource" "a" {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("precondition fail refresh-only", func(t *testing.T) {
|
||||||
|
state := states.BuildState(func(s *states.SyncState) {
|
||||||
|
s.SetResourceInstanceCurrent(mustResourceInstanceAddr("test_resource.a"), &states.ResourceInstanceObjectSrc{
|
||||||
|
AttrsJSON: []byte(`{"value":"boop","output":"blorp"}`),
|
||||||
|
Status: states.ObjectReady,
|
||||||
|
}, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`))
|
||||||
|
})
|
||||||
|
_, diags := ctx.Plan(m, state, &PlanOpts{
|
||||||
|
Mode: plans.RefreshOnlyMode,
|
||||||
|
SetVariables: InputValues{
|
||||||
|
"boop": &InputValue{
|
||||||
|
Value: cty.StringVal("nope"),
|
||||||
|
SourceType: ValueFromCLIArg,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assertNoErrors(t, diags)
|
||||||
|
if len(diags) == 0 {
|
||||||
|
t.Fatalf("no diags, but should have warnings")
|
||||||
|
}
|
||||||
|
if got, want := diags.ErrWithWarnings().Error(), "Resource precondition failed: Wrong boop."; got != want {
|
||||||
|
t.Fatalf("wrong warning:\ngot: %s\nwant: %q", got, want)
|
||||||
|
}
|
||||||
|
if !p.ReadResourceCalled {
|
||||||
|
t.Errorf("Provider's ReadResource wasn't called; should've been")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("postcondition fail", func(t *testing.T) {
|
t.Run("postcondition fail", func(t *testing.T) {
|
||||||
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
|
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
|
||||||
m := req.ProposedNewState.AsValueMap()
|
m := req.ProposedNewState.AsValueMap()
|
||||||
|
@ -2308,7 +2336,108 @@ resource "test_resource" "a" {
|
||||||
t.Fatalf("wrong error:\ngot: %s\nwant: %q", got, want)
|
t.Fatalf("wrong error:\ngot: %s\nwant: %q", got, want)
|
||||||
}
|
}
|
||||||
if !p.PlanResourceChangeCalled {
|
if !p.PlanResourceChangeCalled {
|
||||||
t.Errorf("Provider's PlanResourceChangeCalled wasn't called; should've been")
|
t.Errorf("Provider's PlanResourceChange wasn't called; should've been")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("postcondition fail refresh-only", func(t *testing.T) {
|
||||||
|
state := states.BuildState(func(s *states.SyncState) {
|
||||||
|
s.SetResourceInstanceCurrent(mustResourceInstanceAddr("test_resource.a"), &states.ResourceInstanceObjectSrc{
|
||||||
|
AttrsJSON: []byte(`{"value":"boop","output":"blorp"}`),
|
||||||
|
Status: states.ObjectReady,
|
||||||
|
}, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`))
|
||||||
|
})
|
||||||
|
p.ReadResourceFn = func(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) {
|
||||||
|
newVal, err := cty.Transform(req.PriorState, func(path cty.Path, v cty.Value) (cty.Value, error) {
|
||||||
|
if len(path) == 1 && path[0] == (cty.GetAttrStep{Name: "output"}) {
|
||||||
|
return cty.StringVal(""), nil
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
// shouldn't get here
|
||||||
|
t.Fatalf("ReadResourceFn transform failed")
|
||||||
|
return providers.ReadResourceResponse{}
|
||||||
|
}
|
||||||
|
return providers.ReadResourceResponse{
|
||||||
|
NewState: newVal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, diags := ctx.Plan(m, state, &PlanOpts{
|
||||||
|
Mode: plans.RefreshOnlyMode,
|
||||||
|
SetVariables: InputValues{
|
||||||
|
"boop": &InputValue{
|
||||||
|
Value: cty.StringVal("boop"),
|
||||||
|
SourceType: ValueFromCLIArg,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assertNoErrors(t, diags)
|
||||||
|
if len(diags) == 0 {
|
||||||
|
t.Fatalf("no diags, but should have warnings")
|
||||||
|
}
|
||||||
|
if got, want := diags.ErrWithWarnings().Error(), "Resource postcondition failed: Output must not be blank."; got != want {
|
||||||
|
t.Fatalf("wrong warning:\ngot: %s\nwant: %q", got, want)
|
||||||
|
}
|
||||||
|
if !p.ReadResourceCalled {
|
||||||
|
t.Errorf("Provider's ReadResource wasn't called; should've been")
|
||||||
|
}
|
||||||
|
if p.PlanResourceChangeCalled {
|
||||||
|
t.Errorf("Provider's PlanResourceChange was called; should'nt've been")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("precondition and postcondition fail refresh-only", func(t *testing.T) {
|
||||||
|
state := states.BuildState(func(s *states.SyncState) {
|
||||||
|
s.SetResourceInstanceCurrent(mustResourceInstanceAddr("test_resource.a"), &states.ResourceInstanceObjectSrc{
|
||||||
|
AttrsJSON: []byte(`{"value":"boop","output":"blorp"}`),
|
||||||
|
Status: states.ObjectReady,
|
||||||
|
}, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`))
|
||||||
|
})
|
||||||
|
p.ReadResourceFn = func(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) {
|
||||||
|
newVal, err := cty.Transform(req.PriorState, func(path cty.Path, v cty.Value) (cty.Value, error) {
|
||||||
|
if len(path) == 1 && path[0] == (cty.GetAttrStep{Name: "output"}) {
|
||||||
|
return cty.StringVal(""), nil
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
// shouldn't get here
|
||||||
|
t.Fatalf("ReadResourceFn transform failed")
|
||||||
|
return providers.ReadResourceResponse{}
|
||||||
|
}
|
||||||
|
return providers.ReadResourceResponse{
|
||||||
|
NewState: newVal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, diags := ctx.Plan(m, state, &PlanOpts{
|
||||||
|
Mode: plans.RefreshOnlyMode,
|
||||||
|
SetVariables: InputValues{
|
||||||
|
"boop": &InputValue{
|
||||||
|
Value: cty.StringVal("nope"),
|
||||||
|
SourceType: ValueFromCLIArg,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assertNoErrors(t, diags)
|
||||||
|
if got, want := len(diags), 2; got != want {
|
||||||
|
t.Errorf("wrong number of warnings, got %d, want %d", got, want)
|
||||||
|
}
|
||||||
|
warnings := diags.ErrWithWarnings().Error()
|
||||||
|
wantWarnings := []string{
|
||||||
|
"Resource precondition failed: Wrong boop.",
|
||||||
|
"Resource postcondition failed: Output must not be blank.",
|
||||||
|
}
|
||||||
|
for _, want := range wantWarnings {
|
||||||
|
if !strings.Contains(warnings, want) {
|
||||||
|
t.Errorf("missing warning:\ngot: %s\nwant to contain: %q", warnings, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !p.ReadResourceCalled {
|
||||||
|
t.Errorf("Provider's ReadResource wasn't called; should've been")
|
||||||
|
}
|
||||||
|
if p.PlanResourceChangeCalled {
|
||||||
|
t.Errorf("Provider's PlanResourceChange was called; should'nt've been")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -2432,6 +2561,39 @@ resource "test_resource" "a" {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("precondition fail refresh-only", func(t *testing.T) {
|
||||||
|
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
|
||||||
|
Mode: plans.RefreshOnlyMode,
|
||||||
|
SetVariables: InputValues{
|
||||||
|
"boop": &InputValue{
|
||||||
|
Value: cty.StringVal("nope"),
|
||||||
|
SourceType: ValueFromCLIArg,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assertNoErrors(t, diags)
|
||||||
|
if len(diags) == 0 {
|
||||||
|
t.Fatalf("no diags, but should have warnings")
|
||||||
|
}
|
||||||
|
if got, want := diags.ErrWithWarnings().Error(), "Resource precondition failed: Wrong boop."; got != want {
|
||||||
|
t.Fatalf("wrong warning:\ngot: %s\nwant: %q", got, want)
|
||||||
|
}
|
||||||
|
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("postcondition fail", func(t *testing.T) {
|
t.Run("postcondition fail", func(t *testing.T) {
|
||||||
p.ReadDataSourceResponse = &providers.ReadDataSourceResponse{
|
p.ReadDataSourceResponse = &providers.ReadDataSourceResponse{
|
||||||
State: cty.ObjectVal(map[string]cty.Value{
|
State: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
@ -2458,6 +2620,60 @@ resource "test_resource" "a" {
|
||||||
t.Errorf("Provider's ReadDataSource wasn't called; should've been")
|
t.Errorf("Provider's ReadDataSource wasn't called; should've been")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("postcondition fail refresh-only", 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.RefreshOnlyMode,
|
||||||
|
SetVariables: InputValues{
|
||||||
|
"boop": &InputValue{
|
||||||
|
Value: cty.StringVal("boop"),
|
||||||
|
SourceType: ValueFromCLIArg,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assertNoErrors(t, diags)
|
||||||
|
if got, want := diags.ErrWithWarnings().Error(), "Resource postcondition failed: Results cannot be empty."; got != want {
|
||||||
|
t.Fatalf("wrong error:\ngot: %s\nwant: %q", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("precondition and postcondition fail refresh-only", func(t *testing.T) {
|
||||||
|
p.ReadDataSourceResponse = &providers.ReadDataSourceResponse{
|
||||||
|
State: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"foo": cty.StringVal("nope"),
|
||||||
|
"results": cty.ListValEmpty(cty.String),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
_, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
|
||||||
|
Mode: plans.RefreshOnlyMode,
|
||||||
|
SetVariables: InputValues{
|
||||||
|
"boop": &InputValue{
|
||||||
|
Value: cty.StringVal("nope"),
|
||||||
|
SourceType: ValueFromCLIArg,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assertNoErrors(t, diags)
|
||||||
|
if got, want := len(diags), 2; got != want {
|
||||||
|
t.Errorf("wrong number of warnings, got %d, want %d", got, want)
|
||||||
|
}
|
||||||
|
warnings := diags.ErrWithWarnings().Error()
|
||||||
|
wantWarnings := []string{
|
||||||
|
"Resource precondition failed: Wrong boop.",
|
||||||
|
"Resource postcondition failed: Results cannot be empty.",
|
||||||
|
}
|
||||||
|
for _, want := range wantWarnings {
|
||||||
|
if !strings.Contains(warnings, want) {
|
||||||
|
t.Errorf("missing warning:\ngot: %s\nwant to contain: %q", warnings, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContext2Plan_outputPrecondition(t *testing.T) {
|
func TestContext2Plan_outputPrecondition(t *testing.T) {
|
||||||
|
@ -2530,6 +2746,36 @@ output "a" {
|
||||||
t.Fatalf("wrong error:\ngot: %s\nwant: %q", got, want)
|
t.Fatalf("wrong error:\ngot: %s\nwant: %q", got, want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("condition fail refresh-only", func(t *testing.T) {
|
||||||
|
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
|
||||||
|
Mode: plans.RefreshOnlyMode,
|
||||||
|
SetVariables: InputValues{
|
||||||
|
"boop": &InputValue{
|
||||||
|
Value: cty.StringVal("nope"),
|
||||||
|
SourceType: ValueFromCLIArg,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assertNoErrors(t, diags)
|
||||||
|
if len(diags) == 0 {
|
||||||
|
t.Fatalf("no diags, but should have warnings")
|
||||||
|
}
|
||||||
|
if got, want := diags.ErrWithWarnings().Error(), "Module output value precondition failed: Wrong boop."; got != want {
|
||||||
|
t.Errorf("wrong warning:\ngot: %s\nwant: %q", got, want)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContext2Plan_preconditionErrors(t *testing.T) {
|
func TestContext2Plan_preconditionErrors(t *testing.T) {
|
||||||
|
|
|
@ -48,12 +48,14 @@ func (c checkType) FailureSummary() string {
|
||||||
//
|
//
|
||||||
// If any of the rules do not pass, the returned diagnostics will contain
|
// If any of the rules do not pass, the returned diagnostics will contain
|
||||||
// errors. Otherwise, it will either be empty or contain only warnings.
|
// 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) {
|
func evalCheckRules(typ checkType, rules []*configs.CheckRule, ctx EvalContext, self addrs.Referenceable, keyData instances.RepetitionData, diagSeverity tfdiags.Severity) (diags tfdiags.Diagnostics) {
|
||||||
if len(rules) == 0 {
|
if len(rules) == 0 {
|
||||||
// Nothing to do
|
// Nothing to do
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
severity := diagSeverity.ToHCL()
|
||||||
|
|
||||||
for _, rule := range rules {
|
for _, rule := range rules {
|
||||||
const errInvalidCondition = "Invalid condition result"
|
const errInvalidCondition = "Invalid condition result"
|
||||||
var ruleDiags tfdiags.Diagnostics
|
var ruleDiags tfdiags.Diagnostics
|
||||||
|
@ -85,7 +87,7 @@ func evalCheckRules(typ checkType, rules []*configs.CheckRule, ctx EvalContext,
|
||||||
}
|
}
|
||||||
if result.IsNull() {
|
if result.IsNull() {
|
||||||
diags = diags.Append(&hcl.Diagnostic{
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
Severity: hcl.DiagError,
|
Severity: severity,
|
||||||
Summary: errInvalidCondition,
|
Summary: errInvalidCondition,
|
||||||
Detail: "Condition expression must return either true or false, not null.",
|
Detail: "Condition expression must return either true or false, not null.",
|
||||||
Subject: rule.Condition.Range().Ptr(),
|
Subject: rule.Condition.Range().Ptr(),
|
||||||
|
@ -98,7 +100,7 @@ func evalCheckRules(typ checkType, rules []*configs.CheckRule, ctx EvalContext,
|
||||||
result, err = convert.Convert(result, cty.Bool)
|
result, err = convert.Convert(result, cty.Bool)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
diags = diags.Append(&hcl.Diagnostic{
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
Severity: hcl.DiagError,
|
Severity: severity,
|
||||||
Summary: errInvalidCondition,
|
Summary: errInvalidCondition,
|
||||||
Detail: fmt.Sprintf("Invalid condition result value: %s.", tfdiags.FormatError(err)),
|
Detail: fmt.Sprintf("Invalid condition result value: %s.", tfdiags.FormatError(err)),
|
||||||
Subject: rule.Condition.Range().Ptr(),
|
Subject: rule.Condition.Range().Ptr(),
|
||||||
|
@ -118,7 +120,7 @@ func evalCheckRules(typ checkType, rules []*configs.CheckRule, ctx EvalContext,
|
||||||
errorValue, err = convert.Convert(errorValue, cty.String)
|
errorValue, err = convert.Convert(errorValue, cty.String)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
diags = diags.Append(&hcl.Diagnostic{
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
Severity: hcl.DiagError,
|
Severity: severity,
|
||||||
Summary: "Invalid error message",
|
Summary: "Invalid error message",
|
||||||
Detail: fmt.Sprintf("Unsuitable value for error message: %s.", tfdiags.FormatError(err)),
|
Detail: fmt.Sprintf("Unsuitable value for error message: %s.", tfdiags.FormatError(err)),
|
||||||
Subject: rule.ErrorMessage.Range().Ptr(),
|
Subject: rule.ErrorMessage.Range().Ptr(),
|
||||||
|
@ -133,7 +135,7 @@ func evalCheckRules(typ checkType, rules []*configs.CheckRule, ctx EvalContext,
|
||||||
errorMessage = "Failed to evaluate condition error message."
|
errorMessage = "Failed to evaluate condition error message."
|
||||||
}
|
}
|
||||||
diags = diags.Append(&hcl.Diagnostic{
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
Severity: hcl.DiagError,
|
Severity: severity,
|
||||||
Summary: typ.FailureSummary(),
|
Summary: typ.FailureSummary(),
|
||||||
Detail: errorMessage,
|
Detail: errorMessage,
|
||||||
Subject: rule.Condition.Range().Ptr(),
|
Subject: rule.Condition.Range().Ptr(),
|
||||||
|
|
|
@ -99,7 +99,7 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer {
|
||||||
&RootVariableTransformer{Config: b.Config, RawValues: b.RootVariableValues},
|
&RootVariableTransformer{Config: b.Config, RawValues: b.RootVariableValues},
|
||||||
&ModuleVariableTransformer{Config: b.Config},
|
&ModuleVariableTransformer{Config: b.Config},
|
||||||
&LocalTransformer{Config: b.Config},
|
&LocalTransformer{Config: b.Config},
|
||||||
&OutputTransformer{Config: b.Config},
|
&OutputTransformer{Config: b.Config, RefreshOnly: b.skipPlanChanges},
|
||||||
|
|
||||||
// Add orphan resources
|
// Add orphan resources
|
||||||
&OrphanResourceInstanceTransformer{
|
&OrphanResourceInstanceTransformer{
|
||||||
|
|
|
@ -24,6 +24,7 @@ type nodeExpandOutput struct {
|
||||||
Config *configs.Output
|
Config *configs.Output
|
||||||
Changes []*plans.OutputChangeSrc
|
Changes []*plans.OutputChangeSrc
|
||||||
Destroy bool
|
Destroy bool
|
||||||
|
RefreshOnly bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -69,6 +70,7 @@ func (n *nodeExpandOutput) DynamicExpand(ctx EvalContext) (*Graph, error) {
|
||||||
Addr: absAddr,
|
Addr: absAddr,
|
||||||
Config: n.Config,
|
Config: n.Config,
|
||||||
Change: change,
|
Change: change,
|
||||||
|
RefreshOnly: n.RefreshOnly,
|
||||||
}
|
}
|
||||||
log.Printf("[TRACE] Expanding output: adding %s as %T", o.Addr.String(), o)
|
log.Printf("[TRACE] Expanding output: adding %s as %T", o.Addr.String(), o)
|
||||||
g.Add(o)
|
g.Add(o)
|
||||||
|
@ -157,6 +159,10 @@ type NodeApplyableOutput struct {
|
||||||
Config *configs.Output // Config is the output in the config
|
Config *configs.Output // Config is the output in the config
|
||||||
// If this is being evaluated during apply, we may have a change recorded already
|
// If this is being evaluated during apply, we may have a change recorded already
|
||||||
Change *plans.OutputChangeSrc
|
Change *plans.OutputChangeSrc
|
||||||
|
|
||||||
|
// Refresh-only mode means that any failing output preconditions are
|
||||||
|
// reported as warnings rather than errors
|
||||||
|
RefreshOnly bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -270,10 +276,15 @@ func (n *NodeApplyableOutput) Execute(ctx EvalContext, op walkOperation) (diags
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkRuleSeverity := tfdiags.Error
|
||||||
|
if n.RefreshOnly {
|
||||||
|
checkRuleSeverity = tfdiags.Warning
|
||||||
|
}
|
||||||
checkDiags := evalCheckRules(
|
checkDiags := evalCheckRules(
|
||||||
checkOutputPrecondition,
|
checkOutputPrecondition,
|
||||||
n.Config.Preconditions,
|
n.Config.Preconditions,
|
||||||
ctx, nil, EvalDataForNoInstanceKey,
|
ctx, nil, EvalDataForNoInstanceKey,
|
||||||
|
checkRuleSeverity,
|
||||||
)
|
)
|
||||||
diags = diags.Append(checkDiags)
|
diags = diags.Append(checkDiags)
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
|
@ -285,7 +296,10 @@ func (n *NodeApplyableOutput) Execute(ctx EvalContext, op walkOperation) (diags
|
||||||
if !changeRecorded || !val.IsWhollyKnown() {
|
if !changeRecorded || !val.IsWhollyKnown() {
|
||||||
// This has to run before we have a state lock, since evaluation also
|
// This has to run before we have a state lock, since evaluation also
|
||||||
// reads the state
|
// reads the state
|
||||||
val, diags = ctx.EvaluateExpr(n.Config.Expr, cty.DynamicPseudoType, nil)
|
var evalDiags tfdiags.Diagnostics
|
||||||
|
val, evalDiags = ctx.EvaluateExpr(n.Config.Expr, cty.DynamicPseudoType, nil)
|
||||||
|
diags = diags.Append(evalDiags)
|
||||||
|
|
||||||
// We'll handle errors below, after we have loaded the module.
|
// We'll handle errors below, after we have loaded the module.
|
||||||
// Outputs don't have a separate mode for validation, so validate
|
// Outputs don't have a separate mode for validation, so validate
|
||||||
// depends_on expressions here too
|
// depends_on expressions here too
|
||||||
|
|
|
@ -655,6 +655,7 @@ func (n *NodeAbstractResourceInstance) plan(
|
||||||
checkResourcePrecondition,
|
checkResourcePrecondition,
|
||||||
n.Config.Preconditions,
|
n.Config.Preconditions,
|
||||||
ctx, nil, keyData,
|
ctx, nil, keyData,
|
||||||
|
tfdiags.Error,
|
||||||
)
|
)
|
||||||
diags = diags.Append(checkDiags)
|
diags = diags.Append(checkDiags)
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
|
@ -1476,7 +1477,7 @@ 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, instances.RepetitionData, tfdiags.Diagnostics) {
|
func (n *NodeAbstractResourceInstance) planDataSource(ctx EvalContext, currentState *states.ResourceInstanceObject, checkRuleSeverity tfdiags.Severity) (*plans.ResourceInstanceChange, *states.ResourceInstanceObject, instances.RepetitionData, tfdiags.Diagnostics) {
|
||||||
var diags tfdiags.Diagnostics
|
var diags tfdiags.Diagnostics
|
||||||
var keyData instances.RepetitionData
|
var keyData instances.RepetitionData
|
||||||
var configVal cty.Value
|
var configVal cty.Value
|
||||||
|
@ -1510,6 +1511,7 @@ func (n *NodeAbstractResourceInstance) planDataSource(ctx EvalContext, currentSt
|
||||||
checkResourcePrecondition,
|
checkResourcePrecondition,
|
||||||
n.Config.Preconditions,
|
n.Config.Preconditions,
|
||||||
ctx, nil, keyData,
|
ctx, nil, keyData,
|
||||||
|
checkRuleSeverity,
|
||||||
)
|
)
|
||||||
diags = diags.Append(checkDiags)
|
diags = diags.Append(checkDiags)
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
|
@ -1689,6 +1691,7 @@ func (n *NodeAbstractResourceInstance) applyDataSource(ctx EvalContext, planned
|
||||||
checkResourcePrecondition,
|
checkResourcePrecondition,
|
||||||
n.Config.Preconditions,
|
n.Config.Preconditions,
|
||||||
ctx, nil, keyData,
|
ctx, nil, keyData,
|
||||||
|
tfdiags.Error,
|
||||||
)
|
)
|
||||||
diags = diags.Append(checkDiags)
|
diags = diags.Append(checkDiags)
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
|
|
|
@ -184,6 +184,7 @@ func (n *NodeApplyableResourceInstance) dataResourceExecute(ctx EvalContext) (di
|
||||||
n.Config.Postconditions,
|
n.Config.Postconditions,
|
||||||
ctx, n.ResourceInstanceAddr().Resource,
|
ctx, n.ResourceInstanceAddr().Resource,
|
||||||
repeatData,
|
repeatData,
|
||||||
|
tfdiags.Error,
|
||||||
)
|
)
|
||||||
diags = diags.Append(checkDiags)
|
diags = diags.Append(checkDiags)
|
||||||
|
|
||||||
|
@ -361,6 +362,7 @@ func (n *NodeApplyableResourceInstance) managedResourceExecute(ctx EvalContext)
|
||||||
checkResourcePostcondition,
|
checkResourcePostcondition,
|
||||||
n.Config.Postconditions,
|
n.Config.Postconditions,
|
||||||
ctx, addr, repeatData,
|
ctx, addr, repeatData,
|
||||||
|
tfdiags.Error,
|
||||||
)
|
)
|
||||||
diags = diags.Append(checkDiags)
|
diags = diags.Append(checkDiags)
|
||||||
|
|
||||||
|
|
|
@ -95,7 +95,12 @@ func (n *NodePlannableResourceInstance) dataResourceExecute(ctx EvalContext) (di
|
||||||
return diags
|
return diags
|
||||||
}
|
}
|
||||||
|
|
||||||
change, state, repeatData, planDiags := n.planDataSource(ctx, state)
|
checkRuleSeverity := tfdiags.Error
|
||||||
|
if n.skipPlanChanges {
|
||||||
|
checkRuleSeverity = tfdiags.Warning
|
||||||
|
}
|
||||||
|
|
||||||
|
change, state, repeatData, planDiags := n.planDataSource(ctx, state, checkRuleSeverity)
|
||||||
diags = diags.Append(planDiags)
|
diags = diags.Append(planDiags)
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
return diags
|
return diags
|
||||||
|
@ -122,6 +127,7 @@ func (n *NodePlannableResourceInstance) dataResourceExecute(ctx EvalContext) (di
|
||||||
checkResourcePostcondition,
|
checkResourcePostcondition,
|
||||||
n.Config.Postconditions,
|
n.Config.Postconditions,
|
||||||
ctx, addr.Resource, repeatData,
|
ctx, addr.Resource, repeatData,
|
||||||
|
checkRuleSeverity,
|
||||||
)
|
)
|
||||||
diags = diags.Append(checkDiags)
|
diags = diags.Append(checkDiags)
|
||||||
|
|
||||||
|
@ -263,9 +269,28 @@ func (n *NodePlannableResourceInstance) managedResourceExecute(ctx EvalContext)
|
||||||
checkResourcePostcondition,
|
checkResourcePostcondition,
|
||||||
n.Config.Postconditions,
|
n.Config.Postconditions,
|
||||||
ctx, addr.Resource, repeatData,
|
ctx, addr.Resource, repeatData,
|
||||||
|
tfdiags.Error,
|
||||||
)
|
)
|
||||||
diags = diags.Append(checkDiags)
|
diags = diags.Append(checkDiags)
|
||||||
} else {
|
} else {
|
||||||
|
// In refresh-only mode we need to evaluate the for-each expression in
|
||||||
|
// order to supply the value to the pre- and post-condition check
|
||||||
|
// blocks. This has the unfortunate edge case of a refresh-only plan
|
||||||
|
// executing with a for-each map which has the same keys but different
|
||||||
|
// values, which could result in a post-condition check relying on that
|
||||||
|
// value being inaccurate. Unless we decide to store the value of the
|
||||||
|
// for-each expression in state, this is unavoidable.
|
||||||
|
forEach, _ := evaluateForEachExpression(n.Config.ForEach, ctx)
|
||||||
|
repeatData := EvalDataForInstanceKey(n.ResourceInstanceAddr().Resource.Key, forEach)
|
||||||
|
|
||||||
|
checkDiags := evalCheckRules(
|
||||||
|
checkResourcePrecondition,
|
||||||
|
n.Config.Preconditions,
|
||||||
|
ctx, nil, repeatData,
|
||||||
|
tfdiags.Warning,
|
||||||
|
)
|
||||||
|
diags = diags.Append(checkDiags)
|
||||||
|
|
||||||
// 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.
|
||||||
// any output values refering to this will not react to the drift.
|
// any output values refering to this will not react to the drift.
|
||||||
|
@ -275,6 +300,19 @@ func (n *NodePlannableResourceInstance) managedResourceExecute(ctx EvalContext)
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
return diags
|
return diags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Here we also evaluate post-conditions after updating the working
|
||||||
|
// state, because we want to check against the result of the refresh.
|
||||||
|
// Unlike in normal planning mode, these checks are still evaluated
|
||||||
|
// even if pre-conditions generated diagnostics, because we have no
|
||||||
|
// planned changes to block.
|
||||||
|
checkDiags = evalCheckRules(
|
||||||
|
checkResourcePostcondition,
|
||||||
|
n.Config.Postconditions,
|
||||||
|
ctx, addr.Resource, repeatData,
|
||||||
|
tfdiags.Warning,
|
||||||
|
)
|
||||||
|
diags = diags.Append(checkDiags)
|
||||||
}
|
}
|
||||||
|
|
||||||
return diags
|
return diags
|
||||||
|
|
|
@ -19,9 +19,13 @@ type OutputTransformer struct {
|
||||||
Config *configs.Config
|
Config *configs.Config
|
||||||
Changes *plans.Changes
|
Changes *plans.Changes
|
||||||
|
|
||||||
// if this is a planed destroy, root outputs are still in the configuration
|
// If this is a planned destroy, root outputs are still in the configuration
|
||||||
// so we need to record that we wish to remove them
|
// so we need to record that we wish to remove them
|
||||||
Destroy bool
|
Destroy bool
|
||||||
|
|
||||||
|
// Refresh-only mode means that any failing output preconditions are
|
||||||
|
// reported as warnings rather than errors
|
||||||
|
RefreshOnly bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *OutputTransformer) Transform(g *Graph) error {
|
func (t *OutputTransformer) Transform(g *Graph) error {
|
||||||
|
@ -83,6 +87,7 @@ func (t *OutputTransformer) transform(g *Graph, c *configs.Config) error {
|
||||||
Addr: addr.Absolute(addrs.RootModuleInstance),
|
Addr: addr.Absolute(addrs.RootModuleInstance),
|
||||||
Config: o,
|
Config: o,
|
||||||
Change: rootChange,
|
Change: rootChange,
|
||||||
|
RefreshOnly: t.RefreshOnly,
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -92,6 +97,7 @@ func (t *OutputTransformer) transform(g *Graph, c *configs.Config) error {
|
||||||
Config: o,
|
Config: o,
|
||||||
Changes: changes,
|
Changes: changes,
|
||||||
Destroy: t.Destroy,
|
Destroy: t.Destroy,
|
||||||
|
RefreshOnly: t.RefreshOnly,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package tfdiags
|
package tfdiags
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/hashicorp/hcl/v2"
|
"github.com/hashicorp/hcl/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -24,6 +26,20 @@ const (
|
||||||
Warning Severity = 'W'
|
Warning Severity = 'W'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ToHCL converts a Severity to the equivalent HCL diagnostic severity.
|
||||||
|
func (s Severity) ToHCL() hcl.DiagnosticSeverity {
|
||||||
|
switch s {
|
||||||
|
case Warning:
|
||||||
|
return hcl.DiagWarning
|
||||||
|
case Error:
|
||||||
|
return hcl.DiagError
|
||||||
|
default:
|
||||||
|
// The above should always be exhaustive for all of the valid
|
||||||
|
// Severity values in this package.
|
||||||
|
panic(fmt.Sprintf("unknown diagnostic severity %s", s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type Description struct {
|
type Description struct {
|
||||||
Address string
|
Address string
|
||||||
Summary string
|
Summary string
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
package tfdiags
|
package tfdiags
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/hashicorp/hcl/v2"
|
"github.com/hashicorp/hcl/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -112,17 +110,7 @@ func (diags Diagnostics) ToHCL() hcl.Diagnostics {
|
||||||
hclDiag := &hcl.Diagnostic{
|
hclDiag := &hcl.Diagnostic{
|
||||||
Summary: desc.Summary,
|
Summary: desc.Summary,
|
||||||
Detail: desc.Detail,
|
Detail: desc.Detail,
|
||||||
}
|
Severity: severity.ToHCL(),
|
||||||
|
|
||||||
switch severity {
|
|
||||||
case Warning:
|
|
||||||
hclDiag.Severity = hcl.DiagWarning
|
|
||||||
case Error:
|
|
||||||
hclDiag.Severity = hcl.DiagError
|
|
||||||
default:
|
|
||||||
// The above should always be exhaustive for all of the valid
|
|
||||||
// Severity values in this package.
|
|
||||||
panic(fmt.Sprintf("unknown diagnostic severity %s", severity))
|
|
||||||
}
|
}
|
||||||
if source.Subject != nil {
|
if source.Subject != nil {
|
||||||
hclDiag.Subject = source.Subject.ToHCL().Ptr()
|
hclDiag.Subject = source.Subject.ToHCL().Ptr()
|
||||||
|
|
Loading…
Reference in New Issue