mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
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:
parent
3827120c25
commit
978263efe9
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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{
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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 (
|
||||
|
@ -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 {
|
||||
|
@ -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:
|
||||
|
@ -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{
|
||||
|
@ -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) + ")"
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
689
internal/terraform/context_apply_checks_test.go
Normal file
689
internal/terraform/context_apply_checks_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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())
|
||||
|
@ -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},
|
||||
|
||||
|
@ -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,
|
||||
|
164
internal/terraform/node_check.go
Normal file
164
internal/terraform/node_check.go
Normal 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)"
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
126
internal/terraform/transform_check.go
Normal file
126
internal/terraform/transform_check.go
Normal 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
|
||||
}
|
||||
}
|
41
internal/tfdiags/errors_as_warnings.go
Normal file
41
internal/tfdiags/errors_as_warnings.go
Normal 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()
|
||||
}
|
Loading…
Reference in New Issue
Block a user