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 { type output struct {
Sensitive bool `json:"sensitive"` Sensitive bool `json:"sensitive"`
Type json.RawMessage `json:"type,omitempty"`
Value json.RawMessage `json:"value,omitempty"` Value json.RawMessage `json:"value,omitempty"`
} }

View File

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

View File

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

View File

@ -38,6 +38,7 @@ type stateValues struct {
type output struct { type output struct {
Sensitive bool `json:"sensitive"` Sensitive bool `json:"sensitive"`
Value json.RawMessage `json:"value,omitempty"` 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 // 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) ret := make(map[string]output)
for k, v := range outputs { 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 { if err != nil {
return ret, err return ret, err
} }
ret[k] = output{ ret[k] = output{
Value: ov, Value: ov,
Type: ot,
Sensitive: v.Sensitive, Sensitive: v.Sensitive,
} }
} }

View File

@ -36,6 +36,7 @@ func TestMarshalOutputs(t *testing.T) {
"test": { "test": {
Sensitive: true, Sensitive: true,
Value: json.RawMessage(`"sekret"`), Value: json.RawMessage(`"sekret"`),
Type: json.RawMessage(`"string"`),
}, },
}, },
false, false,
@ -51,6 +52,39 @@ func TestMarshalOutputs(t *testing.T) {
"test": { "test": {
Sensitive: false, Sensitive: false,
Value: json.RawMessage(`"not_so_sekret"`), 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, false,
@ -67,10 +101,8 @@ func TestMarshalOutputs(t *testing.T) {
} else if err != nil { } else if err != nil {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
} }
eq := reflect.DeepEqual(got, test.Want) if !cmp.Equal(test.Want, got) {
if !eq { t.Fatalf("wrong result:\n%s", cmp.Diff(test.Want, got))
// 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)
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -217,6 +217,7 @@ The following example illustrates the structure of a `<values-representation>`:
"outputs": { "outputs": {
"private_ip": { "private_ip": {
"value": "192.168.3.2", "value": "192.168.3.2",
"type": "string",
"sensitive": false "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. 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. 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. 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.