actions: preserve unknown metadata in the json plan (#37611)

* actions: preserve unknown metadata in the json plan

* copywrite headers
This commit is contained in:
Liam Cervante 2025-09-15 14:21:14 +02:00 committed by GitHub
parent 1ac8497994
commit ffdf9f2a86
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 236 additions and 68 deletions

View file

@ -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
}

View file

@ -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),

View file

@ -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
}

View 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
}