Checks: Introduce check blocks into the terraform node and transform graph (#32735)

* Add support for scoped resources

* refactor existing checks addrs and add check block addr

* Add configuration for check blocks

* introduce check blocks into the terraform node and transform  graph

* address comments

* address comments

* don't execute checks during destroy operations

* don't even include check nodes for destroy operations
This commit is contained in:
Liam Cervante 2023-03-23 16:07:31 +01:00 committed by GitHub
parent 3827120c25
commit 978263efe9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1463 additions and 113 deletions

View File

@ -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)

View File

@ -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 {

View File

@ -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"
}
}

View File

@ -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 {

View File

@ -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{

View File

@ -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))
}

View File

@ -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 {

View File

@ -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)
}

View File

@ -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

View File

@ -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.

View File

@ -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 (

View File

@ -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 {

View File

@ -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:

View File

@ -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{

View File

@ -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) + ")"
}

View File

@ -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))
}

View File

@ -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)
}
}
}
}

View File

@ -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
}

View File

@ -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())

View File

@ -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},

View File

@ -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,

View File

@ -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)"
}

View File

@ -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
}

View File

@ -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"
}
}

View File

@ -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
}
}

View File

@ -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()
}