diff --git a/internal/command/jsonformat/plan_test.go b/internal/command/jsonformat/plan_test.go index 6ecb6d1d45..0ef25d612b 100644 --- a/internal/command/jsonformat/plan_test.go +++ b/internal/command/jsonformat/plan_test.go @@ -34,12 +34,10 @@ func TestRenderHuman_InvokeActionPlan(t *testing.T) { plan := Plan{ ActionInvocations: []jsonplan.ActionInvocation{ { - Address: "action.test_action.action", - Type: "test_action", - Name: "action", - ConfigValues: map[string]json.RawMessage{ - "attr": []byte("\"one\""), - }, + Address: "action.test_action.action", + Type: "test_action", + Name: "action", + ConfigValues: json.RawMessage("{\"attr\":\"one\"}"), ConfigSensitive: nil, ProviderName: "test", InvokeActionTrigger: new(jsonplan.InvokeActionTrigger), @@ -95,12 +93,10 @@ func TestRenderHuman_InvokeActionPlanWithRefresh(t *testing.T) { plan := Plan{ ActionInvocations: []jsonplan.ActionInvocation{ { - Address: "action.test_action.action", - Type: "test_action", - Name: "action", - ConfigValues: map[string]json.RawMessage{ - "attr": []byte("\"one\""), - }, + Address: "action.test_action.action", + Type: "test_action", + Name: "action", + ConfigValues: json.RawMessage("{\"attr\":\"one\"}"), ConfigSensitive: nil, ProviderName: "test", InvokeActionTrigger: new(jsonplan.InvokeActionTrigger), @@ -8483,7 +8479,7 @@ func TestResourceChange_actions(t *testing.T) { TriggeringResourceAddress: triggeringResourceAddr.String(), ActionTriggerEvent: "BeforeCreate", }, - ConfigValues: marshalConfigValues(cty.ObjectVal(map[string]cty.Value{ + ConfigValues: marshalCtyJson(t, cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"), "disk": cty.ObjectVal(map[string]cty.Value{ "size": cty.StringVal("100"), @@ -8527,7 +8523,7 @@ func TestResourceChange_actions(t *testing.T) { TriggeringResourceAddress: triggeringResourceAddr.String(), ActionTriggerEvent: "AfterCreate", }, - ConfigValues: marshalConfigValues(cty.ObjectVal(map[string]cty.Value{ + ConfigValues: marshalCtyJson(t, cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"), "disk": cty.ObjectVal(map[string]cty.Value{ "size": cty.StringVal("100"), @@ -8571,7 +8567,7 @@ func TestResourceChange_actions(t *testing.T) { TriggeringResourceAddress: triggeringResourceAddr.String(), ActionTriggerEvent: "BeforeCreate", }, - ConfigValues: marshalConfigValues(cty.ObjectVal(map[string]cty.Value{ + ConfigValues: marshalCtyJson(t, cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("first-block-and-action"), })), }, @@ -8586,7 +8582,7 @@ func TestResourceChange_actions(t *testing.T) { TriggeringResourceAddress: triggeringResourceAddr.String(), ActionTriggerEvent: "BeforeCreate", }, - ConfigValues: marshalConfigValues(cty.ObjectVal(map[string]cty.Value{ + ConfigValues: marshalCtyJson(t, cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("first-block-second-action"), })), }, @@ -8601,7 +8597,7 @@ func TestResourceChange_actions(t *testing.T) { TriggeringResourceAddress: triggeringResourceAddr.String(), ActionTriggerEvent: "AfterCreate", }, - ConfigValues: marshalConfigValues(cty.ObjectVal(map[string]cty.Value{ + ConfigValues: marshalCtyJson(t, cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("second-block-first-action"), })), }, @@ -8616,7 +8612,7 @@ func TestResourceChange_actions(t *testing.T) { TriggeringResourceAddress: triggeringResourceAddr.String(), ActionTriggerEvent: "AfterCreate", }, - ConfigValues: marshalConfigValues(cty.ObjectVal(map[string]cty.Value{ + ConfigValues: marshalCtyJson(t, cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("third-block-first-action"), })), }, @@ -8631,7 +8627,7 @@ func TestResourceChange_actions(t *testing.T) { TriggeringResourceAddress: triggeringResourceAddr.String(), ActionTriggerEvent: "BeforeCreate", }, - ConfigValues: marshalConfigValues(cty.ObjectVal(map[string]cty.Value{ + ConfigValues: marshalCtyJson(t, cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("fourth-block-first-action"), })), }, @@ -8739,23 +8735,6 @@ func TestResourceChange_actions(t *testing.T) { }) } } -func marshalConfigValues(value cty.Value) map[string]json.RawMessage { - // unmark our value to show all values - v, _ := value.UnmarkDeep() - - if v == cty.NilVal || v.IsNull() { - return nil - } - - ret := make(map[string]json.RawMessage) - it := value.ElementIterator() - for it.Next() { - k, v := it.Element() - vJSON, _ := ctyjson.Marshal(v, v.Type()) - ret[k.AsString()] = json.RawMessage(vJSON) - } - return ret -} func outputChange(name string, before, after cty.Value, sensitive bool) *plans.OutputChangeSrc { addr := addrs.AbsOutputValue{ @@ -8931,3 +8910,11 @@ func marshalJson(t *testing.T, data interface{}) json.RawMessage { } return result } + +func marshalCtyJson(t *testing.T, data cty.Value) json.RawMessage { + result, err := ctyjson.Marshal(data, data.Type()) + if err != nil { + t.Fatalf("failed to marshal json: %v", err) + } + return result +} diff --git a/internal/command/jsonformat/structured/change.go b/internal/command/jsonformat/structured/change.go index 7c97efdaf8..e909888017 100644 --- a/internal/command/jsonformat/structured/change.go +++ b/internal/command/jsonformat/structured/change.go @@ -187,9 +187,9 @@ func FromJsonViewsOutput(output viewsjson.Output) Change { func FromJsonActionInvocation(actionInvocation jsonplan.ActionInvocation) Change { return Change{ - Before: unwrapAttributeValues(actionInvocation.ConfigValues), - After: unwrapAttributeValues(actionInvocation.ConfigValues), - Unknown: false, + Before: unmarshalGeneric(actionInvocation.ConfigValues), + After: unmarshalGeneric(actionInvocation.ConfigValues), + Unknown: unmarshalGeneric(actionInvocation.ConfigUnknown), BeforeSensitive: unmarshalGeneric(actionInvocation.ConfigSensitive), AfterSensitive: unmarshalGeneric(actionInvocation.ConfigSensitive), diff --git a/internal/command/jsonplan/action_invocations.go b/internal/command/jsonplan/action_invocations.go index 9f15db30ef..aa5af199d6 100644 --- a/internal/command/jsonplan/action_invocations.go +++ b/internal/command/jsonplan/action_invocations.go @@ -28,8 +28,9 @@ type ActionInvocation struct { Name string `json:"name,omitempty"` // ConfigValues is the JSON representation of the values in the config block of the action - ConfigValues map[string]json.RawMessage `json:"config_values,omitempty"` - ConfigSensitive json.RawMessage `json:"config_sensitive,omitempty"` + ConfigValues json.RawMessage `json:"config_values,omitempty"` + ConfigSensitive json.RawMessage `json:"config_sensitive,omitempty"` + ConfigUnknown json.RawMessage `json:"config_unknown,omitempty"` // ProviderName allows the property "type" to be interpreted unambiguously // in the unusual situation where a provider offers a type whose @@ -93,24 +94,6 @@ func ActionInvocationCompare(a, b ActionInvocation) int { return 0 } -func marshalConfigValues(value cty.Value) map[string]json.RawMessage { - // unmark our value to show all values - v, _ := value.UnmarkDeep() - - if v == cty.NilVal || v.IsNull() { - return nil - } - - ret := make(map[string]json.RawMessage) - it := v.ElementIterator() - for it.Next() { - k, v := it.Element() - vJSON, _ := ctyjson.Marshal(v, v.Type()) - ret[k.AsString()] = json.RawMessage(vJSON) - } - return ret -} - func MarshalActionInvocations(actions []*plans.ActionInvocationInstanceSrc, schemas *terraform.Schemas) ([]ActionInvocation, error) { ret := make([]ActionInvocation, 0, len(actions)) for _, action := range actions { @@ -157,8 +140,12 @@ func MarshalActionInvocation(action *plans.ActionInvocationInstanceSrc, schemas return ai, fmt.Errorf("unsupported action trigger type: %T", at) } + var config []byte + var sensitive []byte + var unknown []byte + if actionDec.ConfigValue != cty.NilVal { - _, pvms := actionDec.ConfigValue.UnmarkDeepWithPaths() + unmarkedValue, pvms := actionDec.ConfigValue.UnmarkDeepWithPaths() sensitivePaths, otherMarks := marks.PathsWithMark(pvms, marks.Sensitive) ephemeralPaths, otherMarks := marks.PathsWithMark(otherMarks, marks.Ephemeral) if len(ephemeralPaths) > 0 { @@ -168,19 +155,31 @@ func MarshalActionInvocation(action *plans.ActionInvocationInstanceSrc, schemas return ai, fmt.Errorf("action %s has config values with unsupported marks: %v", action.Addr, otherMarks) } - configValue := actionDec.ConfigValue - if !configValue.IsWhollyKnown() { - configValue = omitUnknowns(actionDec.ConfigValue) - } - cs := jsonstate.SensitiveAsBool(marks.MarkPaths(configValue, marks.Sensitive, sensitivePaths)) - configSensitive, err := ctyjson.Marshal(cs, cs.Type()) + unknownValue := unknownAsBool(unmarkedValue) + unknown, err = ctyjson.Marshal(unknownValue, unknownValue.Type()) + if err != nil { + return ai, err + } + + configValue := omitUnknowns(unmarkedValue) + config, err = ctyjson.Marshal(configValue, configValue.Type()) + if err != nil { + return ai, err + } + + sensitivePaths = append(sensitivePaths, schema.ConfigSchema.SensitivePaths(unmarkedValue, nil)...) + cs := jsonstate.SensitiveAsBool(marks.MarkPaths(unmarkedValue, marks.Sensitive, sensitivePaths)) + sensitive, err = ctyjson.Marshal(cs, cs.Type()) if err != nil { return ai, err } - ai.ConfigValues = marshalConfigValues(configValue) - ai.ConfigSensitive = configSensitive } + + ai.ConfigValues = config + ai.ConfigSensitive = sensitive + ai.ConfigUnknown = unknown + return ai, nil } diff --git a/internal/command/jsonplan/action_invocations_test.go b/internal/command/jsonplan/action_invocations_test.go new file mode 100644 index 0000000000..ff83e9849b --- /dev/null +++ b/internal/command/jsonplan/action_invocations_test.go @@ -0,0 +1,182 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package jsonplan + +import ( + "encoding/json" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/msgpack" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/terraform" +) + +func TestMarshalActionInvocations(t *testing.T) { + + action := addrs.AbsActionInstance{ + Module: addrs.RootModuleInstance, + Action: addrs.ActionInstance{ + Action: addrs.Action{ + Type: "test_action", + Name: "test", + }, + Key: addrs.NoKey, + }, + } + + provider := addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.Provider{ + Type: "test", + Namespace: "hashicorp", + Hostname: addrs.DefaultProviderRegistryHost, + }, + } + + schemas := &terraform.Schemas{ + Providers: map[addrs.Provider]providers.ProviderSchema{ + provider.Provider: { + Actions: map[string]providers.ActionSchema{ + "test_action": { + ConfigSchema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "optional": { + Type: cty.String, + }, + "sensitive": { + Type: cty.String, + Sensitive: true, + }, + }, + }, + }, + }, + }, + }, + } + + tcs := map[string]struct { + input *plans.ActionInvocationInstanceSrc + output ActionInvocation + }{ + "no metadata": { + input: &plans.ActionInvocationInstanceSrc{ + Addr: action, + ActionTrigger: new(plans.InvokeActionTrigger), + ConfigValue: mustDynamicValue(t, cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("hello"), + "sensitive": cty.StringVal("world"), + })), + SensitiveConfigPaths: nil, + ProviderAddr: provider, + }, + output: ActionInvocation{ + Address: "action.test_action.test", + Type: "test_action", + Name: "test", + ConfigValues: mustJson(t, map[string]interface{}{ + "optional": "hello", + "sensitive": "world", + }), + ConfigSensitive: mustJson(t, map[string]interface{}{ + "sensitive": true, + }), + ConfigUnknown: mustJson(t, map[string]interface{}{}), + ProviderName: "registry.terraform.io/hashicorp/test", + InvokeActionTrigger: new(InvokeActionTrigger), + }, + }, + "unknown value": { + input: &plans.ActionInvocationInstanceSrc{ + Addr: action, + ActionTrigger: new(plans.InvokeActionTrigger), + ConfigValue: mustDynamicValue(t, cty.ObjectVal(map[string]cty.Value{ + "optional": cty.UnknownVal(cty.String), + "sensitive": cty.StringVal("world"), + })), + SensitiveConfigPaths: nil, + ProviderAddr: provider, + }, + output: ActionInvocation{ + Address: "action.test_action.test", + Type: "test_action", + Name: "test", + ConfigValues: mustJson(t, map[string]interface{}{ + "sensitive": "world", + }), + ConfigSensitive: mustJson(t, map[string]interface{}{ + "sensitive": true, + }), + ConfigUnknown: mustJson(t, map[string]interface{}{ + "optional": true, + }), + ProviderName: "registry.terraform.io/hashicorp/test", + InvokeActionTrigger: new(InvokeActionTrigger), + }, + }, + "extra sensitive": { + input: &plans.ActionInvocationInstanceSrc{ + Addr: action, + ActionTrigger: new(plans.InvokeActionTrigger), + ConfigValue: mustDynamicValue(t, cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("hello"), + "sensitive": cty.StringVal("world"), + })), + SensitiveConfigPaths: []cty.Path{cty.GetAttrPath("optional")}, + ProviderAddr: provider, + }, + output: ActionInvocation{ + Address: "action.test_action.test", + Type: "test_action", + Name: "test", + ConfigValues: mustJson(t, map[string]interface{}{ + "optional": "hello", + "sensitive": "world", + }), + ConfigSensitive: mustJson(t, map[string]interface{}{ + "optional": true, + "sensitive": true, + }), + ConfigUnknown: mustJson(t, map[string]interface{}{}), + ProviderName: "registry.terraform.io/hashicorp/test", + InvokeActionTrigger: new(InvokeActionTrigger), + }, + }, + } + + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + out, err := MarshalActionInvocation(tc.input, schemas) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(tc.output, out); len(diff) > 0 { + t.Fatal(diff) + } + }) + } + +} + +func mustDynamicValue(t *testing.T, value cty.Value) []byte { + out, err := msgpack.Marshal(value, value.Type()) + if err != nil { + t.Fatal(err) + } + return out +} + +func mustJson(t *testing.T, data interface{}) json.RawMessage { + out, err := json.Marshal(data) + if err != nil { + t.Fatal(err) + } + return out +}