Merge pull request #30945 from hashicorp/alisdair/jsonstate-output-type

json-output: Add output type to JSON format
This commit is contained in:
Alisdair McDiarmid 2022-04-27 13:36:43 -04:00 committed by GitHub
commit b02867bed3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 78 additions and 7 deletions

View File

@ -106,6 +106,7 @@ type change struct {
type output struct {
Sensitive bool `json:"sensitive"`
Type json.RawMessage `json:"type,omitempty"`
Value json.RawMessage `json:"value,omitempty"`
}

View File

@ -57,7 +57,7 @@ func marshalPlannedOutputs(changes *plans.Changes) (map[string]output, error) {
continue
}
var after []byte
var after, afterType []byte
changeV, err := oc.Decode()
if err != nil {
return ret, err
@ -68,7 +68,12 @@ func marshalPlannedOutputs(changes *plans.Changes) (map[string]output, error) {
changeV.After, _ = changeV.After.UnmarkDeep()
if changeV.After != cty.NilVal && changeV.After.IsWhollyKnown() {
after, err = ctyjson.Marshal(changeV.After, changeV.After.Type())
ty := changeV.After.Type()
after, err = ctyjson.Marshal(changeV.After, ty)
if err != nil {
return ret, err
}
afterType, err = ctyjson.MarshalType(ty)
if err != nil {
return ret, err
}
@ -76,6 +81,7 @@ func marshalPlannedOutputs(changes *plans.Changes) (map[string]output, error) {
ret[oc.Addr.OutputValue.Name] = output{
Value: json.RawMessage(after),
Type: json.RawMessage(afterType),
Sensitive: oc.Sensitive,
}
}

View File

@ -137,6 +137,7 @@ func TestMarshalPlannedOutputs(t *testing.T) {
map[string]output{
"bar": {
Sensitive: false,
Type: json.RawMessage(`"string"`),
Value: json.RawMessage(`"after"`),
},
},

View File

@ -38,6 +38,7 @@ type stateValues struct {
type output struct {
Sensitive bool `json:"sensitive"`
Value json.RawMessage `json:"value,omitempty"`
Type json.RawMessage `json:"type,omitempty"`
}
// module is the representation of a module in state. This can be the root module
@ -180,12 +181,18 @@ func marshalOutputs(outputs map[string]*states.OutputValue) (map[string]output,
ret := make(map[string]output)
for k, v := range outputs {
ov, err := ctyjson.Marshal(v.Value, v.Value.Type())
ty := v.Value.Type()
ov, err := ctyjson.Marshal(v.Value, ty)
if err != nil {
return ret, err
}
ot, err := ctyjson.MarshalType(ty)
if err != nil {
return ret, err
}
ret[k] = output{
Value: ov,
Type: ot,
Sensitive: v.Sensitive,
}
}

View File

@ -36,6 +36,7 @@ func TestMarshalOutputs(t *testing.T) {
"test": {
Sensitive: true,
Value: json.RawMessage(`"sekret"`),
Type: json.RawMessage(`"string"`),
},
},
false,
@ -51,6 +52,39 @@ func TestMarshalOutputs(t *testing.T) {
"test": {
Sensitive: false,
Value: json.RawMessage(`"not_so_sekret"`),
Type: json.RawMessage(`"string"`),
},
},
false,
},
{
map[string]*states.OutputValue{
"mapstring": {
Sensitive: false,
Value: cty.MapVal(map[string]cty.Value{
"beep": cty.StringVal("boop"),
}),
},
"setnumber": {
Sensitive: false,
Value: cty.SetVal([]cty.Value{
cty.NumberIntVal(3),
cty.NumberIntVal(5),
cty.NumberIntVal(7),
cty.NumberIntVal(11),
}),
},
},
map[string]output{
"mapstring": {
Sensitive: false,
Value: json.RawMessage(`{"beep":"boop"}`),
Type: json.RawMessage(`["map","string"]`),
},
"setnumber": {
Sensitive: false,
Value: json.RawMessage(`[3,5,7,11]`),
Type: json.RawMessage(`["set","number"]`),
},
},
false,
@ -67,10 +101,8 @@ func TestMarshalOutputs(t *testing.T) {
} else if err != nil {
t.Fatalf("unexpected error: %s", err)
}
eq := reflect.DeepEqual(got, test.Want)
if !eq {
// printing the output isn't terribly useful, but it does help indicate which test case failed
t.Fatalf("wrong result:\nGot: %#v\nWant: %#v\n", got, test.Want)
if !cmp.Equal(test.Want, got) {
t.Fatalf("wrong result:\n%s", cmp.Diff(test.Want, got))
}
}
}

View File

@ -9,6 +9,7 @@
"outputs": {
"test": {
"sensitive": true,
"type": "string",
"value": "bar"
}
},
@ -71,6 +72,7 @@
"outputs": {
"test": {
"sensitive": true,
"type": "string",
"value": "bar"
}
},

View File

@ -5,6 +5,7 @@
"outputs": {
"test": {
"sensitive": false,
"type": "string",
"value": "baz"
}
},

View File

@ -9,6 +9,7 @@
"outputs": {
"test": {
"sensitive": false,
"type": "string",
"value": "bar"
}
},
@ -62,6 +63,7 @@
"outputs": {
"test": {
"sensitive": false,
"type": "string",
"value": "bar"
}
},

View File

@ -9,6 +9,7 @@
"outputs": {
"test": {
"sensitive": false,
"type": "string",
"value": "bar"
}
},
@ -94,6 +95,7 @@
"outputs": {
"test": {
"sensitive": false,
"type": "string",
"value": "bar"
}
},

View File

@ -9,6 +9,7 @@
"outputs": {
"test": {
"sensitive": false,
"type": "string",
"value": "bar"
}
},
@ -73,6 +74,7 @@
"outputs": {
"test": {
"sensitive": false,
"type": "string",
"value": "bar"
}
},

View File

@ -13,6 +13,7 @@
"outputs": {
"foo_id": {
"sensitive": false,
"type": "string",
"value": "placeholder"
}
},
@ -37,6 +38,7 @@
"outputs": {
"foo_id": {
"sensitive": false,
"type": "string",
"value": "placeholder"
}
},

View File

@ -4,6 +4,7 @@
"outputs": {
"test": {
"sensitive": false,
"type": "string",
"value": "baz"
}
},
@ -79,6 +80,7 @@
"outputs": {
"test": {
"sensitive": false,
"type": "string",
"value": "baz"
}
},

View File

@ -10,6 +10,7 @@
"outputs": {
"test": {
"sensitive": false,
"type": "string",
"value": "bar"
}
},
@ -113,6 +114,7 @@
"outputs": {
"test": {
"sensitive": false,
"type": "string",
"value": "bar"
}
},

View File

@ -9,6 +9,7 @@
"outputs": {
"test": {
"sensitive": false,
"type": "string",
"value": "bar"
}
},
@ -62,6 +63,7 @@
"outputs": {
"test": {
"sensitive": false,
"type": "string",
"value": "bar"
}
},

View File

@ -9,6 +9,7 @@
"outputs": {
"test": {
"sensitive": false,
"type": "string",
"value": "bar"
}
},
@ -62,6 +63,7 @@
"outputs": {
"test": {
"sensitive": false,
"type": "string",
"value": "bar"
}
},

View File

@ -9,6 +9,7 @@
"outputs": {
"test": {
"sensitive": true,
"type": "string",
"value": "boop"
}
},
@ -74,6 +75,7 @@
"outputs": {
"test": {
"sensitive": true,
"type": "string",
"value": "boop"
}
},

View File

@ -217,6 +217,7 @@ The following example illustrates the structure of a `<values-representation>`:
"outputs": {
"private_ip": {
"value": "192.168.3.2",
"type": "string",
"sensitive": false
}
},
@ -307,6 +308,8 @@ The following example illustrates the structure of a `<values-representation>`:
The translation of attribute and output values is the same intuitive mapping from HCL types to JSON types used by Terraform's [`jsonencode`](/language/functions/jsonencode) function. This mapping does lose some information: lists, sets, and tuples all lower to JSON arrays while maps and objects both lower to JSON objects. Unknown values and null values are both treated as absent or null.
Output values include a `"type"` field, which is a [serialization of the value's type](https://pkg.go.dev/github.com/zclconf/go-cty/cty#Type.MarshalJSON). For primitive types this is a string value, such as `"number"` or `"bool"`. Complex types are represented as a nested JSON array, such as `["map","string"]` or `["object",{"a":"number"}]`. This can be used to reconstruct the output value with the correct type.
Only the "current" object for each resource instance is described. "Deposed" objects are not reflected in this structure at all; in plan representations, you can refer to the change representations for further details.
The intent of this structure is to give a caller access to a similar level of detail as is available to expressions within the configuration itself. This common representation is not suitable for all use-cases because it loses information compared to the data structures it is built from. For more complex needs, use the more elaborate changes and configuration representations.