mirror of
https://github.com/hashicorp/terraform.git
synced 2026-05-28 04:03:27 -04:00
actions: preserve unknown metadata in the json plan (#37611)
* actions: preserve unknown metadata in the json plan * copywrite headers
This commit is contained in:
parent
1ac8497994
commit
ffdf9f2a86
4 changed files with 236 additions and 68 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
182
internal/command/jsonplan/action_invocations_test.go
Normal file
182
internal/command/jsonplan/action_invocations_test.go
Normal file
|
|
@ -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
|
||||
}
|
||||
Loading…
Reference in a new issue