Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 12 commits into from
Mar 23, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 16 additions & 0 deletions internal/checks/state_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
18 changes: 17 additions & 1 deletion internal/checks/state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
}

/////////////////////////////////////////////////////////////////////////

Expand Down Expand Up @@ -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")
}
Expand All @@ -124,6 +134,7 @@ func TestChecksHappyPath(t *testing.T) {
resourceC,
rootOutput,
childOutput,
checkBlock,
)
gotConfigAddrs := checks.AllConfigAddrs()
if diff := cmp.Diff(wantConfigAddrs, gotConfigAddrs); diff != "" {
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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)
}
}
Expand All @@ -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 {
Expand Down
7 changes: 7 additions & 0 deletions internal/checks/testdata/happypath/checks-happypath.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
2 changes: 2 additions & 0 deletions internal/command/format/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
35 changes: 35 additions & 0 deletions internal/command/jsonchecks/checks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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{
Expand Down
15 changes: 15 additions & 0 deletions internal/command/jsonchecks/objects.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand All @@ -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))
}
Expand Down
2 changes: 2 additions & 0 deletions internal/command/jsonformat/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions internal/command/jsonplan/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down
3 changes: 3 additions & 0 deletions internal/command/views/json/change.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions internal/plans/changes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down