diff --git a/internal/checks/state_init.go b/internal/checks/state_init.go index a2af43c92453..4ab043740b96 100644 --- a/internal/checks/state_init.go +++ b/internal/checks/state_init.go @@ -49,6 +49,22 @@ func collectInitialStatuses(into addrs.Map[addrs.ConfigCheckable, *configCheckab into.Put(addr, st) } + for _, c := range cfg.Module.Checks { + addr := c.Addr().InModule(moduleAddr) + + st := &configCheckableState{ + checkTypes: map[addrs.CheckRuleType]int{ + addrs.CheckAssertion: len(c.Asserts), + }, + } + + if c.DataResource != nil { + st.checkTypes[addrs.CheckDataResource] = 1 + } + + into.Put(addr, st) + } + // Must also visit child modules to collect everything for _, child := range cfg.Children { collectInitialStatuses(into, child) diff --git a/internal/checks/state_test.go b/internal/checks/state_test.go index 03168daebf05..4829e04c38b8 100644 --- a/internal/checks/state_test.go +++ b/internal/checks/state_test.go @@ -62,6 +62,9 @@ func TestChecksHappyPath(t *testing.T) { childOutput := addrs.OutputValue{ Name: "b", }.InModule(moduleChild) + checkBlock := addrs.Check{ + Name: "check", + }.InModule(addrs.RootModule) // First some consistency checks to make sure our configuration is the // shape we are relying on it to be. @@ -77,6 +80,9 @@ func TestChecksHappyPath(t *testing.T) { if addr := resourceNonExist; cfg.Module.ResourceByAddr(addr.Resource) != nil { t.Fatalf("configuration includes %s, which is not supposed to exist", addr) } + if addr := checkBlock; cfg.Module.Checks[addr.Check.Name] == nil { + t.Fatalf("configuration does not include %s", addr) + } ///////////////////////////////////////////////////////////////////////// @@ -109,6 +115,10 @@ func TestChecksHappyPath(t *testing.T) { if addr := resourceNonExist; checks.ConfigHasChecks(addr) { t.Errorf("checks detected for %s, even though it doesn't exist", addr) } + if addr := checkBlock; !checks.ConfigHasChecks(addr) { + t.Errorf("checks not detected for %s", addr) + missing++ + } if missing > 0 { t.Fatalf("missing some configuration objects we'd need for subsequent testing") } @@ -124,6 +134,7 @@ func TestChecksHappyPath(t *testing.T) { resourceC, rootOutput, childOutput, + checkBlock, ) gotConfigAddrs := checks.AllConfigAddrs() if diff := cmp.Diff(wantConfigAddrs, gotConfigAddrs); diff != "" { @@ -153,6 +164,7 @@ func TestChecksHappyPath(t *testing.T) { resourceInstC0 := resourceC.Resource.Absolute(moduleChildInst).Instance(addrs.IntKey(0)) resourceInstC1 := resourceC.Resource.Absolute(moduleChildInst).Instance(addrs.IntKey(1)) childOutputInst := childOutput.OutputValue.Absolute(moduleChildInst) + checkBlockInst := checkBlock.Check.Absolute(addrs.RootModuleInstance) checks.ReportCheckableObjects(resourceA, addrs.MakeSet[addrs.Checkable](resourceInstA)) checks.ReportCheckResult(resourceInstA, addrs.ResourcePrecondition, 0, StatusPass) @@ -172,6 +184,9 @@ func TestChecksHappyPath(t *testing.T) { checks.ReportCheckableObjects(rootOutput, addrs.MakeSet[addrs.Checkable](rootOutputInst)) checks.ReportCheckResult(rootOutputInst, addrs.OutputPrecondition, 0, StatusPass) + checks.ReportCheckableObjects(checkBlock, addrs.MakeSet[addrs.Checkable](checkBlockInst)) + checks.ReportCheckResult(checkBlockInst, addrs.CheckAssertion, 0, StatusPass) + ///////////////////////////////////////////////////////////////////////// // This "section" is simulating what we might do to report the results @@ -185,7 +200,7 @@ func TestChecksHappyPath(t *testing.T) { t.Errorf("incorrect final aggregate check status for %s: %s, but want %s", configAddr, got, want) } } - if got, want := configCount, 5; got != want { + if got, want := configCount, 6; got != want { t.Errorf("incorrect number of known config addresses %d; want %d", got, want) } } @@ -198,6 +213,7 @@ func TestChecksHappyPath(t *testing.T) { resourceInstC0, resourceInstC1, childOutputInst, + checkBlockInst, ) for _, addr := range objAddrs { if got, want := checks.ObjectCheckStatus(addr), StatusPass; got != want { diff --git a/internal/checks/testdata/happypath/checks-happypath.tf b/internal/checks/testdata/happypath/checks-happypath.tf index 4a6ca46fca16..a9cd055b7ee3 100644 --- a/internal/checks/testdata/happypath/checks-happypath.tf +++ b/internal/checks/testdata/happypath/checks-happypath.tf @@ -30,3 +30,10 @@ output "a" { error_message = "A has no id." } } + +check "check" { + assert { + condition = null_resource.a.id != "" + error_message = "check block: A has no id" + } +} diff --git a/internal/command/format/diff.go b/internal/command/format/diff.go index ed30e3214aff..fe8ec31e7ef7 100644 --- a/internal/command/format/diff.go +++ b/internal/command/format/diff.go @@ -77,6 +77,8 @@ func ResourceChange( buf.WriteString("\n # (config refers to values not yet known)") case plans.ResourceInstanceReadBecauseDependencyPending: buf.WriteString("\n # (depends on a resource or a module with changes pending)") + case plans.ResourceInstanceReadBecauseCheckNested: + buf.WriteString("\n # (data will be read during apply for a check block)") } case plans.Update: switch language { diff --git a/internal/command/jsonchecks/checks_test.go b/internal/command/jsonchecks/checks_test.go index 6e0f52da4faa..55a947dc9a6f 100644 --- a/internal/command/jsonchecks/checks_test.go +++ b/internal/command/jsonchecks/checks_test.go @@ -36,6 +36,8 @@ func TestMarshalCheckStates(t *testing.T) { outputAInstAddr := addrs.Checkable(addrs.OutputValue{Name: "a"}.Absolute(addrs.RootModuleInstance)) outputBAddr := addrs.ConfigCheckable(addrs.OutputValue{Name: "b"}.InModule(moduleChildAddr.Module())) outputBInstAddr := addrs.Checkable(addrs.OutputValue{Name: "b"}.Absolute(moduleChildAddr)) + checkBlockAAddr := addrs.ConfigCheckable(addrs.Check{Name: "a"}.InModule(addrs.RootModule)) + checkBlockAInstAddr := addrs.Checkable(addrs.Check{Name: "a"}.Absolute(addrs.RootModuleInstance)) tests := map[string]struct { Input *states.CheckResults @@ -90,9 +92,42 @@ func TestMarshalCheckStates(t *testing.T) { }), ), }), + addrs.MakeMapElem(checkBlockAAddr, &states.CheckResultAggregate{ + Status: checks.StatusFail, + ObjectResults: addrs.MakeMap( + addrs.MakeMapElem(checkBlockAInstAddr, &states.CheckResultObject{ + Status: checks.StatusFail, + FailureMessages: []string{ + "Couldn't reverse the polarity.", + }, + }), + ), + }), ), }, []any{ + map[string]any{ + "//": "EXPERIMENTAL: see docs for details", + "address": map[string]any{ + "kind": "check", + "to_display": "check.a", + "name": "a", + }, + "instances": []any{ + map[string]any{ + "address": map[string]any{ + "to_display": `check.a`, + }, + "problems": []any{ + map[string]any{ + "message": "Couldn't reverse the polarity.", + }, + }, + "status": "fail", + }, + }, + "status": "fail", + }, map[string]any{ "//": "EXPERIMENTAL: see docs for details", "address": map[string]any{ diff --git a/internal/command/jsonchecks/objects.go b/internal/command/jsonchecks/objects.go index d7a5014fee13..ec0185755876 100644 --- a/internal/command/jsonchecks/objects.go +++ b/internal/command/jsonchecks/objects.go @@ -45,6 +45,17 @@ func makeStaticObjectAddr(addr addrs.ConfigCheckable) staticObjectAddr { if !addr.Module.IsRoot() { ret["module"] = addr.Module.String() } + case addrs.ConfigCheck: + if kind := addr.CheckableKind(); kind != addrs.CheckableCheck { + // Something has gone very wrong + panic(fmt.Sprintf("%T has CheckableKind %s", addr, kind)) + } + + ret["kind"] = "check" + ret["name"] = addr.Check.Name + if !addr.Module.IsRoot() { + ret["module"] = addr.Module.String() + } default: panic(fmt.Sprintf("unsupported ConfigCheckable implementation %T", addr)) } @@ -71,6 +82,10 @@ func makeDynamicObjectAddr(addr addrs.Checkable) dynamicObjectAddr { if !addr.Module.IsRoot() { ret["module"] = addr.Module.String() } + case addrs.AbsCheck: + if !addr.Module.IsRoot() { + ret["module"] = addr.Module.String() + } default: panic(fmt.Sprintf("unsupported Checkable implementation %T", addr)) } diff --git a/internal/command/jsonformat/plan.go b/internal/command/jsonformat/plan.go index 5deaeca54467..c214b0ad213b 100644 --- a/internal/command/jsonformat/plan.go +++ b/internal/command/jsonformat/plan.go @@ -361,6 +361,8 @@ func resourceChangeComment(resource jsonplan.ResourceChange, action plans.Action buf.WriteString("\n # (config refers to values not yet known)") case jsonplan.ResourceInstanceReadBecauseDependencyPending: buf.WriteString("\n # (depends on a resource or a module with changes pending)") + case jsonplan.ResourceInstanceReadBecauseCheckNested: + buf.WriteString("\n # (config will be reloaded to verify a check block)") } case plans.Update: switch changeCause { diff --git a/internal/command/jsonplan/plan.go b/internal/command/jsonplan/plan.go index fca01a1d28a7..5e960083344f 100644 --- a/internal/command/jsonplan/plan.go +++ b/internal/command/jsonplan/plan.go @@ -39,6 +39,7 @@ const ( ResourceInstanceDeleteBecauseNoMoveTarget = "delete_because_no_move_target" ResourceInstanceReadBecauseConfigUnknown = "read_because_config_unknown" ResourceInstanceReadBecauseDependencyPending = "read_because_dependency_pending" + ResourceInstanceReadBecauseCheckNested = "read_because_check_nested" ) // Plan is the top-level representation of the json format of a plan. It includes @@ -492,6 +493,8 @@ func MarshalResourceChanges(resources []*plans.ResourceInstanceChangeSrc, schema r.ActionReason = ResourceInstanceReadBecauseConfigUnknown case plans.ResourceInstanceReadBecauseDependencyPending: r.ActionReason = ResourceInstanceReadBecauseDependencyPending + case plans.ResourceInstanceReadBecauseCheckNested: + r.ActionReason = ResourceInstanceReadBecauseCheckNested default: return nil, fmt.Errorf("resource %s has an unsupported action reason %s", r.Address, rc.ActionReason) } diff --git a/internal/command/views/json/change.go b/internal/command/views/json/change.go index 60439e509017..fb4ec26e3df7 100644 --- a/internal/command/views/json/change.go +++ b/internal/command/views/json/change.go @@ -83,6 +83,7 @@ const ( ReasonDeleteBecauseNoMoveTarget ChangeReason = "delete_because_no_move_target" ReasonReadBecauseConfigUnknown ChangeReason = "read_because_config_unknown" ReasonReadBecauseDependencyPending ChangeReason = "read_because_dependency_pending" + ReasonReadBecauseCheckNested ChangeReason = "read_because_check_nested" ) func changeReason(reason plans.ResourceInstanceChangeActionReason) ChangeReason { @@ -113,6 +114,8 @@ func changeReason(reason plans.ResourceInstanceChangeActionReason) ChangeReason return ReasonDeleteBecauseNoMoveTarget case plans.ResourceInstanceReadBecauseDependencyPending: return ReasonReadBecauseDependencyPending + case plans.ResourceInstanceReadBecauseCheckNested: + return ReasonReadBecauseCheckNested default: // This should never happen, but there's no good way to guarantee // exhaustive handling of the enum, so a generic fall back is better diff --git a/internal/plans/changes.go b/internal/plans/changes.go index 7c54928331e6..7206d4f4645f 100644 --- a/internal/plans/changes.go +++ b/internal/plans/changes.go @@ -444,6 +444,12 @@ const ( // depends on a managed resource instance which has its own changes // pending. ResourceInstanceReadBecauseDependencyPending ResourceInstanceChangeActionReason = '!' + + // ResourceInstanceReadBecauseCheckNested indicates that the resource must + // be read during apply (as well as during planning) because it is inside + // a check block and when the check assertions execute we want them to use + // the most up-to-date data. + ResourceInstanceReadBecauseCheckNested ResourceInstanceChangeActionReason = '#' ) // OutputChange describes a change to an output value. diff --git a/internal/plans/internal/planproto/planfile.pb.go b/internal/plans/internal/planproto/planfile.pb.go index ec329d3ba198..df4ff03b73aa 100644 --- a/internal/plans/internal/planproto/planfile.pb.go +++ b/internal/plans/internal/planproto/planfile.pb.go @@ -152,6 +152,7 @@ const ( ResourceInstanceActionReason_REPLACE_BY_TRIGGERS ResourceInstanceActionReason = 9 ResourceInstanceActionReason_READ_BECAUSE_CONFIG_UNKNOWN ResourceInstanceActionReason = 10 ResourceInstanceActionReason_READ_BECAUSE_DEPENDENCY_PENDING ResourceInstanceActionReason = 11 + ResourceInstanceActionReason_READ_BECAUSE_CHECK_NESTED ResourceInstanceActionReason = 13 ResourceInstanceActionReason_DELETE_BECAUSE_NO_MOVE_TARGET ResourceInstanceActionReason = 12 ) @@ -170,6 +171,7 @@ var ( 9: "REPLACE_BY_TRIGGERS", 10: "READ_BECAUSE_CONFIG_UNKNOWN", 11: "READ_BECAUSE_DEPENDENCY_PENDING", + 13: "READ_BECAUSE_CHECK_NESTED", 12: "DELETE_BECAUSE_NO_MOVE_TARGET", } ResourceInstanceActionReason_value = map[string]int32{ @@ -185,6 +187,7 @@ var ( "REPLACE_BY_TRIGGERS": 9, "READ_BECAUSE_CONFIG_UNKNOWN": 10, "READ_BECAUSE_DEPENDENCY_PENDING": 11, + "READ_BECAUSE_CHECK_NESTED": 13, "DELETE_BECAUSE_NO_MOVE_TARGET": 12, } ) @@ -276,6 +279,7 @@ const ( CheckResults_UNSPECIFIED CheckResults_ObjectKind = 0 CheckResults_RESOURCE CheckResults_ObjectKind = 1 CheckResults_OUTPUT_VALUE CheckResults_ObjectKind = 2 + CheckResults_CHECK CheckResults_ObjectKind = 3 ) // Enum value maps for CheckResults_ObjectKind. @@ -284,11 +288,13 @@ var ( 0: "UNSPECIFIED", 1: "RESOURCE", 2: "OUTPUT_VALUE", + 3: "CHECK", } CheckResults_ObjectKind_value = map[string]int32{ "UNSPECIFIED": 0, "RESOURCE": 1, "OUTPUT_VALUE": 2, + "CHECK": 3, } ) @@ -1353,7 +1359,7 @@ var file_planfile_proto_rawDesc = []byte{ 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x06, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x22, 0xdd, + 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x22, 0xe8, 0x03, 0x0a, 0x0c, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x33, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x75, @@ -1380,65 +1386,68 @@ var file_planfile_proto_rawDesc = []byte{ 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x50, 0x41, 0x53, 0x53, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x46, 0x41, 0x49, 0x4c, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x22, - 0x3d, 0x0a, 0x0a, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x0f, 0x0a, + 0x48, 0x0a, 0x0a, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x0f, 0x0a, 0x0b, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, - 0x4f, 0x55, 0x54, 0x50, 0x55, 0x54, 0x5f, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x10, 0x02, 0x22, 0x28, - 0x0a, 0x0c, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x18, - 0x0a, 0x07, 0x6d, 0x73, 0x67, 0x70, 0x61, 0x63, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x07, 0x6d, 0x73, 0x67, 0x70, 0x61, 0x63, 0x6b, 0x22, 0xa5, 0x01, 0x0a, 0x04, 0x50, 0x61, 0x74, - 0x68, 0x12, 0x27, 0x0a, 0x05, 0x73, 0x74, 0x65, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x11, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x2e, 0x53, - 0x74, 0x65, 0x70, 0x52, 0x05, 0x73, 0x74, 0x65, 0x70, 0x73, 0x1a, 0x74, 0x0a, 0x04, 0x53, 0x74, - 0x65, 0x70, 0x12, 0x27, 0x0a, 0x0e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0d, 0x61, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x65, - 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x14, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, - 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x48, 0x00, 0x52, 0x0a, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x4b, 0x65, 0x79, 0x42, 0x0a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, - 0x2a, 0x31, 0x0a, 0x04, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x0a, 0x0a, 0x06, 0x4e, 0x4f, 0x52, 0x4d, - 0x41, 0x4c, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x53, 0x54, 0x52, 0x4f, 0x59, 0x10, - 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x52, 0x45, 0x46, 0x52, 0x45, 0x53, 0x48, 0x5f, 0x4f, 0x4e, 0x4c, - 0x59, 0x10, 0x02, 0x2a, 0x70, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x08, 0x0a, - 0x04, 0x4e, 0x4f, 0x4f, 0x50, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x52, 0x45, 0x41, 0x54, - 0x45, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x52, 0x45, 0x41, 0x44, 0x10, 0x02, 0x12, 0x0a, 0x0a, - 0x06, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x44, 0x45, 0x4c, - 0x45, 0x54, 0x45, 0x10, 0x05, 0x12, 0x16, 0x0a, 0x12, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, - 0x54, 0x48, 0x45, 0x4e, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x10, 0x06, 0x12, 0x16, 0x0a, - 0x12, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x5f, 0x54, 0x48, 0x45, 0x4e, 0x5f, 0x44, 0x45, 0x4c, - 0x45, 0x54, 0x45, 0x10, 0x07, 0x2a, 0xa9, 0x03, 0x0a, 0x1c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, - 0x12, 0x1b, 0x0a, 0x17, 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, - 0x55, 0x53, 0x45, 0x5f, 0x54, 0x41, 0x49, 0x4e, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x16, 0x0a, - 0x12, 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x5f, 0x42, 0x59, 0x5f, 0x52, 0x45, 0x51, 0x55, - 0x45, 0x53, 0x54, 0x10, 0x02, 0x12, 0x21, 0x0a, 0x1d, 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, - 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x43, 0x41, 0x4e, 0x4e, 0x4f, 0x54, 0x5f, - 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x10, 0x03, 0x12, 0x25, 0x0a, 0x21, 0x44, 0x45, 0x4c, 0x45, - 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x4e, 0x4f, 0x5f, 0x52, 0x45, - 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x10, 0x04, 0x12, - 0x23, 0x0a, 0x1f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, - 0x45, 0x5f, 0x57, 0x52, 0x4f, 0x4e, 0x47, 0x5f, 0x52, 0x45, 0x50, 0x45, 0x54, 0x49, 0x54, 0x49, - 0x4f, 0x4e, 0x10, 0x05, 0x12, 0x1e, 0x0a, 0x1a, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, - 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x43, 0x4f, 0x55, 0x4e, 0x54, 0x5f, 0x49, 0x4e, 0x44, - 0x45, 0x58, 0x10, 0x06, 0x12, 0x1b, 0x0a, 0x17, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, - 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x45, 0x41, 0x43, 0x48, 0x5f, 0x4b, 0x45, 0x59, 0x10, - 0x07, 0x12, 0x1c, 0x0a, 0x18, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, - 0x55, 0x53, 0x45, 0x5f, 0x4e, 0x4f, 0x5f, 0x4d, 0x4f, 0x44, 0x55, 0x4c, 0x45, 0x10, 0x08, 0x12, - 0x17, 0x0a, 0x13, 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x5f, 0x42, 0x59, 0x5f, 0x54, 0x52, - 0x49, 0x47, 0x47, 0x45, 0x52, 0x53, 0x10, 0x09, 0x12, 0x1f, 0x0a, 0x1b, 0x52, 0x45, 0x41, 0x44, - 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x5f, - 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x0a, 0x12, 0x23, 0x0a, 0x1f, 0x52, 0x45, 0x41, - 0x44, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x44, 0x45, 0x50, 0x45, 0x4e, 0x44, - 0x45, 0x4e, 0x43, 0x59, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x0b, 0x12, 0x21, - 0x0a, 0x1d, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, - 0x5f, 0x4e, 0x4f, 0x5f, 0x4d, 0x4f, 0x56, 0x45, 0x5f, 0x54, 0x41, 0x52, 0x47, 0x45, 0x54, 0x10, - 0x0c, 0x42, 0x42, 0x5a, 0x40, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, - 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, - 0x6f, 0x72, 0x6d, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x6c, 0x61, - 0x6e, 0x73, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x6c, 0x61, 0x6e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x4f, 0x55, 0x54, 0x50, 0x55, 0x54, 0x5f, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x10, 0x02, 0x12, 0x09, + 0x0a, 0x05, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x10, 0x03, 0x22, 0x28, 0x0a, 0x0c, 0x44, 0x79, 0x6e, + 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x73, 0x67, + 0x70, 0x61, 0x63, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6d, 0x73, 0x67, 0x70, + 0x61, 0x63, 0x6b, 0x22, 0xa5, 0x01, 0x0a, 0x04, 0x50, 0x61, 0x74, 0x68, 0x12, 0x27, 0x0a, 0x05, + 0x73, 0x74, 0x65, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x74, 0x66, + 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x2e, 0x53, 0x74, 0x65, 0x70, 0x52, 0x05, + 0x73, 0x74, 0x65, 0x70, 0x73, 0x1a, 0x74, 0x0a, 0x04, 0x53, 0x74, 0x65, 0x70, 0x12, 0x27, 0x0a, + 0x0e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0d, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x74, 0x66, + 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x48, 0x00, 0x52, 0x0a, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x4b, 0x65, 0x79, 0x42, + 0x0a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x2a, 0x31, 0x0a, 0x04, 0x4d, + 0x6f, 0x64, 0x65, 0x12, 0x0a, 0x0a, 0x06, 0x4e, 0x4f, 0x52, 0x4d, 0x41, 0x4c, 0x10, 0x00, 0x12, + 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x53, 0x54, 0x52, 0x4f, 0x59, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, + 0x52, 0x45, 0x46, 0x52, 0x45, 0x53, 0x48, 0x5f, 0x4f, 0x4e, 0x4c, 0x59, 0x10, 0x02, 0x2a, 0x70, + 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4f, 0x50, + 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x10, 0x01, 0x12, 0x08, + 0x0a, 0x04, 0x52, 0x45, 0x41, 0x44, 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x55, 0x50, 0x44, 0x41, + 0x54, 0x45, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x10, 0x05, + 0x12, 0x16, 0x0a, 0x12, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x54, 0x48, 0x45, 0x4e, 0x5f, + 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x10, 0x06, 0x12, 0x16, 0x0a, 0x12, 0x43, 0x52, 0x45, 0x41, + 0x54, 0x45, 0x5f, 0x54, 0x48, 0x45, 0x4e, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x10, 0x07, + 0x2a, 0xc8, 0x03, 0x0a, 0x1c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, + 0x74, 0x61, 0x6e, 0x63, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x61, 0x73, 0x6f, + 0x6e, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x52, + 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x54, + 0x41, 0x49, 0x4e, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x52, 0x45, 0x50, 0x4c, + 0x41, 0x43, 0x45, 0x5f, 0x42, 0x59, 0x5f, 0x52, 0x45, 0x51, 0x55, 0x45, 0x53, 0x54, 0x10, 0x02, + 0x12, 0x21, 0x0a, 0x1d, 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, + 0x55, 0x53, 0x45, 0x5f, 0x43, 0x41, 0x4e, 0x4e, 0x4f, 0x54, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, + 0x45, 0x10, 0x03, 0x12, 0x25, 0x0a, 0x21, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, + 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x4e, 0x4f, 0x5f, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, + 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x10, 0x04, 0x12, 0x23, 0x0a, 0x1f, 0x44, 0x45, + 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x57, 0x52, 0x4f, + 0x4e, 0x47, 0x5f, 0x52, 0x45, 0x50, 0x45, 0x54, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x05, 0x12, + 0x1e, 0x0a, 0x1a, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, + 0x45, 0x5f, 0x43, 0x4f, 0x55, 0x4e, 0x54, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x10, 0x06, 0x12, + 0x1b, 0x0a, 0x17, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, + 0x45, 0x5f, 0x45, 0x41, 0x43, 0x48, 0x5f, 0x4b, 0x45, 0x59, 0x10, 0x07, 0x12, 0x1c, 0x0a, 0x18, + 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x4e, + 0x4f, 0x5f, 0x4d, 0x4f, 0x44, 0x55, 0x4c, 0x45, 0x10, 0x08, 0x12, 0x17, 0x0a, 0x13, 0x52, 0x45, + 0x50, 0x4c, 0x41, 0x43, 0x45, 0x5f, 0x42, 0x59, 0x5f, 0x54, 0x52, 0x49, 0x47, 0x47, 0x45, 0x52, + 0x53, 0x10, 0x09, 0x12, 0x1f, 0x0a, 0x1b, 0x52, 0x45, 0x41, 0x44, 0x5f, 0x42, 0x45, 0x43, 0x41, + 0x55, 0x53, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, + 0x57, 0x4e, 0x10, 0x0a, 0x12, 0x23, 0x0a, 0x1f, 0x52, 0x45, 0x41, 0x44, 0x5f, 0x42, 0x45, 0x43, + 0x41, 0x55, 0x53, 0x45, 0x5f, 0x44, 0x45, 0x50, 0x45, 0x4e, 0x44, 0x45, 0x4e, 0x43, 0x59, 0x5f, + 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x0b, 0x12, 0x1d, 0x0a, 0x19, 0x52, 0x45, 0x41, + 0x44, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x5f, + 0x4e, 0x45, 0x53, 0x54, 0x45, 0x44, 0x10, 0x0d, 0x12, 0x21, 0x0a, 0x1d, 0x44, 0x45, 0x4c, 0x45, + 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x4e, 0x4f, 0x5f, 0x4d, 0x4f, + 0x56, 0x45, 0x5f, 0x54, 0x41, 0x52, 0x47, 0x45, 0x54, 0x10, 0x0c, 0x42, 0x42, 0x5a, 0x40, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, + 0x6f, 0x72, 0x70, 0x2f, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x6c, 0x61, 0x6e, 0x73, 0x2f, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x6c, 0x61, 0x6e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/internal/plans/internal/planproto/planfile.proto b/internal/plans/internal/planproto/planfile.proto index fa7cb774c958..f7a10353b045 100644 --- a/internal/plans/internal/planproto/planfile.proto +++ b/internal/plans/internal/planproto/planfile.proto @@ -156,6 +156,7 @@ enum ResourceInstanceActionReason { REPLACE_BY_TRIGGERS = 9; READ_BECAUSE_CONFIG_UNKNOWN = 10; READ_BECAUSE_DEPENDENCY_PENDING = 11; + READ_BECAUSE_CHECK_NESTED = 13; DELETE_BECAUSE_NO_MOVE_TARGET = 12; } @@ -237,6 +238,7 @@ message CheckResults { UNSPECIFIED = 0; RESOURCE = 1; OUTPUT_VALUE = 2; + CHECK = 3; } message ObjectResult { diff --git a/internal/plans/planfile/tfplan.go b/internal/plans/planfile/tfplan.go index 19866b6714e9..f8b982497d79 100644 --- a/internal/plans/planfile/tfplan.go +++ b/internal/plans/planfile/tfplan.go @@ -5,6 +5,7 @@ import ( "io" "io/ioutil" + "github.com/zclconf/go-cty/cty" "google.golang.org/protobuf/proto" "github.com/hashicorp/terraform/internal/addrs" @@ -15,7 +16,6 @@ import ( "github.com/hashicorp/terraform/internal/plans/internal/planproto" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/version" - "github.com/zclconf/go-cty/cty" ) const tfplanFormatVersion = 3 @@ -114,6 +114,8 @@ func readTfplan(r io.Reader) (*plans.Plan, error) { objKind = addrs.CheckableResource case planproto.CheckResults_OUTPUT_VALUE: objKind = addrs.CheckableOutputValue + case planproto.CheckResults_CHECK: + objKind = addrs.CheckableCheck default: return nil, fmt.Errorf("aggregate check results for %s have unsupported object kind %s", rawCRs.ConfigAddr, objKind) } @@ -333,6 +335,8 @@ func resourceChangeFromTfplan(rawChange *planproto.ResourceInstanceChange) (*pla ret.ActionReason = plans.ResourceInstanceReadBecauseConfigUnknown case planproto.ResourceInstanceActionReason_READ_BECAUSE_DEPENDENCY_PENDING: ret.ActionReason = plans.ResourceInstanceReadBecauseDependencyPending + case planproto.ResourceInstanceActionReason_READ_BECAUSE_CHECK_NESTED: + ret.ActionReason = plans.ResourceInstanceReadBecauseCheckNested case planproto.ResourceInstanceActionReason_DELETE_BECAUSE_NO_MOVE_TARGET: ret.ActionReason = plans.ResourceInstanceDeleteBecauseNoMoveTarget default: @@ -524,6 +528,8 @@ func writeTfplan(plan *plans.Plan, w io.Writer) error { pcrs.Kind = planproto.CheckResults_RESOURCE case addrs.CheckableOutputValue: pcrs.Kind = planproto.CheckResults_OUTPUT_VALUE + case addrs.CheckableCheck: + pcrs.Kind = planproto.CheckResults_CHECK default: return fmt.Errorf("checkable configuration %s has unsupported object type kind %s", configElem.Key, kind) } @@ -711,6 +717,8 @@ func resourceChangeToTfplan(change *plans.ResourceInstanceChangeSrc) (*planproto ret.ActionReason = planproto.ResourceInstanceActionReason_READ_BECAUSE_CONFIG_UNKNOWN case plans.ResourceInstanceReadBecauseDependencyPending: ret.ActionReason = planproto.ResourceInstanceActionReason_READ_BECAUSE_DEPENDENCY_PENDING + case plans.ResourceInstanceReadBecauseCheckNested: + ret.ActionReason = planproto.ResourceInstanceActionReason_READ_BECAUSE_CHECK_NESTED case plans.ResourceInstanceDeleteBecauseNoMoveTarget: ret.ActionReason = planproto.ResourceInstanceActionReason_DELETE_BECAUSE_NO_MOVE_TARGET default: diff --git a/internal/plans/planfile/tfplan_test.go b/internal/plans/planfile/tfplan_test.go index 6984ceafdcad..217319373b9e 100644 --- a/internal/plans/planfile/tfplan_test.go +++ b/internal/plans/planfile/tfplan_test.go @@ -196,6 +196,25 @@ func TestTFPlanRoundTrip(t *testing.T) { ), }, ), + addrs.MakeMapElem[addrs.ConfigCheckable]( + addrs.Check{ + Name: "check", + }.InModule(addrs.RootModule), + &states.CheckResultAggregate{ + Status: checks.StatusFail, + ObjectResults: addrs.MakeMap( + addrs.MakeMapElem[addrs.Checkable]( + addrs.Check{ + Name: "check", + }.Absolute(addrs.RootModuleInstance), + &states.CheckResultObject{ + Status: checks.StatusFail, + FailureMessages: []string{"check failed"}, + }, + ), + ), + }, + ), ), }, TargetAddrs: []addrs.Targetable{ diff --git a/internal/plans/resourceinstancechangeactionreason_string.go b/internal/plans/resourceinstancechangeactionreason_string.go index 742aae235651..9915ec85bc1f 100644 --- a/internal/plans/resourceinstancechangeactionreason_string.go +++ b/internal/plans/resourceinstancechangeactionreason_string.go @@ -21,23 +21,25 @@ func _() { _ = x[ResourceInstanceDeleteBecauseNoMoveTarget-65] _ = x[ResourceInstanceReadBecauseConfigUnknown-63] _ = x[ResourceInstanceReadBecauseDependencyPending-33] + _ = x[ResourceInstanceReadBecauseCheckNested-35] } const ( _ResourceInstanceChangeActionReason_name_0 = "ResourceInstanceChangeNoReason" _ResourceInstanceChangeActionReason_name_1 = "ResourceInstanceReadBecauseDependencyPending" - _ResourceInstanceChangeActionReason_name_2 = "ResourceInstanceReadBecauseConfigUnknown" - _ResourceInstanceChangeActionReason_name_3 = "ResourceInstanceDeleteBecauseNoMoveTarget" - _ResourceInstanceChangeActionReason_name_4 = "ResourceInstanceDeleteBecauseCountIndexResourceInstanceReplaceByTriggersResourceInstanceDeleteBecauseEachKeyResourceInstanceReplaceBecauseCannotUpdate" - _ResourceInstanceChangeActionReason_name_5 = "ResourceInstanceDeleteBecauseNoModuleResourceInstanceDeleteBecauseNoResourceConfig" - _ResourceInstanceChangeActionReason_name_6 = "ResourceInstanceReplaceByRequest" - _ResourceInstanceChangeActionReason_name_7 = "ResourceInstanceReplaceBecauseTainted" - _ResourceInstanceChangeActionReason_name_8 = "ResourceInstanceDeleteBecauseWrongRepetition" + _ResourceInstanceChangeActionReason_name_2 = "ResourceInstanceReadBecauseCheckNested" + _ResourceInstanceChangeActionReason_name_3 = "ResourceInstanceReadBecauseConfigUnknown" + _ResourceInstanceChangeActionReason_name_4 = "ResourceInstanceDeleteBecauseNoMoveTarget" + _ResourceInstanceChangeActionReason_name_5 = "ResourceInstanceDeleteBecauseCountIndexResourceInstanceReplaceByTriggersResourceInstanceDeleteBecauseEachKeyResourceInstanceReplaceBecauseCannotUpdate" + _ResourceInstanceChangeActionReason_name_6 = "ResourceInstanceDeleteBecauseNoModuleResourceInstanceDeleteBecauseNoResourceConfig" + _ResourceInstanceChangeActionReason_name_7 = "ResourceInstanceReplaceByRequest" + _ResourceInstanceChangeActionReason_name_8 = "ResourceInstanceReplaceBecauseTainted" + _ResourceInstanceChangeActionReason_name_9 = "ResourceInstanceDeleteBecauseWrongRepetition" ) var ( - _ResourceInstanceChangeActionReason_index_4 = [...]uint8{0, 39, 72, 108, 150} - _ResourceInstanceChangeActionReason_index_5 = [...]uint8{0, 37, 82} + _ResourceInstanceChangeActionReason_index_5 = [...]uint8{0, 39, 72, 108, 150} + _ResourceInstanceChangeActionReason_index_6 = [...]uint8{0, 37, 82} ) func (i ResourceInstanceChangeActionReason) String() string { @@ -46,22 +48,24 @@ func (i ResourceInstanceChangeActionReason) String() string { return _ResourceInstanceChangeActionReason_name_0 case i == 33: return _ResourceInstanceChangeActionReason_name_1 - case i == 63: + case i == 35: return _ResourceInstanceChangeActionReason_name_2 - case i == 65: + case i == 63: return _ResourceInstanceChangeActionReason_name_3 + case i == 65: + return _ResourceInstanceChangeActionReason_name_4 case 67 <= i && i <= 70: i -= 67 - return _ResourceInstanceChangeActionReason_name_4[_ResourceInstanceChangeActionReason_index_4[i]:_ResourceInstanceChangeActionReason_index_4[i+1]] + return _ResourceInstanceChangeActionReason_name_5[_ResourceInstanceChangeActionReason_index_5[i]:_ResourceInstanceChangeActionReason_index_5[i+1]] case 77 <= i && i <= 78: i -= 77 - return _ResourceInstanceChangeActionReason_name_5[_ResourceInstanceChangeActionReason_index_5[i]:_ResourceInstanceChangeActionReason_index_5[i+1]] + return _ResourceInstanceChangeActionReason_name_6[_ResourceInstanceChangeActionReason_index_6[i]:_ResourceInstanceChangeActionReason_index_6[i+1]] case i == 82: - return _ResourceInstanceChangeActionReason_name_6 - case i == 84: return _ResourceInstanceChangeActionReason_name_7 - case i == 87: + case i == 84: return _ResourceInstanceChangeActionReason_name_8 + case i == 87: + return _ResourceInstanceChangeActionReason_name_9 default: return "ResourceInstanceChangeActionReason(" + strconv.FormatInt(int64(i), 10) + ")" } diff --git a/internal/states/statefile/version4.go b/internal/states/statefile/version4.go index 2cdde4be7107..18a775bd5f62 100644 --- a/internal/states/statefile/version4.go +++ b/internal/states/statefile/version4.go @@ -638,6 +638,8 @@ func decodeCheckableObjectKindV4(in string) addrs.CheckableKind { return addrs.CheckableResource case "output": return addrs.CheckableOutputValue + case "check": + return addrs.CheckableCheck default: // We'll treat anything else as invalid just as a concession to // forward-compatible parsing, in case a later version of Terraform @@ -652,6 +654,8 @@ func encodeCheckableObjectKindV4(in addrs.CheckableKind) string { return "resource" case addrs.CheckableOutputValue: return "output" + case addrs.CheckableCheck: + return "check" default: panic(fmt.Sprintf("unsupported checkable object kind %s", in)) } diff --git a/internal/terraform/context_apply_checks_test.go b/internal/terraform/context_apply_checks_test.go new file mode 100644 index 000000000000..b18642605690 --- /dev/null +++ b/internal/terraform/context_apply_checks_test.go @@ -0,0 +1,689 @@ +package terraform + +import ( + "testing" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/checks" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// This file contains 'integration' tests for the Terraform check blocks. +// +// These tests could live in context_apply_test or context_apply2_test but given +// the size of those files, it makes sense to keep these check related tests +// grouped together. + +type checksTestingStatus struct { + status checks.Status + messages []string +} + +func TestContextChecks(t *testing.T) { + tests := map[string]struct { + configs map[string]string + plan map[string]checksTestingStatus + planError string + apply map[string]checksTestingStatus + applyError string + state *states.State + provider *MockProvider + providerHook func(*MockProvider) + }{ + "passing": { + configs: map[string]string{ + "main.tf": ` +provider "checks" {} + +check "passing" { + data "checks_object" "positive" {} + + assert { + condition = data.checks_object.positive.number >= 0 + error_message = "negative number" + } +} +`, + }, + plan: map[string]checksTestingStatus{ + "passing": { + status: checks.StatusPass, + }, + }, + apply: map[string]checksTestingStatus{ + "passing": { + status: checks.StatusPass, + }, + }, + provider: &MockProvider{ + Meta: "checks", + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + DataSources: map[string]providers.Schema{ + "checks_object": { + Block: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "number": { + Type: cty.Number, + Computed: true, + }, + }, + }, + }, + }, + }, + ReadDataSourceFn: func(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + return providers.ReadDataSourceResponse{ + State: cty.ObjectVal(map[string]cty.Value{ + "number": cty.NumberIntVal(0), + }), + } + }, + }, + }, + "failing": { + configs: map[string]string{ + "main.tf": ` +provider "checks" {} + +check "failing" { + data "checks_object" "positive" {} + + assert { + condition = data.checks_object.positive.number >= 0 + error_message = "negative number" + } +} +`, + }, + plan: map[string]checksTestingStatus{ + "failing": { + status: checks.StatusFail, + messages: []string{"negative number"}, + }, + }, + apply: map[string]checksTestingStatus{ + "failing": { + status: checks.StatusFail, + messages: []string{"negative number"}, + }, + }, + provider: &MockProvider{ + Meta: "checks", + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + DataSources: map[string]providers.Schema{ + "checks_object": { + Block: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "number": { + Type: cty.Number, + Computed: true, + }, + }, + }, + }, + }, + }, + ReadDataSourceFn: func(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + return providers.ReadDataSourceResponse{ + State: cty.ObjectVal(map[string]cty.Value{ + "number": cty.NumberIntVal(-1), + }), + } + }, + }, + }, + "mixed": { + configs: map[string]string{ + "main.tf": ` +provider "checks" {} + +check "failing" { + data "checks_object" "neutral" {} + + assert { + condition = data.checks_object.neutral.number >= 0 + error_message = "negative number" + } + + assert { + condition = data.checks_object.neutral.number < 0 + error_message = "positive number" + } +} +`, + }, + plan: map[string]checksTestingStatus{ + "failing": { + status: checks.StatusFail, + messages: []string{"positive number"}, + }, + }, + apply: map[string]checksTestingStatus{ + "failing": { + status: checks.StatusFail, + messages: []string{"positive number"}, + }, + }, + provider: &MockProvider{ + Meta: "checks", + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + DataSources: map[string]providers.Schema{ + "checks_object": { + Block: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "number": { + Type: cty.Number, + Computed: true, + }, + }, + }, + }, + }, + }, + ReadDataSourceFn: func(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + return providers.ReadDataSourceResponse{ + State: cty.ObjectVal(map[string]cty.Value{ + "number": cty.NumberIntVal(0), + }), + } + }, + }, + }, + "nested data blocks reload during apply": { + configs: map[string]string{ + "main.tf": ` +provider "checks" {} + +data "checks_object" "data_block" {} + +check "data_block" { + assert { + condition = data.checks_object.data_block.number >= 0 + error_message = "negative number" + } +} + +check "nested_data_block" { + data "checks_object" "nested_data_block" {} + + assert { + condition = data.checks_object.nested_data_block.number >= 0 + error_message = "negative number" + } +} +`, + }, + plan: map[string]checksTestingStatus{ + "nested_data_block": { + status: checks.StatusFail, + messages: []string{"negative number"}, + }, + "data_block": { + status: checks.StatusFail, + messages: []string{"negative number"}, + }, + }, + apply: map[string]checksTestingStatus{ + "nested_data_block": { + status: checks.StatusPass, + }, + "data_block": { + status: checks.StatusFail, + messages: []string{"negative number"}, + }, + }, + provider: &MockProvider{ + Meta: "checks", + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + DataSources: map[string]providers.Schema{ + "checks_object": { + Block: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "number": { + Type: cty.Number, + Computed: true, + }, + }, + }, + }, + }, + }, + ReadDataSourceFn: func(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + return providers.ReadDataSourceResponse{ + State: cty.ObjectVal(map[string]cty.Value{ + "number": cty.NumberIntVal(-1), + }), + } + }, + }, + providerHook: func(provider *MockProvider) { + provider.ReadDataSourceFn = func(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + // The data returned by the data sources are changing + // between the plan and apply stage. The nested data block + // will update to reflect this while the normal data block + // will not detect the change. + return providers.ReadDataSourceResponse{ + State: cty.ObjectVal(map[string]cty.Value{ + "number": cty.NumberIntVal(0), + }), + } + } + }, + }, + "returns unknown for unknown config": { + configs: map[string]string{ + "main.tf": ` +provider "checks" {} + +resource "checks_object" "resource_block" {} + +check "resource_block" { + data "checks_object" "data_block" { + id = checks_object.resource_block.id + } + + assert { + condition = data.checks_object.data_block.number >= 0 + error_message = "negative number" + } +} +`, + }, + plan: map[string]checksTestingStatus{ + "resource_block": { + status: checks.StatusUnknown, + }, + }, + apply: map[string]checksTestingStatus{ + "resource_block": { + status: checks.StatusPass, + }, + }, + provider: &MockProvider{ + Meta: "checks", + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "checks_object": { + Block: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + }, + DataSources: map[string]providers.Schema{ + "checks_object": { + Block: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + "number": { + Type: cty.Number, + Computed: true, + }, + }, + }, + }, + }, + }, + PlanResourceChangeFn: func(request providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { + return providers.PlanResourceChangeResponse{ + PlannedState: cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + }), + } + }, + ApplyResourceChangeFn: func(request providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { + return providers.ApplyResourceChangeResponse{ + NewState: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("7A9F887D-44C7-4281-80E5-578E41F99DFC"), + }), + } + }, + ReadDataSourceFn: func(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + values := request.Config.AsValueMap() + if id, ok := values["id"]; ok { + if id.IsKnown() && id.AsString() == "7A9F887D-44C7-4281-80E5-578E41F99DFC" { + return providers.ReadDataSourceResponse{ + State: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("7A9F887D-44C7-4281-80E5-578E41F99DFC"), + "number": cty.NumberIntVal(0), + }), + } + } + } + + return providers.ReadDataSourceResponse{ + Diagnostics: tfdiags.Diagnostics{tfdiags.Sourceless(tfdiags.Error, "shouldn't make it here", "really shouldn't make it here")}, + } + }, + }, + }, + "failing nested data source doesn't block the plan": { + configs: map[string]string{ + "main.tf": ` +provider "checks" {} + +check "error" { + data "checks_object" "data_block" {} + + assert { + condition = data.checks_object.data_block.number >= 0 + error_message = "negative number" + } +} +`, + }, + plan: map[string]checksTestingStatus{ + "error": { + status: checks.StatusFail, + messages: []string{ + "data source read failed: something bad happened and the provider couldn't read the data source", + }, + }, + }, + apply: map[string]checksTestingStatus{ + "error": { + status: checks.StatusFail, + messages: []string{ + "data source read failed: something bad happened and the provider couldn't read the data source", + }, + }, + }, + provider: &MockProvider{ + Meta: "checks", + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + DataSources: map[string]providers.Schema{ + "checks_object": { + Block: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "number": { + Type: cty.Number, + Computed: true, + }, + }, + }, + }, + }, + }, + ReadDataSourceFn: func(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + return providers.ReadDataSourceResponse{ + Diagnostics: tfdiags.Diagnostics{tfdiags.Sourceless(tfdiags.Error, "data source read failed", "something bad happened and the provider couldn't read the data source")}, + } + }, + }, + }, + "check failing in state and passing after plan and apply": { + configs: map[string]string{ + "main.tf": ` +provider "checks" {} + +resource "checks_object" "resource" { + number = 0 +} + +check "passing" { + assert { + condition = checks_object.resource.number >= 0 + error_message = "negative number" + } +} +`, + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "checks_object", + Name: "resource", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"number": -1}`), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }) + }), + plan: map[string]checksTestingStatus{ + "passing": { + status: checks.StatusPass, + }, + }, + apply: map[string]checksTestingStatus{ + "passing": { + status: checks.StatusPass, + }, + }, + provider: &MockProvider{ + Meta: "checks", + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "checks_object": { + Block: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "number": { + Type: cty.Number, + Required: true, + }, + }, + }, + }, + }, + }, + PlanResourceChangeFn: func(request providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { + return providers.PlanResourceChangeResponse{ + PlannedState: cty.ObjectVal(map[string]cty.Value{ + "number": cty.NumberIntVal(0), + }), + } + }, + ApplyResourceChangeFn: func(request providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { + return providers.ApplyResourceChangeResponse{ + NewState: cty.ObjectVal(map[string]cty.Value{ + "number": cty.NumberIntVal(0), + }), + } + }, + }, + }, + "failing data source does block the plan": { + configs: map[string]string{ + "main.tf": ` +provider "checks" {} + +data "checks_object" "data_block" {} + +check "error" { + assert { + condition = data.checks_object.data_block.number >= 0 + error_message = "negative number" + } +} +`, + }, + planError: "data source read failed: something bad happened and the provider couldn't read the data source", + provider: &MockProvider{ + Meta: "checks", + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + DataSources: map[string]providers.Schema{ + "checks_object": { + Block: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "number": { + Type: cty.Number, + Computed: true, + }, + }, + }, + }, + }, + }, + ReadDataSourceFn: func(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + return providers.ReadDataSourceResponse{ + Diagnostics: tfdiags.Diagnostics{tfdiags.Sourceless(tfdiags.Error, "data source read failed", "something bad happened and the provider couldn't read the data source")}, + } + }, + }, + }, + "invalid reference into check block": { + configs: map[string]string{ + "main.tf": ` +provider "checks" {} + +data "checks_object" "data_block" { + id = data.checks_object.nested_data_block.id +} + +check "error" { + data "checks_object" "nested_data_block" {} + + assert { + condition = data.checks_object.data_block.number >= 0 + error_message = "negative number" + } +} +`, + }, + planError: "Reference to scoped resource: The referenced data resource \"checks_object\" \"nested_data_block\" is not available from this context.", + provider: &MockProvider{ + Meta: "checks", + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + DataSources: map[string]providers.Schema{ + "checks_object": { + Block: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + Optional: true, + }, + }, + }, + }, + }, + }, + ReadDataSourceFn: func(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + input := request.Config.AsValueMap() + if _, ok := input["id"]; ok { + return providers.ReadDataSourceResponse{ + State: request.Config, + } + } + + return providers.ReadDataSourceResponse{ + State: cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + }), + } + }, + }, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + configs := testModuleInline(t, test.configs) + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider(test.provider.Meta.(string)): testProviderFuncFixed(test.provider), + }, + }) + + initialState := states.NewState() + if test.state != nil { + initialState = test.state + } + + plan, diags := ctx.Plan(configs, initialState, &PlanOpts{ + Mode: plans.NormalMode, + }) + if validateError(t, "planning", test.planError, diags) { + return + } + validateCheckResults(t, "planning", test.plan, plan.Checks) + + if test.providerHook != nil { + // This gives an opportunity to change the behaviour of the + // provider between the plan and apply stages. + test.providerHook(test.provider) + } + + state, diags := ctx.Apply(plan, configs) + if validateError(t, "apply", test.applyError, diags) { + return + } + validateCheckResults(t, "apply", test.apply, state.CheckResults) + }) + } +} + +func validateError(t *testing.T, stage string, expected string, actual tfdiags.Diagnostics) bool { + if expected != "" { + if !actual.HasErrors() { + t.Errorf("expected %s to error with \"%s\", but no errors were returned", stage, expected) + } else if expected != actual.Err().Error() { + t.Errorf("expected %s to error with \"%s\" but found \"%s\"", stage, expected, actual.Err()) + } + return true + } + + assertNoErrors(t, actual) + return false +} + +func validateCheckResults(t *testing.T, stage string, expected map[string]checksTestingStatus, actual *states.CheckResults) { + + // Just a quick sanity check that the plan or apply process didn't create + // some non-existent checks. + if len(expected) != len(actual.ConfigResults.Keys()) { + t.Errorf("expected %d check results but found %d after %s", len(expected), len(actual.ConfigResults.Keys()), stage) + } + + // Now, lets make sure the checks all match what we expect. + for check, want := range expected { + results := actual.GetObjectResult(addrs.Check{ + Name: check, + }.Absolute(addrs.RootModuleInstance)) + + if results.Status != want.status { + t.Errorf("%s: wanted %s but got %s after %s", check, want.status, results.Status, stage) + } + + if len(want.messages) != len(results.FailureMessages) { + t.Errorf("%s: expected %d failure messages but had %d after %s", check, len(want.messages), len(results.FailureMessages), stage) + } + + max := len(want.messages) + if len(results.FailureMessages) > max { + max = len(results.FailureMessages) + } + + for ix := 0; ix < max; ix++ { + var expected, actual string + if ix < len(want.messages) { + expected = want.messages[ix] + } + if ix < len(results.FailureMessages) { + actual = results.FailureMessages[ix] + } + + // Order matters! + if actual != expected { + t.Errorf("%s: expected failure message at %d to be \"%s\" but was \"%s\" after %s", check, ix, expected, actual, stage) + } + } + + } +} diff --git a/internal/terraform/eval_conditions.go b/internal/terraform/eval_conditions.go index 9edd9199be24..881f5dd59520 100644 --- a/internal/terraform/eval_conditions.go +++ b/internal/terraform/eval_conditions.go @@ -67,9 +67,8 @@ type checkResult struct { FailureMessage string } -func evalCheckRule(typ addrs.CheckRuleType, rule *configs.CheckRule, ctx EvalContext, self addrs.Checkable, keyData instances.RepetitionData, severity hcl.DiagnosticSeverity) (checkResult, tfdiags.Diagnostics) { +func validateCheckRule(typ addrs.CheckRuleType, rule *configs.CheckRule, ctx EvalContext, self addrs.Checkable, keyData instances.RepetitionData) (string, *hcl.EvalContext, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics - const errInvalidCondition = "Invalid condition result" refs, moreDiags := lang.ReferencesInExpr(rule.Condition) diags = diags.Append(moreDiags) @@ -77,29 +76,47 @@ func evalCheckRule(typ addrs.CheckRuleType, rule *configs.CheckRule, ctx EvalCon diags = diags.Append(moreDiags) refs = append(refs, moreRefs...) - var selfReference addrs.Referenceable - // Only resource postconditions can refer to self - if typ == addrs.ResourcePostcondition { + var selfReference, sourceReference addrs.Referenceable + switch typ { + case addrs.ResourcePostcondition: switch s := self.(type) { case addrs.AbsResourceInstance: + // Only resource postconditions can refer to self selfReference = s.Resource default: panic(fmt.Sprintf("Invalid self reference type %t", self)) } + case addrs.CheckAssertion: + switch s := self.(type) { + case addrs.AbsCheck: + // Only check blocks have scoped resources so need to specify their + // source. + sourceReference = s.Check + default: + panic(fmt.Sprintf("Invalid source reference type %t", self)) + } } - scope := ctx.EvaluationScope(selfReference, nil, keyData) + scope := ctx.EvaluationScope(selfReference, sourceReference, keyData) hclCtx, moreDiags := scope.EvalContext(refs) diags = diags.Append(moreDiags) - resultVal, hclDiags := rule.Condition.Value(hclCtx) - diags = diags.Append(hclDiags) + errorMessage, moreDiags := evalCheckErrorMessage(rule.ErrorMessage, hclCtx) + diags = diags.Append(moreDiags) + + return errorMessage, hclCtx, diags +} +func evalCheckRule(typ addrs.CheckRuleType, rule *configs.CheckRule, ctx EvalContext, self addrs.Checkable, keyData instances.RepetitionData, severity hcl.DiagnosticSeverity) (checkResult, tfdiags.Diagnostics) { // NOTE: Intentionally not passing the caller's selected severity in here, // because this reports errors in the configuration itself, not the failure // of an otherwise-valid condition. - errorMessage, moreDiags := evalCheckErrorMessage(rule.ErrorMessage, hclCtx) - diags = diags.Append(moreDiags) + errorMessage, hclCtx, diags := validateCheckRule(typ, rule, ctx, self, keyData) + + const errInvalidCondition = "Invalid condition result" + + resultVal, hclDiags := rule.Condition.Value(hclCtx) + diags = diags.Append(hclDiags) if diags.HasErrors() { log.Printf("[TRACE] evalCheckRule: %s: %s", typ, diags.Err().Error()) @@ -107,6 +124,19 @@ func evalCheckRule(typ addrs.CheckRuleType, rule *configs.CheckRule, ctx EvalCon } if !resultVal.IsKnown() { + + // Check assertions warn if a status is unknown. + if typ == addrs.CheckAssertion { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: fmt.Sprintf("%s known after apply", typ.Description()), + Detail: "The condition could not be evaluated at this time, a result will be known when this plan is applied.", + Subject: rule.Condition.Range().Ptr(), + Expression: rule.Condition, + EvalContext: hclCtx, + }) + } + // We'll wait until we've learned more, then. return checkResult{Status: checks.StatusUnknown}, diags } diff --git a/internal/terraform/evaluate_valid_test.go b/internal/terraform/evaluate_valid_test.go index 920bdbef2d7b..2d8d5734b900 100644 --- a/internal/terraform/evaluate_valid_test.go +++ b/internal/terraform/evaluate_valid_test.go @@ -5,6 +5,7 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configschema" @@ -14,41 +15,42 @@ import ( func TestStaticValidateReferences(t *testing.T) { tests := []struct { Ref string + Src addrs.Referenceable WantErr string }{ { - "aws_instance.no_count", - ``, + Ref: "aws_instance.no_count", + WantErr: ``, }, { - "aws_instance.count", - ``, + Ref: "aws_instance.count", + WantErr: ``, }, { - "aws_instance.count[0]", - ``, + Ref: "aws_instance.count[0]", + WantErr: ``, }, { - "aws_instance.nonexist", - `Reference to undeclared resource: A managed resource "aws_instance" "nonexist" has not been declared in the root module.`, + Ref: "aws_instance.nonexist", + WantErr: `Reference to undeclared resource: A managed resource "aws_instance" "nonexist" has not been declared in the root module.`, }, { - "beep.boop", - `Reference to undeclared resource: A managed resource "beep" "boop" has not been declared in the root module. + Ref: "beep.boop", + WantErr: `Reference to undeclared resource: A managed resource "beep" "boop" has not been declared in the root module. Did you mean the data resource data.beep.boop?`, }, { - "aws_instance.no_count[0]", - `Unexpected resource instance key: Because aws_instance.no_count does not have "count" or "for_each" set, references to it must not include an index key. Remove the bracketed index to refer to the single instance of this resource.`, + Ref: "aws_instance.no_count[0]", + WantErr: `Unexpected resource instance key: Because aws_instance.no_count does not have "count" or "for_each" set, references to it must not include an index key. Remove the bracketed index to refer to the single instance of this resource.`, }, { - "aws_instance.count.foo", + Ref: "aws_instance.count.foo", // In this case we return two errors that are somewhat redundant with // one another, but we'll accept that because they both report the // problem from different perspectives and so give the user more // opportunity to understand what's going on here. - `2 problems: + WantErr: `2 problems: - Missing resource instance key: Because aws_instance.count has "count" set, its attributes must be accessed on specific instances. @@ -57,12 +59,21 @@ For example, to correlate with indices of a referring resource, use: - Unsupported attribute: This object has no argument, nested block, or exported attribute named "foo".`, }, { - "boop_instance.yep", - ``, + Ref: "boop_instance.yep", + WantErr: ``, }, { - "boop_whatever.nope", - `Invalid resource type: A managed resource type "boop_whatever" is not supported by provider "registry.terraform.io/foobar/beep".`, + Ref: "boop_whatever.nope", + WantErr: `Invalid resource type: A managed resource type "boop_whatever" is not supported by provider "registry.terraform.io/foobar/beep".`, + }, + { + Ref: "data.boop_data.boop_nested", + WantErr: `Reference to scoped resource: The referenced data resource "boop_data" "boop_nested" is not available from this context.`, + }, + { + Ref: "data.boop_data.boop_nested", + WantErr: ``, + Src: addrs.Check{Name: "foo"}, }, } @@ -80,6 +91,16 @@ For example, to correlate with indices of a referring resource, use: // intentional mismatch between resource type prefix and provider type "boop_instance": {}, }, + DataSources: map[string]*configschema.Block{ + "boop_data": { + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, }, }), } @@ -100,7 +121,7 @@ For example, to correlate with indices of a referring resource, use: Evaluator: evaluator, } - diags = data.StaticValidateReferences(refs, nil, nil) + diags = data.StaticValidateReferences(refs, nil, test.Src) if diags.HasErrors() { if test.WantErr == "" { t.Fatalf("Unexpected diagnostics: %s", diags.Err()) diff --git a/internal/terraform/graph_builder_apply.go b/internal/terraform/graph_builder_apply.go index 29e60ff6a0d8..57c1660a845b 100644 --- a/internal/terraform/graph_builder_apply.go +++ b/internal/terraform/graph_builder_apply.go @@ -110,6 +110,13 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer { Config: b.Config, }, + // Add nodes and edges for check block assertions. Check block data + // sources were added earlier. + &checkTransformer{ + Config: b.Config, + Operation: b.Operation, + }, + // Attach the state &AttachStateTransformer{State: b.State}, diff --git a/internal/terraform/graph_builder_plan.go b/internal/terraform/graph_builder_plan.go index f085e9aa2dad..ffa88207cd62 100644 --- a/internal/terraform/graph_builder_plan.go +++ b/internal/terraform/graph_builder_plan.go @@ -127,6 +127,13 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer { Planning: true, }, + // Add nodes and edges for the check block assertions. Check block data + // sources were added earlier. + &checkTransformer{ + Config: b.Config, + Operation: b.Operation, + }, + // Add orphan resources &OrphanResourceInstanceTransformer{ Concrete: b.ConcreteResourceOrphan, diff --git a/internal/terraform/node_check.go b/internal/terraform/node_check.go new file mode 100644 index 000000000000..486bfe107c0d --- /dev/null +++ b/internal/terraform/node_check.go @@ -0,0 +1,164 @@ +package terraform + +import ( + "log" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/checks" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/dag" + "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +var ( + _ GraphNodeModulePath = (*nodeReportCheck)(nil) + _ GraphNodeExecutable = (*nodeReportCheck)(nil) +) + +// nodeReportCheck calls the ReportCheckableObjects function for our assertions +// within the check blocks. +// +// We need this to happen before the checks are actually verified and before any +// nested data blocks, so the creator of this structure should make sure this +// node is a parent of any nested data blocks. +// +// This needs to be separate to nodeExpandCheck, because the actual checks +// should happen after referenced data blocks rather than before. +type nodeReportCheck struct { + addr addrs.ConfigCheck +} + +func (n *nodeReportCheck) ModulePath() addrs.Module { + return n.addr.Module +} + +func (n *nodeReportCheck) Execute(ctx EvalContext, _ walkOperation) tfdiags.Diagnostics { + exp := ctx.InstanceExpander() + modInsts := exp.ExpandModule(n.ModulePath()) + + instAddrs := addrs.MakeSet[addrs.Checkable]() + for _, modAddr := range modInsts { + instAddrs.Add(n.addr.Check.Absolute(modAddr)) + } + ctx.Checks().ReportCheckableObjects(n.addr, instAddrs) + return nil +} + +func (n *nodeReportCheck) Name() string { + return n.addr.String() + " (report)" +} + +var ( + _ GraphNodeModulePath = (*nodeExpandCheck)(nil) + _ GraphNodeDynamicExpandable = (*nodeExpandCheck)(nil) + _ GraphNodeReferencer = (*nodeExpandCheck)(nil) +) + +// nodeExpandCheck creates child nodes that actually execute the assertions for +// a given check block. +// +// This must happen after any other nodes/resources/data sources that are +// referenced, so we implement GraphNodeReferencer. +// +// This needs to be separate to nodeReportCheck as nodeReportCheck must happen +// first, while nodeExpandCheck must execute after any referenced blocks. +type nodeExpandCheck struct { + addr addrs.ConfigCheck + config *configs.Check + + makeInstance func(addrs.AbsCheck, *configs.Check) dag.Vertex +} + +func (n *nodeExpandCheck) ModulePath() addrs.Module { + return n.addr.Module +} + +func (n *nodeExpandCheck) DynamicExpand(ctx EvalContext) (*Graph, error) { + exp := ctx.InstanceExpander() + modInsts := exp.ExpandModule(n.ModulePath()) + + var g Graph + for _, modAddr := range modInsts { + testAddr := n.addr.Check.Absolute(modAddr) + log.Printf("[TRACE] nodeExpandCheck: Node for %s", testAddr) + g.Add(n.makeInstance(testAddr, n.config)) + } + addRootNodeToGraph(&g) + + return &g, nil +} + +func (n *nodeExpandCheck) References() []*addrs.Reference { + var refs []*addrs.Reference + for _, assert := range n.config.Asserts { + condition, _ := lang.ReferencesInExpr(assert.Condition) + message, _ := lang.ReferencesInExpr(assert.ErrorMessage) + refs = append(refs, condition...) + refs = append(refs, message...) + } + return refs +} + +func (n *nodeExpandCheck) Name() string { + return n.addr.String() + " (expand)" +} + +var ( + _ GraphNodeModuleInstance = (*nodeCheckAssert)(nil) + _ GraphNodeExecutable = (*nodeCheckAssert)(nil) +) + +type nodeCheckAssert struct { + addr addrs.AbsCheck + config *configs.Check + + // We only want to actually execute the checks during the plan and apply + // operations, but we still want to validate our config during + // other operations. + executeChecks bool +} + +func (n *nodeCheckAssert) ModulePath() addrs.Module { + return n.Path().Module() +} + +func (n *nodeCheckAssert) Path() addrs.ModuleInstance { + return n.addr.Module +} + +func (n *nodeCheckAssert) Execute(ctx EvalContext, _ walkOperation) tfdiags.Diagnostics { + + // We only want to actually execute the checks during specific + // operations, such as plan and applies. + if n.executeChecks { + if status := ctx.Checks().ObjectCheckStatus(n.addr); status == checks.StatusFail || status == checks.StatusError { + // This check is already failing, so we won't try and evaluate it. + // This typically means there was an error in a data block within + // the check block. + return nil + } + + return evalCheckRules( + addrs.CheckAssertion, + n.config.Asserts, + ctx, + n.addr, + EvalDataForNoInstanceKey, + tfdiags.Warning) + + } + + // Otherwise let's still validate the config and references and return + // diagnostics if references do not exist etc. + var diags tfdiags.Diagnostics + for _, assert := range n.config.Asserts { + _, _, moreDiags := validateCheckRule(addrs.CheckAssertion, assert, ctx, n.addr, EvalDataForNoInstanceKey) + diags = diags.Append(moreDiags) + } + return diags +} + +func (n *nodeCheckAssert) Name() string { + return n.addr.String() + " (assertions)" +} diff --git a/internal/terraform/node_resource_abstract_instance.go b/internal/terraform/node_resource_abstract_instance.go index 709298ee519a..0d458fd29cb6 100644 --- a/internal/terraform/node_resource_abstract_instance.go +++ b/internal/terraform/node_resource_abstract_instance.go @@ -9,6 +9,7 @@ import ( "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/checks" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/instances" @@ -1587,7 +1588,31 @@ func (n *NodeAbstractResourceInstance) planDataSource(ctx EvalContext, checkRule return nil, nil, keyData, diags } - unmarkedConfigVal, configMarkPaths := configVal.UnmarkDeepWithPaths() + check, nested := n.nestedInCheckBlock() + if nested { + // Going forward from this point, the only reason we will fail is + // that the data source fails to load its data. Normally, this would + // cancel the entire plan and this error message would bubble its way + // back up to the user. + // + // But, if we are in a check block then we don't want this data block to + // cause the plan to fail. We also need to report a status on the data + // block so the check processing later on knows whether to attempt to + // process the checks. Either we'll report the data block as failed + // if/when we load the data block later, or we want to report it as a + // success overall. + // + // Therefore, we create a deferred function here that will check if the + // status for the check has been updated yet, and if not we will set it + // to be StatusPass. The rest of this function will only update the + // status if it should be StatusFail. + defer func() { + status := ctx.Checks().ObjectCheckStatus(check.Addr().Absolute(n.Addr.Module)) + if status == checks.StatusUnknown { + ctx.Checks().ReportCheckResult(check.Addr().Absolute(n.Addr.Module), addrs.CheckDataResource, 0, checks.StatusPass) + } + }() + } configKnown := configVal.IsWhollyKnown() depsPending := n.dependenciesHavePendingChanges(ctx) @@ -1620,6 +1645,7 @@ func (n *NodeAbstractResourceInstance) planDataSource(ctx EvalContext, checkRule reason = plans.ResourceInstanceReadBecauseDependencyPending } + unmarkedConfigVal, configMarkPaths := configVal.UnmarkDeepWithPaths() proposedNewVal := objchange.PlannedDataResourceObject(schema, unmarkedConfigVal) proposedNewVal = proposedNewVal.MarkWithPaths(configMarkPaths) @@ -1653,16 +1679,83 @@ func (n *NodeAbstractResourceInstance) planDataSource(ctx EvalContext, checkRule // can read the data source into the state. newVal, readDiags := n.readDataSource(ctx, configVal) diags = diags.Append(readDiags) - if diags.HasErrors() { - return nil, nil, keyData, diags + + // Now we've loaded the data, and diags tells us whether we were successful + // or not, we are going to create our plannedChange and our + // proposedNewState. + var plannedChange *plans.ResourceInstanceChange + var plannedNewState *states.ResourceInstanceObject + + // If we are a nested block, then we want to create a plannedChange that + // tells Terraform to reload the data block during the apply stage even if + // we managed to get the data now. + // Another consideration is that if we failed to load the data, we need to + // disguise that for a nested block. Nested blocks will report the overall + // check as failed but won't affect the rest of the plan operation or block + // an apply operation. + + if nested { + // Let's fix things up for a nested data block. + // + // A nested data block doesn't error, and creates a planned change. So, + // if we encountered an error we'll tidy up newVal so it makes sense + // and handle the error. We'll also create the plannedChange if + // appropriate. + + if diags.HasErrors() { + // If we had errors, then we can cover that up by marking the new + // state as unknown. + unmarkedConfigVal, configMarkPaths := configVal.UnmarkDeepWithPaths() + newVal = objchange.PlannedDataResourceObject(schema, unmarkedConfigVal) + newVal = newVal.MarkWithPaths(configMarkPaths) + + // We still want to report the check as failed even if we are still + // letting it run again during the apply stage. + ctx.Checks().ReportCheckFailure(check.Addr().Absolute(n.Addr.Module), addrs.CheckDataResource, 0, diags.Err().Error()) + + // Also, let's hide the errors so that execution can continue as + // normal. + diags = tfdiags.WithErrorsAsWarnings(diags) + } + + if !skipPlanChanges { + // refreshOnly plans cannot produce planned changes, so we only do + // this if skipPlanChanges is false. + plannedChange = &plans.ResourceInstanceChange{ + Addr: n.Addr, + PrevRunAddr: n.prevRunAddr(ctx), + ProviderAddr: n.ResolvedProvider, + Change: plans.Change{ + Action: plans.Read, + Before: priorVal, + After: newVal, + }, + ActionReason: plans.ResourceInstanceReadBecauseCheckNested, + } + } + } - plannedNewState := &states.ResourceInstanceObject{ - Value: newVal, - Status: states.ObjectReady, + if !diags.HasErrors() { + // Finally, let's make our new state. + plannedNewState = &states.ResourceInstanceObject{ + Value: newVal, + Status: states.ObjectReady, + } } - return nil, plannedNewState, keyData, diags + return plannedChange, plannedNewState, keyData, diags +} + +// nestedInCheckBlock determines if this resource is nested in a Check config +// block. If so, this resource will be loaded during both plan and apply +// operations to make sure the check is always giving the latest information. +func (n *NodeAbstractResourceInstance) nestedInCheckBlock() (*configs.Check, bool) { + if n.Config.Container != nil { + check, ok := n.Config.Container.(*configs.Check) + return check, ok + } + return nil, false } // dependenciesHavePendingChanges determines whether any managed resource the @@ -1784,7 +1877,19 @@ func (n *NodeAbstractResourceInstance) applyDataSource(ctx EvalContext, planned newVal, readDiags := n.readDataSource(ctx, configVal) diags = diags.Append(readDiags) - if diags.HasErrors() { + + if check, nested := n.nestedInCheckBlock(); nested { + // We're just going to jump in here and hide away any erros for nested + // data blocks. + if diags.HasErrors() { + ctx.Checks().ReportCheckFailure(check.Addr().Absolute(n.Addr.Module), addrs.CheckDataResource, 0, diags.Err().Error()) + return nil, keyData, tfdiags.WithErrorsAsWarnings(diags) + } + + // If no errors, just remember to report this as a success and continue + // as normal. + ctx.Checks().ReportCheckResult(check.Addr().Absolute(n.Addr.Module), addrs.CheckDataResource, 0, checks.StatusPass) + } else if diags.HasErrors() { return nil, keyData, diags } diff --git a/internal/terraform/testdata/static-validate-refs/static-validate-refs.tf b/internal/terraform/testdata/static-validate-refs/static-validate-refs.tf index 3667a4e11f35..2f71e21713d6 100644 --- a/internal/terraform/testdata/static-validate-refs/static-validate-refs.tf +++ b/internal/terraform/testdata/static-validate-refs/static-validate-refs.tf @@ -21,3 +21,12 @@ resource "boop_whatever" "nope" { data "beep" "boop" { } + +check "foo" { + data "boop_data" "boop_nested" {} + + assert { + condition = data.boop_data.boop_nested.id == null + error_message = "check failed" + } +} diff --git a/internal/terraform/transform_check.go b/internal/terraform/transform_check.go new file mode 100644 index 000000000000..5f469b8ad3b0 --- /dev/null +++ b/internal/terraform/transform_check.go @@ -0,0 +1,126 @@ +package terraform + +import ( + "log" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/dag" +) + +type checkTransformer struct { + // Config for the entire module. + Config *configs.Config + + // Operation is the current operation this node will be part of. + Operation walkOperation +} + +var _ GraphTransformer = (*checkTransformer)(nil) + +func (t *checkTransformer) Transform(graph *Graph) error { + return t.transform(graph, t.Config, graph.Vertices()) +} + +func (t *checkTransformer) transform(g *Graph, cfg *configs.Config, allNodes []dag.Vertex) error { + + if t.Operation == walkDestroy || t.Operation == walkPlanDestroy { + // Don't include anything about checks during destroy operations. + // + // For other plan and normal apply operations we do everything, for + // destroy operations we do nothing. For any other operations we still + // include the check nodes, but we don't actually execute the checks + // instead we still validate their references and make sure their + // conditions make sense etc. + return nil + } + + moduleAddr := cfg.Path + + for _, check := range cfg.Module.Checks { + configAddr := check.Addr().InModule(moduleAddr) + + // We want to create a node for each check block. This node will execute + // after anything it references, and will update the checks object + // embedded in the plan and/or state. + + log.Printf("[TRACE] checkTransformer: Nodes and edges for %s", configAddr) + expand := &nodeExpandCheck{ + addr: configAddr, + config: check, + makeInstance: func(addr addrs.AbsCheck, cfg *configs.Check) dag.Vertex { + return &nodeCheckAssert{ + addr: addr, + config: cfg, + executeChecks: t.ExecuteChecks(), + } + }, + } + g.Add(expand) + + // We also need to report the checks we are going to execute before we + // try and execute them. + if t.ReportChecks() { + report := &nodeReportCheck{ + addr: configAddr, + } + g.Add(report) + + // This part ensures we report our checks before our nested data + // block executes and attempts to report on a check. + for _, other := range allNodes { + if resource, isResource := other.(GraphNodeConfigResource); isResource { + resourceAddr := resource.ResourceAddr() + if !resourceAddr.Module.Equal(moduleAddr) { + // This resource isn't in the same module as our check + // so skip it. + continue + } + + resourceCfg := cfg.Module.ResourceByAddr(resourceAddr.Resource) + if resourceCfg != nil && resourceCfg.Container != nil && resourceCfg.Container.Accessible(check.Addr()) { + // Make sure we report our checks before we execute any + // embedded data resource. + g.Connect(dag.BasicEdge(other, report)) + continue + } + } + } + } + } + + for _, child := range cfg.Children { + if err := t.transform(g, child, allNodes); err != nil { + return err + } + } + + return nil +} + +// ReportChecks returns true if this operation should report any check blocks +// that it is about to execute. +// +// This is generally only true for planning operations, as apply operations +// recreate the expected checks from the plan. +func (t *checkTransformer) ReportChecks() bool { + return t.Operation == walkPlan +} + +// ExecuteChecks returns true if this operation should actually execute any +// check blocks in the config. +// +// If this returns false we will still create and execute check nodes in the +// graph, but they will only validate things like references and syntax. +func (t *checkTransformer) ExecuteChecks() bool { + switch t.Operation { + case walkPlan, walkApply: + // We only actually execute the checks for plan and apply operations. + return true + default: + // For everything else, we still want to validate the checks make sense + // logically and syntactically, but we won't actually resolve the check + // conditions. + return false + } +} diff --git a/internal/tfdiags/errors_as_warnings.go b/internal/tfdiags/errors_as_warnings.go new file mode 100644 index 000000000000..c208a3601431 --- /dev/null +++ b/internal/tfdiags/errors_as_warnings.go @@ -0,0 +1,41 @@ +package tfdiags + +type diagForceWarningSeverity struct { + wrapped Diagnostic +} + +func WithErrorsAsWarnings(diags Diagnostics) Diagnostics { + if len(diags) == 0 { + return nil + } + + ret := make(Diagnostics, len(diags)) + for i, diag := range diags { + if diag.Severity() == Error { + ret[i] = diagForceWarningSeverity{diag} + } else { + ret[i] = diag + } + } + return ret +} + +func (diag diagForceWarningSeverity) Severity() Severity { + return Warning +} + +func (diag diagForceWarningSeverity) Description() Description { + return diag.wrapped.Description() +} + +func (diag diagForceWarningSeverity) Source() Source { + return diag.wrapped.Source() +} + +func (diag diagForceWarningSeverity) FromExpr() *FromExpr { + return diag.wrapped.FromExpr() +} + +func (diag diagForceWarningSeverity) ExtraInfo() any { + return diag.wrapped.ExtraInfo() +}