mirror of https://github.com/hashicorp/terraform
This is a new-shaped representation of check results which follows the two-tiered structure of static objects and dynamic instances of objects, thereby allowing consumers to see which checkable objects exist in the configuration even if a dynamic evaluation error prevented actually expanding them all to determine their declared instances. Eventually we'll include this in the state too, but this initially adds it only to the plan in order to replace the now-deprecated experimental conditions result that was present but undocumented in Terraform v1.2.pull/31706/head
parent
d63871f70d
commit
fe7e6f970e
@ -0,0 +1,116 @@
|
||||
package jsonchecks
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/states"
|
||||
)
|
||||
|
||||
// MarshalCheckStates is the main entry-point for this package, which takes
|
||||
// the top-level model object for checks in state and plan, and returns a
|
||||
// JSON representation of it suitable for use in public integration points.
|
||||
func MarshalCheckStates(results *states.CheckResults) []byte {
|
||||
jsonResults := make([]checkResultStatic, 0, results.ConfigResults.Len())
|
||||
|
||||
for _, elem := range results.ConfigResults.Elems {
|
||||
staticAddr := elem.Key
|
||||
aggrResult := elem.Value
|
||||
|
||||
objects := make([]checkResultDynamic, 0, aggrResult.ObjectResults.Len())
|
||||
for _, elem := range aggrResult.ObjectResults.Elems {
|
||||
dynamicAddr := elem.Key
|
||||
result := elem.Value
|
||||
|
||||
problems := make([]checkProblem, 0, len(result.FailureMessages))
|
||||
for _, msg := range result.FailureMessages {
|
||||
problems = append(problems, checkProblem{
|
||||
Message: msg,
|
||||
})
|
||||
}
|
||||
sort.Slice(problems, func(i, j int) bool {
|
||||
return problems[i].Message < problems[j].Message
|
||||
})
|
||||
|
||||
objects = append(objects, checkResultDynamic{
|
||||
Address: makeDynamicObjectAddr(dynamicAddr),
|
||||
Status: checkStatusForJSON(result.Status),
|
||||
Problems: problems,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(objects, func(i, j int) bool {
|
||||
return objects[i].Address["to_display"].(string) < objects[j].Address["to_display"].(string)
|
||||
})
|
||||
|
||||
jsonResults = append(jsonResults, checkResultStatic{
|
||||
Address: makeStaticObjectAddr(staticAddr),
|
||||
Status: checkStatusForJSON(aggrResult.Status),
|
||||
Instances: objects,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(jsonResults, func(i, j int) bool {
|
||||
return jsonResults[i].Address["to_display"].(string) < jsonResults[j].Address["to_display"].(string)
|
||||
})
|
||||
|
||||
ret, err := json.Marshal(jsonResults)
|
||||
if err != nil {
|
||||
// We totally control the input to json.Marshal, so any error here
|
||||
// is a bug in the code above.
|
||||
panic(fmt.Sprintf("invalid input to json.Marshal: %s", err))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// checkResultStatic is the container for the static, configuration-driven
|
||||
// idea of "checkable object" -- a resource block with conditions, for example --
|
||||
// which ensures that we can always say _something_ about each checkable
|
||||
// object in the configuration even if Terraform Core encountered an error
|
||||
// before being able to determine the dynamic instances of the checkable object.
|
||||
type checkResultStatic struct {
|
||||
// Address is the address of the checkable object this result relates to.
|
||||
Address staticObjectAddr `json:"address"`
|
||||
|
||||
// Status is the aggregate status for all of the dynamic objects belonging
|
||||
// to this static object.
|
||||
Status checkStatus `json:"status"`
|
||||
|
||||
// Instances contains the results for each individual dynamic object that
|
||||
// belongs to this static object.
|
||||
Instances []checkResultDynamic `json:"instances,omitempty"`
|
||||
}
|
||||
|
||||
// checkResultDynamic describes the check result for a dynamic object, which
|
||||
// results from Terraform Core evaluating the "expansion" (e.g. count or for_each)
|
||||
// of the containing object or its own containing module(s).
|
||||
type checkResultDynamic struct {
|
||||
// Address augments the Address of the containing checkResultStatic with
|
||||
// instance-specific extra properties or overridden properties.
|
||||
Address dynamicObjectAddr `json:"address"`
|
||||
|
||||
// Status is the status for this specific dynamic object.
|
||||
Status checkStatus `json:"status"`
|
||||
|
||||
// Problems describes some optional details associated with a failure
|
||||
// status, describing what fails.
|
||||
//
|
||||
// This does not include the errors for status "error", because Terraform
|
||||
// Core emits those separately as normal diagnostics. However, if a
|
||||
// particular object has a mixture of conditions that failed and conditions
|
||||
// that were invalid then status can be "error" while simultaneously
|
||||
// returning problems in this property.
|
||||
Problems []checkProblem `json:"problems,omitempty"`
|
||||
}
|
||||
|
||||
// checkProblem describes one of potentially several problems that led to
|
||||
// a check being classified as status "fail".
|
||||
type checkProblem struct {
|
||||
// Message is the condition error message provided by the author.
|
||||
Message string `json:"message"`
|
||||
|
||||
// We don't currently have any other problem-related data, but this is
|
||||
// intentionally an object to allow us to add other data over time, such
|
||||
// as the source location where the failing condition was defined.
|
||||
}
|
||||
@ -0,0 +1,206 @@
|
||||
package jsonchecks
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/internal/checks"
|
||||
"github.com/hashicorp/terraform/internal/states"
|
||||
)
|
||||
|
||||
func TestMarshalCheckStates(t *testing.T) {
|
||||
resourceAAddr := addrs.ConfigCheckable(addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test",
|
||||
Name: "a",
|
||||
}.InModule(addrs.RootModule))
|
||||
resourceAInstAddr := addrs.Checkable(addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test",
|
||||
Name: "a",
|
||||
}.Instance(addrs.StringKey("foo")).Absolute(addrs.RootModuleInstance))
|
||||
moduleChildAddr := addrs.RootModuleInstance.Child("child", addrs.IntKey(0))
|
||||
resourceBAddr := addrs.ConfigCheckable(addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test",
|
||||
Name: "b",
|
||||
}.InModule(moduleChildAddr.Module()))
|
||||
resourceBInstAddr := addrs.Checkable(addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test",
|
||||
Name: "b",
|
||||
}.Instance(addrs.NoKey).Absolute(moduleChildAddr))
|
||||
outputAAddr := addrs.ConfigCheckable(addrs.OutputValue{Name: "a"}.InModule(addrs.RootModule))
|
||||
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))
|
||||
|
||||
tests := map[string]struct {
|
||||
Input *states.CheckResults
|
||||
Want any
|
||||
}{
|
||||
"empty": {
|
||||
&states.CheckResults{},
|
||||
[]any{},
|
||||
},
|
||||
"failures": {
|
||||
&states.CheckResults{
|
||||
ConfigResults: addrs.MakeMap(
|
||||
addrs.MakeMapElem(resourceAAddr, &states.CheckResultAggregate{
|
||||
Status: checks.StatusFail,
|
||||
ObjectResults: addrs.MakeMap(
|
||||
addrs.MakeMapElem(resourceAInstAddr, &states.CheckResultObject{
|
||||
Status: checks.StatusFail,
|
||||
FailureMessages: []string{
|
||||
"Not enough boops.",
|
||||
"Too many beeps.",
|
||||
},
|
||||
}),
|
||||
),
|
||||
}),
|
||||
addrs.MakeMapElem(resourceBAddr, &states.CheckResultAggregate{
|
||||
Status: checks.StatusFail,
|
||||
ObjectResults: addrs.MakeMap(
|
||||
addrs.MakeMapElem(resourceBInstAddr, &states.CheckResultObject{
|
||||
Status: checks.StatusFail,
|
||||
FailureMessages: []string{
|
||||
"Splines are too pointy.",
|
||||
},
|
||||
}),
|
||||
),
|
||||
}),
|
||||
addrs.MakeMapElem(outputAAddr, &states.CheckResultAggregate{
|
||||
Status: checks.StatusFail,
|
||||
ObjectResults: addrs.MakeMap(
|
||||
addrs.MakeMapElem(outputAInstAddr, &states.CheckResultObject{
|
||||
Status: checks.StatusFail,
|
||||
}),
|
||||
),
|
||||
}),
|
||||
addrs.MakeMapElem(outputBAddr, &states.CheckResultAggregate{
|
||||
Status: checks.StatusFail,
|
||||
ObjectResults: addrs.MakeMap(
|
||||
addrs.MakeMapElem(outputBInstAddr, &states.CheckResultObject{
|
||||
Status: checks.StatusFail,
|
||||
FailureMessages: []string{
|
||||
"Not object-oriented enough.",
|
||||
},
|
||||
}),
|
||||
),
|
||||
}),
|
||||
),
|
||||
},
|
||||
[]any{
|
||||
map[string]any{
|
||||
"address": map[string]any{
|
||||
"kind": "output_value",
|
||||
"module": "module.child",
|
||||
"name": "b",
|
||||
"to_display": "module.child.output.b",
|
||||
},
|
||||
"instances": []any{
|
||||
map[string]any{
|
||||
"address": map[string]any{
|
||||
"module": "module.child[0]",
|
||||
"to_display": "module.child[0].output.b",
|
||||
},
|
||||
"problems": []any{
|
||||
map[string]any{
|
||||
"message": "Not object-oriented enough.",
|
||||
},
|
||||
},
|
||||
"status": "fail",
|
||||
},
|
||||
},
|
||||
"status": "fail",
|
||||
},
|
||||
map[string]any{
|
||||
"address": map[string]any{
|
||||
"kind": "resource",
|
||||
"mode": "managed",
|
||||
"module": "module.child",
|
||||
"name": "b",
|
||||
"to_display": "module.child.test.b",
|
||||
"type": "test",
|
||||
},
|
||||
"instances": []any{
|
||||
map[string]any{
|
||||
"address": map[string]any{
|
||||
"module": "module.child[0]",
|
||||
"to_display": "module.child[0].test.b",
|
||||
},
|
||||
"problems": []any{
|
||||
map[string]any{
|
||||
"message": "Splines are too pointy.",
|
||||
},
|
||||
},
|
||||
"status": "fail",
|
||||
},
|
||||
},
|
||||
"status": "fail",
|
||||
},
|
||||
map[string]any{
|
||||
"address": map[string]any{
|
||||
"kind": "output_value",
|
||||
"name": "a",
|
||||
"to_display": "output.a",
|
||||
},
|
||||
"instances": []any{
|
||||
map[string]any{
|
||||
"address": map[string]any{
|
||||
"to_display": "output.a",
|
||||
},
|
||||
"status": "fail",
|
||||
},
|
||||
},
|
||||
"status": "fail",
|
||||
},
|
||||
map[string]any{
|
||||
"address": map[string]any{
|
||||
"kind": "resource",
|
||||
"mode": "managed",
|
||||
"name": "a",
|
||||
"to_display": "test.a",
|
||||
"type": "test",
|
||||
},
|
||||
"instances": []any{
|
||||
map[string]any{
|
||||
"address": map[string]any{
|
||||
"to_display": `test.a["foo"]`,
|
||||
"instance_key": "foo",
|
||||
},
|
||||
"problems": []any{
|
||||
map[string]any{
|
||||
"message": "Not enough boops.",
|
||||
},
|
||||
map[string]any{
|
||||
"message": "Too many beeps.",
|
||||
},
|
||||
},
|
||||
"status": "fail",
|
||||
},
|
||||
},
|
||||
"status": "fail",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
gotBytes := MarshalCheckStates(test.Input)
|
||||
|
||||
var got any
|
||||
err := json.Unmarshal(gotBytes, &got)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(test.Want, got); diff != "" {
|
||||
t.Errorf("wrong result\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
// Package jsonchecks implements the common JSON representation of check
|
||||
// results/statuses that we use across both the JSON plan and JSON state
|
||||
// representations.
|
||||
package jsonchecks
|
||||
@ -0,0 +1,69 @@
|
||||
package jsonchecks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
)
|
||||
|
||||
type staticObjectAddr map[string]interface{}
|
||||
|
||||
func makeStaticObjectAddr(addr addrs.ConfigCheckable) staticObjectAddr {
|
||||
ret := map[string]interface{}{
|
||||
"to_display": addr.String(),
|
||||
}
|
||||
|
||||
switch addr := addr.(type) {
|
||||
case addrs.ConfigResource:
|
||||
ret["kind"] = "resource"
|
||||
switch addr.Resource.Mode {
|
||||
case addrs.ManagedResourceMode:
|
||||
ret["mode"] = "managed"
|
||||
case addrs.DataResourceMode:
|
||||
ret["mode"] = "data"
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported resource mode %#v", addr.Resource.Mode))
|
||||
}
|
||||
ret["type"] = addr.Resource.Type
|
||||
ret["name"] = addr.Resource.Name
|
||||
if !addr.Module.IsRoot() {
|
||||
ret["module"] = addr.Module.String()
|
||||
}
|
||||
case addrs.ConfigOutputValue:
|
||||
ret["kind"] = "output_value"
|
||||
ret["name"] = addr.OutputValue.Name
|
||||
if !addr.Module.IsRoot() {
|
||||
ret["module"] = addr.Module.String()
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported ConfigCheckable implementation %T", addr))
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
type dynamicObjectAddr map[string]interface{}
|
||||
|
||||
func makeDynamicObjectAddr(addr addrs.Checkable) dynamicObjectAddr {
|
||||
ret := map[string]interface{}{
|
||||
"to_display": addr.String(),
|
||||
}
|
||||
|
||||
switch addr := addr.(type) {
|
||||
case addrs.AbsResourceInstance:
|
||||
if !addr.Module.IsRoot() {
|
||||
ret["module"] = addr.Module.String()
|
||||
}
|
||||
if addr.Resource.Key != addrs.NoKey {
|
||||
ret["instance_key"] = addr.Resource.Key
|
||||
}
|
||||
case addrs.AbsOutputValue:
|
||||
if !addr.Module.IsRoot() {
|
||||
ret["module"] = addr.Module.String()
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported Checkable implementation %T", addr))
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package jsonchecks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/checks"
|
||||
)
|
||||
|
||||
type checkStatus []byte
|
||||
|
||||
func checkStatusForJSON(s checks.Status) checkStatus {
|
||||
if ret, ok := checkStatuses[s]; ok {
|
||||
return ret
|
||||
}
|
||||
panic(fmt.Sprintf("unsupported check status %#v", s))
|
||||
}
|
||||
|
||||
func (s checkStatus) MarshalJSON() ([]byte, error) {
|
||||
return []byte(s), nil
|
||||
}
|
||||
|
||||
var checkStatuses = map[checks.Status]checkStatus{
|
||||
checks.StatusPass: checkStatus(`"pass"`),
|
||||
checks.StatusFail: checkStatus(`"fail"`),
|
||||
checks.StatusError: checkStatus(`"error"`),
|
||||
checks.StatusUnknown: checkStatus(`"unknown"`),
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
package jsonplan
|
||||
Loading…
Reference in new issue