mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-25 16:31:10 -06:00
states/statefile: Serialize check results into state snapshots
This allows us to retain check results from one run into the next, so that we can react to status changes between runs and potentially report e.g. that a previously-failing check has now been fixed, or that a previously-failing check is "still failing" so that an operator can get a hint as to whether a problem is something they've just introduced or if it was already an active problem before they made a change.
This commit is contained in:
parent
9e4861adbb
commit
6de3b1bd16
@ -104,6 +104,7 @@ func TestStatePersist(t *testing.T) {
|
||||
"terraform_version": version.Version,
|
||||
"outputs": map[string]interface{}{},
|
||||
"resources": []interface{}{},
|
||||
"check_results": nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -125,6 +126,7 @@ func TestStatePersist(t *testing.T) {
|
||||
"terraform_version": version.Version,
|
||||
"outputs": map[string]interface{}{},
|
||||
"resources": []interface{}{},
|
||||
"check_results": nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -149,6 +151,7 @@ func TestStatePersist(t *testing.T) {
|
||||
},
|
||||
},
|
||||
"resources": []interface{}{},
|
||||
"check_results": nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -173,6 +176,7 @@ func TestStatePersist(t *testing.T) {
|
||||
},
|
||||
},
|
||||
"resources": []interface{}{},
|
||||
"check_results": nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -204,6 +208,7 @@ func TestStatePersist(t *testing.T) {
|
||||
},
|
||||
},
|
||||
"resources": []interface{}{},
|
||||
"check_results": nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -379,6 +384,7 @@ func TestWriteStateForMigration(t *testing.T) {
|
||||
"terraform_version": version.Version,
|
||||
"outputs": map[string]interface{}{"foo": map[string]interface{}{"type": string("string"), "value": string("bar")}},
|
||||
"resources": []interface{}{},
|
||||
"check_results": nil,
|
||||
},
|
||||
},
|
||||
force: true,
|
||||
@ -397,6 +403,7 @@ func TestWriteStateForMigration(t *testing.T) {
|
||||
"terraform_version": version.Version,
|
||||
"outputs": map[string]interface{}{"foo": map[string]interface{}{"type": string("string"), "value": string("bar")}},
|
||||
"resources": []interface{}{},
|
||||
"check_results": nil,
|
||||
},
|
||||
},
|
||||
force: true,
|
||||
@ -533,6 +540,7 @@ func TestWriteStateForMigrationWithForcePushClient(t *testing.T) {
|
||||
"terraform_version": version.Version,
|
||||
"outputs": map[string]interface{}{"foo": map[string]interface{}{"type": string("string"), "value": string("bar")}},
|
||||
"resources": []interface{}{},
|
||||
"check_results": nil,
|
||||
},
|
||||
},
|
||||
force: true,
|
||||
@ -551,6 +559,7 @@ func TestWriteStateForMigrationWithForcePushClient(t *testing.T) {
|
||||
"terraform_version": version.Version,
|
||||
"outputs": map[string]interface{}{"foo": map[string]interface{}{"type": string("string"), "value": string("bar")}},
|
||||
"resources": []interface{}{},
|
||||
"check_results": nil,
|
||||
},
|
||||
},
|
||||
force: true,
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
ctyjson "github.com/zclconf/go-cty/cty/json"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/internal/checks"
|
||||
"github.com/hashicorp/terraform/internal/lang/marks"
|
||||
"github.com/hashicorp/terraform/internal/states"
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
@ -292,6 +293,17 @@ func prepareStateV4(sV4 *stateV4) (*File, tfdiags.Diagnostics) {
|
||||
}
|
||||
}
|
||||
|
||||
// Saved check results from the previous run, if any.
|
||||
// We differentiate absense from an empty array here so that we can
|
||||
// recognize if the previous run was with a version of Terraform that
|
||||
// didn't support checks yet, or if there just weren't any checkable
|
||||
// objects to record, in case that's important for certain messaging.
|
||||
if sV4.CheckResults != nil {
|
||||
var moreDiags tfdiags.Diagnostics
|
||||
state.CheckResults, moreDiags = decodeCheckResultsV4(sV4.CheckResults)
|
||||
diags = diags.Append(moreDiags)
|
||||
}
|
||||
|
||||
file.State = state
|
||||
return file, diags
|
||||
}
|
||||
@ -402,6 +414,10 @@ func writeStateV4(file *File, w io.Writer) tfdiags.Diagnostics {
|
||||
}
|
||||
}
|
||||
|
||||
if file.State.CheckResults != nil {
|
||||
sV4.CheckResults = encodeCheckResultsV4(file.State.CheckResults)
|
||||
}
|
||||
|
||||
sV4.normalize()
|
||||
|
||||
src, err := json.MarshalIndent(sV4, "", " ")
|
||||
@ -496,6 +512,116 @@ func appendInstanceObjectStateV4(rs *states.Resource, is *states.ResourceInstanc
|
||||
}), diags
|
||||
}
|
||||
|
||||
func decodeCheckResultsV4(in []checkResultsV4) (*states.CheckResults, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
ret := &states.CheckResults{}
|
||||
if len(in) == 0 {
|
||||
return ret, diags
|
||||
}
|
||||
|
||||
ret.ConfigResults = addrs.MakeMap[addrs.ConfigCheckable, *states.CheckResultAggregate]()
|
||||
for _, aggrIn := range in {
|
||||
// Some trickiness here: we only have an address parser for
|
||||
// addrs.Checkable and not for addrs.ConfigCheckable, but that's okay
|
||||
// because once we have an addrs.Checkable we can always derive an
|
||||
// addrs.ConfigCheckable from it, and a ConfigCheckable should always
|
||||
// be the same syntax as a Checkable with no index information and
|
||||
// thus we can reuse the same parser for both here.
|
||||
configAddrProxy, moreDiags := addrs.ParseCheckableStr(aggrIn.ConfigAddr)
|
||||
diags = diags.Append(moreDiags)
|
||||
if moreDiags.HasErrors() {
|
||||
continue
|
||||
}
|
||||
configAddr := configAddrProxy.ConfigCheckable()
|
||||
if configAddr.String() != configAddrProxy.String() {
|
||||
// This is how we catch if the config address included index
|
||||
// information that would be allowed in a Checkable but not
|
||||
// in a ConfigCheckable.
|
||||
diags = diags.Append(fmt.Errorf("invalid checkable config address %s", aggrIn.ConfigAddr))
|
||||
continue
|
||||
}
|
||||
|
||||
aggr := &states.CheckResultAggregate{
|
||||
Status: decodeCheckStatusV4(aggrIn.Status),
|
||||
}
|
||||
|
||||
if len(aggrIn.Objects) != 0 {
|
||||
aggr.ObjectResults = addrs.MakeMap[addrs.Checkable, *states.CheckResultObject]()
|
||||
for _, objectIn := range aggrIn.Objects {
|
||||
objectAddr, moreDiags := addrs.ParseCheckableStr(objectIn.ObjectAddr)
|
||||
diags = diags.Append(moreDiags)
|
||||
if moreDiags.HasErrors() {
|
||||
continue
|
||||
}
|
||||
|
||||
obj := &states.CheckResultObject{
|
||||
Status: decodeCheckStatusV4(objectIn.Status),
|
||||
FailureMessages: objectIn.FailureMessages,
|
||||
}
|
||||
aggr.ObjectResults.Put(objectAddr, obj)
|
||||
}
|
||||
}
|
||||
|
||||
ret.ConfigResults.Put(configAddr, aggr)
|
||||
}
|
||||
|
||||
return ret, diags
|
||||
}
|
||||
|
||||
func encodeCheckResultsV4(in *states.CheckResults) []checkResultsV4 {
|
||||
ret := make([]checkResultsV4, 0, in.ConfigResults.Len())
|
||||
|
||||
for _, configElem := range in.ConfigResults.Elems {
|
||||
configResultsOut := checkResultsV4{
|
||||
ConfigAddr: configElem.Key.String(),
|
||||
Status: encodeCheckStatusV4(configElem.Value.Status),
|
||||
}
|
||||
for _, objectElem := range configElem.Value.ObjectResults.Elems {
|
||||
configResultsOut.Objects = append(configResultsOut.Objects, checkResultsObjectV4{
|
||||
ObjectAddr: objectElem.Key.String(),
|
||||
Status: encodeCheckStatusV4(objectElem.Value.Status),
|
||||
FailureMessages: objectElem.Value.FailureMessages,
|
||||
})
|
||||
}
|
||||
|
||||
ret = append(ret, configResultsOut)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func decodeCheckStatusV4(in string) checks.Status {
|
||||
switch in {
|
||||
case "pass":
|
||||
return checks.StatusPass
|
||||
case "fail":
|
||||
return checks.StatusFail
|
||||
case "error":
|
||||
return checks.StatusError
|
||||
default:
|
||||
// We'll treat anything else as unknown just as a concession to
|
||||
// forward-compatible parsing, in case a later version of Terraform
|
||||
// introduces a new status.
|
||||
return checks.StatusUnknown
|
||||
}
|
||||
}
|
||||
|
||||
func encodeCheckStatusV4(in checks.Status) string {
|
||||
switch in {
|
||||
case checks.StatusPass:
|
||||
return "pass"
|
||||
case checks.StatusFail:
|
||||
return "fail"
|
||||
case checks.StatusError:
|
||||
return "error"
|
||||
case checks.StatusUnknown:
|
||||
return "unknown"
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported check status %s", in))
|
||||
}
|
||||
}
|
||||
|
||||
type stateV4 struct {
|
||||
Version stateVersionV4 `json:"version"`
|
||||
TerraformVersion string `json:"terraform_version"`
|
||||
@ -503,6 +629,7 @@ type stateV4 struct {
|
||||
Lineage string `json:"lineage"`
|
||||
RootOutputs map[string]outputStateV4 `json:"outputs"`
|
||||
Resources []resourceStateV4 `json:"resources"`
|
||||
CheckResults []checkResultsV4 `json:"check_results"`
|
||||
}
|
||||
|
||||
// normalize makes some in-place changes to normalize the way items are
|
||||
@ -548,6 +675,18 @@ type instanceObjectStateV4 struct {
|
||||
CreateBeforeDestroy bool `json:"create_before_destroy,omitempty"`
|
||||
}
|
||||
|
||||
type checkResultsV4 struct {
|
||||
ConfigAddr string `json:"config_addr"`
|
||||
Status string `json:"status"`
|
||||
Objects []checkResultsObjectV4 `json:"objects"`
|
||||
}
|
||||
|
||||
type checkResultsObjectV4 struct {
|
||||
ObjectAddr string `json:"object_addr"`
|
||||
Status string `json:"status"`
|
||||
FailureMessages []string `json:"failure_messages,omitempty"`
|
||||
}
|
||||
|
||||
// stateVersionV4 is a weird special type we use to produce our hard-coded
|
||||
// "version": 4 in the JSON serialization.
|
||||
type stateVersionV4 struct{}
|
||||
|
Loading…
Reference in New Issue
Block a user