Store sensitive attribute paths in state (#26338)

* Add creation test and simplify in-place test

* Add deletion test

* Start adding marking from state

Start storing paths that should be marked
when pulled out of state. Implements deep
copy for attr paths. This commit also includes some
comment noise from investigations, and fixing the diff test

* Fix apply stripping marks

* Expand diff tests

* Basic apply test

* Update comments on equality checks to clarify current understanding

* Add JSON serialization for sensitive paths

We need to serialize a slice of cty.Path values to be used to re-mark
the sensitive values of a resource instance when loading the state file.
Paths consist of a list of steps, each of which may be either getting an
attribute value by name, or indexing into a collection by string or
number.

To serialize these without building a complex parser for a compact
string form, we render a nested array of small objects, like so:

[
  [
    { type: "get_attr", value: "foo" },
    { type: "index", value: { "type": "number", "value": 2 } }
  ]
]

The above example is equivalent to a path `foo[2]`.

* Format diffs with map types

Comparisons need unmarked values to operate on,
so create unmarked values for those operations. Additionally,
change diff to cover map types

* Remove debugging printing

* Fix bug with marking non-sensitive values

When pulling a sensitive value from state,
we were previously using those marks to remark
the planned new value, but that new value
might *not* be sensitive, so let's not do that

* Fix apply test

Apply was not passing the second state
through to the third pass at apply

* Consistency in checking for length of paths vs inspecting into value

* In apply, don't mark with before paths

* AttrPaths test coverage for DeepCopy

* Revert format changes

Reverts format changes in format/diff for this
branch so those changes can be discussed on a separate PR

* Refactor name of AttrPaths to AttrSensitivePaths

* Rename AttributePaths/attributePaths for naming consistency

Co-authored-by: Alisdair McDiarmid <alisdair@users.noreply.github.com>
This commit is contained in:
Pam Selle 2020-09-24 12:40:17 -04:00 committed by GitHub
parent a7c5a72c3d
commit 0a02e7040f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 686 additions and 60 deletions

View File

@ -3624,24 +3624,13 @@ func TestResourceChange_nestedMap(t *testing.T) {
func TestResourceChange_sensitiveVariable(t *testing.T) { func TestResourceChange_sensitiveVariable(t *testing.T) {
testCases := map[string]testCase{ testCases := map[string]testCase{
"in-place update - creation": { "creation": {
Action: plans.Update, Action: plans.Create,
Mode: addrs.ManagedResourceMode, Mode: addrs.ManagedResourceMode,
Before: cty.ObjectVal(map[string]cty.Value{ Before: cty.NullVal(cty.EmptyObject),
"id": cty.StringVal("i-02ae66f368e8518a9"),
"ami": cty.StringVal("ami-BEFORE"),
"root_block_device": cty.MapValEmpty(cty.Object(map[string]cty.Type{
"volume_type": cty.String,
})),
}),
After: cty.ObjectVal(map[string]cty.Value{ After: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("i-02ae66f368e8518a9"), "id": cty.StringVal("i-02ae66f368e8518a9"),
"ami": cty.StringVal("ami-AFTER"), "ami": cty.StringVal("ami-123"),
"root_block_device": cty.MapVal(map[string]cty.Value{
"a": cty.ObjectVal(map[string]cty.Value{
"volume_type": cty.StringVal("gp2"),
}),
}),
}), }),
AfterValMarks: []cty.PathValueMarks{ AfterValMarks: []cty.PathValueMarks{
{ {
@ -3655,29 +3644,141 @@ func TestResourceChange_sensitiveVariable(t *testing.T) {
"id": {Type: cty.String, Optional: true, Computed: true}, "id": {Type: cty.String, Optional: true, Computed: true},
"ami": {Type: cty.String, Optional: true}, "ami": {Type: cty.String, Optional: true},
}, },
BlockTypes: map[string]*configschema.NestedBlock{ },
"root_block_device": { ExpectedOutput: ` # test_instance.example will be created
Block: configschema.Block{ + resource "test_instance" "example" {
Attributes: map[string]*configschema.Attribute{ + ami = (sensitive)
"volume_type": { + id = "i-02ae66f368e8518a9"
Type: cty.String, }
Optional: true, `,
Computed: true, },
}, "in-place update - before sensitive": {
}, Action: plans.Update,
}, Mode: addrs.ManagedResourceMode,
Nesting: configschema.NestingMap, Before: cty.ObjectVal(map[string]cty.Value{
}, "id": cty.StringVal("i-02ae66f368e8518a9"),
"ami": cty.StringVal("ami-BEFORE"),
}),
After: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("i-02ae66f368e8518a9"),
"ami": cty.StringVal("ami-AFTER"),
}),
BeforeValMarks: []cty.PathValueMarks{
{
Path: cty.Path{cty.GetAttrStep{Name: "ami"}},
Marks: cty.NewValueMarks("sensitive"),
}},
RequiredReplace: cty.NewPathSet(),
Tainted: false,
Schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Optional: true, Computed: true},
"ami": {Type: cty.String, Optional: true},
}, },
}, },
ExpectedOutput: ` # test_instance.example will be updated in-place ExpectedOutput: ` # test_instance.example will be updated in-place
~ resource "test_instance" "example" { ~ resource "test_instance" "example" {
~ ami = (sensitive) ~ ami = (sensitive)
id = "i-02ae66f368e8518a9" id = "i-02ae66f368e8518a9"
}
+ root_block_device "a" { `,
+ volume_type = "gp2" },
} "in-place update - after sensitive, map type": {
Action: plans.Update,
Mode: addrs.ManagedResourceMode,
Before: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("i-02ae66f368e8518a9"),
"tags": cty.MapVal(map[string]cty.Value{
"name": cty.StringVal("anna"),
}),
}),
After: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("i-02ae66f368e8518a9"),
"tags": cty.MapVal(map[string]cty.Value{
"name": cty.StringVal("bob"),
}),
}),
AfterValMarks: []cty.PathValueMarks{
{
Path: cty.Path{cty.GetAttrStep{Name: "tags"}, cty.IndexStep{Key: cty.StringVal("name")}},
Marks: cty.NewValueMarks("sensitive"),
}},
RequiredReplace: cty.NewPathSet(),
Tainted: false,
Schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Optional: true, Computed: true},
"tags": {Type: cty.Map(cty.String), Optional: true},
},
},
ExpectedOutput: ` # test_instance.example will be updated in-place
~ resource "test_instance" "example" {
id = "i-02ae66f368e8518a9"
~ tags = (sensitive)
}
`,
},
"in-place update - both sensitive": {
Action: plans.Update,
Mode: addrs.ManagedResourceMode,
Before: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("i-02ae66f368e8518a9"),
"ami": cty.StringVal("ami-BEFORE"),
}),
After: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("i-02ae66f368e8518a9"),
"ami": cty.StringVal("ami-AFTER"),
}),
BeforeValMarks: []cty.PathValueMarks{
{
Path: cty.Path{cty.GetAttrStep{Name: "ami"}},
Marks: cty.NewValueMarks("sensitive"),
}},
AfterValMarks: []cty.PathValueMarks{
{
Path: cty.Path{cty.GetAttrStep{Name: "ami"}},
Marks: cty.NewValueMarks("sensitive"),
}},
RequiredReplace: cty.NewPathSet(),
Tainted: false,
Schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Optional: true, Computed: true},
"ami": {Type: cty.String, Optional: true},
},
},
ExpectedOutput: ` # test_instance.example will be updated in-place
~ resource "test_instance" "example" {
~ ami = (sensitive)
id = "i-02ae66f368e8518a9"
}
`,
},
"deletion": {
Action: plans.Delete,
Mode: addrs.ManagedResourceMode,
Before: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("i-02ae66f368e8518a9"),
"ami": cty.StringVal("ami-BEFORE"),
}),
After: cty.NullVal(cty.EmptyObject),
BeforeValMarks: []cty.PathValueMarks{
{
Path: cty.Path{cty.GetAttrStep{Name: "ami"}},
Marks: cty.NewValueMarks("sensitive"),
}},
RequiredReplace: cty.NewPathSet(),
Tainted: false,
Schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Optional: true, Computed: true},
"ami": {Type: cty.String, Optional: true},
},
},
ExpectedOutput: ` # test_instance.example will be destroyed
- resource "test_instance" "example" {
- ami = (sensitive)
- id = "i-02ae66f368e8518a9" -> null
} }
`, `,
}, },

View File

@ -238,7 +238,7 @@ type PlanResourceChangeResponse struct {
// configuration is applied. // configuration is applied.
PlannedState cty.Value PlannedState cty.Value
// RequiresReplace is the list of thee attributes that are requiring // RequiresReplace is the list of the attributes that are requiring
// resource replacement. // resource replacement.
RequiresReplace []cty.Path RequiresReplace []cty.Path

View File

@ -98,10 +98,11 @@ func (o *ResourceInstanceObject) Encode(ty cty.Type, schemaVersion uint64) (*Res
// and raise an error about that. // and raise an error about that.
val := cty.UnknownAsNull(o.Value) val := cty.UnknownAsNull(o.Value)
// If it contains marks, dump those now // If it contains marks, save these in state
unmarked := val unmarked := val
var pvm []cty.PathValueMarks
if val.ContainsMarked() { if val.ContainsMarked() {
unmarked, _ = val.UnmarkDeep() unmarked, pvm = val.UnmarkDeepWithPaths()
} }
src, err := ctyjson.Marshal(unmarked, ty) src, err := ctyjson.Marshal(unmarked, ty)
if err != nil { if err != nil {
@ -111,6 +112,7 @@ func (o *ResourceInstanceObject) Encode(ty cty.Type, schemaVersion uint64) (*Res
return &ResourceInstanceObjectSrc{ return &ResourceInstanceObjectSrc{
SchemaVersion: schemaVersion, SchemaVersion: schemaVersion,
AttrsJSON: src, AttrsJSON: src,
AttrSensitivePaths: pvm,
Private: o.Private, Private: o.Private,
Status: o.Status, Status: o.Status,
Dependencies: o.Dependencies, Dependencies: o.Dependencies,

View File

@ -49,6 +49,10 @@ type ResourceInstanceObjectSrc struct {
// the recommendations in the AttrsJSON documentation above. // the recommendations in the AttrsJSON documentation above.
AttrsFlat map[string]string AttrsFlat map[string]string
// AttrSensitivePaths is an array of paths to mark as sensitive coming out of
// state, or to save as sensitive paths when saving state
AttrSensitivePaths []cty.PathValueMarks
// These fields all correspond to the fields of the same name on // These fields all correspond to the fields of the same name on
// ResourceInstanceObject. // ResourceInstanceObject.
Private []byte Private []byte
@ -78,6 +82,10 @@ func (os *ResourceInstanceObjectSrc) Decode(ty cty.Type) (*ResourceInstanceObjec
} }
} else { } else {
val, err = ctyjson.Unmarshal(os.AttrsJSON, ty) val, err = ctyjson.Unmarshal(os.AttrsJSON, ty)
// Mark the value with paths if applicable
if os.AttrSensitivePaths != nil {
val = val.MarkWithPaths(os.AttrSensitivePaths)
}
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -144,6 +144,12 @@ func (obj *ResourceInstanceObjectSrc) DeepCopy() *ResourceInstanceObjectSrc {
copy(attrsJSON, obj.AttrsJSON) copy(attrsJSON, obj.AttrsJSON)
} }
var attrPaths []cty.PathValueMarks
if obj.AttrSensitivePaths != nil {
attrPaths = make([]cty.PathValueMarks, len(obj.AttrSensitivePaths))
copy(attrPaths, obj.AttrSensitivePaths)
}
var private []byte var private []byte
if obj.Private != nil { if obj.Private != nil {
private = make([]byte, len(obj.Private)) private = make([]byte, len(obj.Private))
@ -164,6 +170,7 @@ func (obj *ResourceInstanceObjectSrc) DeepCopy() *ResourceInstanceObjectSrc {
Private: private, Private: private,
AttrsFlat: attrsFlat, AttrsFlat: attrsFlat,
AttrsJSON: attrsJSON, AttrsJSON: attrsJSON,
AttrSensitivePaths: attrPaths,
Dependencies: dependencies, Dependencies: dependencies,
CreateBeforeDestroy: obj.CreateBeforeDestroy, CreateBeforeDestroy: obj.CreateBeforeDestroy,
} }

View File

@ -230,7 +230,14 @@ func TestStateDeepCopy(t *testing.T) {
Status: ObjectReady, Status: ObjectReady,
SchemaVersion: 1, SchemaVersion: 1,
AttrsJSON: []byte(`{"woozles":"confuzles"}`), AttrsJSON: []byte(`{"woozles":"confuzles"}`),
Private: []byte("private data"), // Sensitive path at "woozles"
AttrSensitivePaths: []cty.PathValueMarks{
{
Path: cty.Path{cty.GetAttrStep{Name: "woozles"}},
Marks: cty.NewValueMarks("sensitive"),
},
},
Private: []byte("private data"),
Dependencies: []addrs.ConfigResource{ Dependencies: []addrs.ConfigResource{
{ {
Module: addrs.RootModule, Module: addrs.RootModule,

View File

@ -7,6 +7,7 @@ import (
"sort" "sort"
version "github.com/hashicorp/go-version" version "github.com/hashicorp/go-version"
"github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json" ctyjson "github.com/zclconf/go-cty/cty/json"
"github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/addrs"
@ -151,6 +152,24 @@ func prepareStateV4(sV4 *stateV4) (*File, tfdiags.Diagnostics) {
} }
} }
// Sensitive paths
if isV4.AttributeSensitivePaths != nil {
paths, pathsDiags := unmarshalPaths([]byte(isV4.AttributeSensitivePaths))
diags = diags.Append(pathsDiags)
if pathsDiags.HasErrors() {
continue
}
var pvm []cty.PathValueMarks
for _, path := range paths {
pvm = append(pvm, cty.PathValueMarks{
Path: path,
Marks: cty.NewValueMarks("sensitive"),
})
}
obj.AttrSensitivePaths = pvm
}
{ {
// Status // Status
raw := isV4.Status raw := isV4.Status
@ -452,16 +471,27 @@ func appendInstanceObjectStateV4(rs *states.Resource, is *states.ResourceInstanc
} }
} }
// Extract paths from path value marks
var paths []cty.Path
for _, vm := range obj.AttrSensitivePaths {
paths = append(paths, vm.Path)
}
// Marshal paths to JSON
attributeSensitivePaths, pathsDiags := marshalPaths(paths)
diags = diags.Append(pathsDiags)
return append(isV4s, instanceObjectStateV4{ return append(isV4s, instanceObjectStateV4{
IndexKey: rawKey, IndexKey: rawKey,
Deposed: string(deposed), Deposed: string(deposed),
Status: status, Status: status,
SchemaVersion: obj.SchemaVersion, SchemaVersion: obj.SchemaVersion,
AttributesFlat: obj.AttrsFlat, AttributesFlat: obj.AttrsFlat,
AttributesRaw: obj.AttrsJSON, AttributesRaw: obj.AttrsJSON,
PrivateRaw: privateRaw, AttributeSensitivePaths: attributeSensitivePaths,
Dependencies: deps, PrivateRaw: privateRaw,
CreateBeforeDestroy: obj.CreateBeforeDestroy, Dependencies: deps,
CreateBeforeDestroy: obj.CreateBeforeDestroy,
}), diags }), diags
} }
@ -505,9 +535,10 @@ type instanceObjectStateV4 struct {
Status string `json:"status,omitempty"` Status string `json:"status,omitempty"`
Deposed string `json:"deposed,omitempty"` Deposed string `json:"deposed,omitempty"`
SchemaVersion uint64 `json:"schema_version"` SchemaVersion uint64 `json:"schema_version"`
AttributesRaw json.RawMessage `json:"attributes,omitempty"` AttributesRaw json.RawMessage `json:"attributes,omitempty"`
AttributesFlat map[string]string `json:"attributes_flat,omitempty"` AttributesFlat map[string]string `json:"attributes_flat,omitempty"`
AttributeSensitivePaths json.RawMessage `json:"sensitive_attributes,omitempty,"`
PrivateRaw []byte `json:"private,omitempty"` PrivateRaw []byte `json:"private,omitempty"`
@ -577,3 +608,135 @@ func (si sortInstancesV4) Less(i, j int) bool {
} }
return false return false
} }
// pathStep is an intermediate representation of a cty.PathStep to facilitate
// consistent JSON serialization. The Value field can either be a cty.Value of
// dynamic type (for index steps), or a string (for get attr steps).
type pathStep struct {
Type string `json:"type"`
Value json.RawMessage `json:"value"`
}
const (
indexPathStepType = "index"
getAttrPathStepType = "get_attr"
)
func unmarshalPaths(buf []byte) ([]cty.Path, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
var jsonPaths [][]pathStep
err := json.Unmarshal(buf, &jsonPaths)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Error unmarshaling path steps",
err.Error(),
))
}
paths := make([]cty.Path, 0, len(jsonPaths))
unmarshalOuter:
for _, jsonPath := range jsonPaths {
var path cty.Path
for _, jsonStep := range jsonPath {
switch jsonStep.Type {
case indexPathStepType:
key, err := ctyjson.Unmarshal(jsonStep.Value, cty.DynamicPseudoType)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Error unmarshaling path step",
fmt.Sprintf("Failed to unmarshal index step key: %s", err),
))
continue unmarshalOuter
}
path = append(path, cty.IndexStep{Key: key})
case getAttrPathStepType:
var name string
if err := json.Unmarshal(jsonStep.Value, &name); err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Error unmarshaling path step",
fmt.Sprintf("Failed to unmarshal get attr step name: %s", err),
))
continue unmarshalOuter
}
path = append(path, cty.GetAttrStep{Name: name})
default:
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Unsupported path step",
fmt.Sprintf("Unsupported path step type %q", jsonStep.Type),
))
continue unmarshalOuter
}
}
paths = append(paths, path)
}
return paths, diags
}
func marshalPaths(paths []cty.Path) ([]byte, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
// cty.Path is a slice of cty.PathSteps, so our representation of a slice
// of paths is a nested slice of our intermediate pathStep struct
jsonPaths := make([][]pathStep, 0, len(paths))
marshalOuter:
for _, path := range paths {
jsonPath := make([]pathStep, 0, len(path))
for _, step := range path {
var jsonStep pathStep
switch s := step.(type) {
case cty.IndexStep:
key, err := ctyjson.Marshal(s.Key, cty.DynamicPseudoType)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Error marshaling path step",
fmt.Sprintf("Failed to marshal index step key %#v: %s", s.Key, err),
))
continue marshalOuter
}
jsonStep.Type = indexPathStepType
jsonStep.Value = key
case cty.GetAttrStep:
name, err := json.Marshal(s.Name)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Error marshaling path step",
fmt.Sprintf("Failed to marshal get attr step name %s: %s", s.Name, err),
))
continue marshalOuter
}
jsonStep.Type = getAttrPathStepType
jsonStep.Value = name
default:
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Unsupported path step",
fmt.Sprintf("Unsupported path step %#v (%t)", step, step),
))
continue marshalOuter
}
jsonPath = append(jsonPath, jsonStep)
}
jsonPaths = append(jsonPaths, jsonPath)
}
buf, err := json.Marshal(jsonPaths)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Error marshaling path steps",
fmt.Sprintf("Failed to marshal path steps: %s", err),
))
}
return buf, diags
}

View File

@ -2,7 +2,11 @@ package statefile
import ( import (
"sort" "sort"
"strings"
"testing" "testing"
"github.com/hashicorp/terraform/tfdiags"
"github.com/zclconf/go-cty/cty"
) )
// This test verifies that modules are sorted before resources: // This test verifies that modules are sorted before resources:
@ -39,3 +43,216 @@ func TestVersion4_sort(t *testing.T) {
} }
} }
} }
func TestVersion4_unmarshalPaths(t *testing.T) {
testCases := map[string]struct {
json string
paths []cty.Path
diags []string
}{
"no paths": {
json: `[]`,
paths: []cty.Path{},
},
"attribute path": {
json: `[
[
{
"type": "get_attr",
"value": "password"
}
]
]`,
paths: []cty.Path{cty.GetAttrPath("password")},
},
"attribute and string index": {
json: `[
[
{
"type": "get_attr",
"value": "triggers"
},
{
"type": "index",
"value": {
"value": "secret",
"type": "string"
}
}
]
]`,
paths: []cty.Path{cty.GetAttrPath("triggers").IndexString("secret")},
},
"attribute, number index, attribute": {
json: `[
[
{
"type": "get_attr",
"value": "identities"
},
{
"type": "index",
"value": {
"value": 2,
"type": "number"
}
},
{
"type": "get_attr",
"value": "private_key"
}
]
]`,
paths: []cty.Path{cty.GetAttrPath("identities").IndexInt(2).GetAttr("private_key")},
},
"multiple paths": {
json: `[
[
{
"type": "get_attr",
"value": "alpha"
}
],
[
{
"type": "get_attr",
"value": "beta"
}
],
[
{
"type": "get_attr",
"value": "gamma"
}
]
]`,
paths: []cty.Path{cty.GetAttrPath("alpha"), cty.GetAttrPath("beta"), cty.GetAttrPath("gamma")},
},
"errors": {
json: `[
[
{
"type": "get_attr",
"value": 5
}
],
[
{
"type": "index",
"value": "test"
}
],
[
{
"type": "invalid_type",
"value": ["this is invalid too"]
}
]
]`,
paths: []cty.Path{},
diags: []string{
"Failed to unmarshal get attr step name",
"Failed to unmarshal index step key",
"Unsupported path step",
},
},
"one invalid path, others valid": {
json: `[
[
{
"type": "get_attr",
"value": "alpha"
}
],
[
{
"type": "invalid_type",
"value": ["this is invalid too"]
}
],
[
{
"type": "get_attr",
"value": "gamma"
}
]
]`,
paths: []cty.Path{cty.GetAttrPath("alpha"), cty.GetAttrPath("gamma")},
diags: []string{"Unsupported path step"},
},
"invalid structure": {
json: `{}`,
paths: []cty.Path{},
diags: []string{"Error unmarshaling path steps"},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
paths, diags := unmarshalPaths([]byte(tc.json))
if len(tc.diags) == 0 {
if len(diags) != 0 {
t.Errorf("expected no diags, got: %#v", diags)
}
} else {
if got, want := len(diags), len(tc.diags); got != want {
t.Fatalf("got %d diags, want %d\n%s", got, want, diags.Err())
}
for i := range tc.diags {
got := tfdiags.Diagnostics{diags[i]}.Err().Error()
if !strings.Contains(got, tc.diags[i]) {
t.Errorf("expected diag %d to contain %q, but was:\n%s", i, tc.diags[i], got)
}
}
}
if len(paths) != len(tc.paths) {
t.Fatalf("got %d paths, want %d", len(paths), len(tc.paths))
}
for i, path := range paths {
if !path.Equals(tc.paths[i]) {
t.Errorf("wrong paths\n got: %#v\nwant: %#v", path, tc.paths[i])
}
}
})
}
}
func TestVersion4_marshalPaths(t *testing.T) {
testCases := map[string]struct {
paths []cty.Path
json string
}{
"no paths": {
paths: []cty.Path{},
json: `[]`,
},
"attribute path": {
paths: []cty.Path{cty.GetAttrPath("password")},
json: `[[{"type":"get_attr","value":"password"}]]`,
},
"attribute, number index, attribute": {
paths: []cty.Path{cty.GetAttrPath("identities").IndexInt(2).GetAttr("private_key")},
json: `[[{"type":"get_attr","value":"identities"},{"type":"index","value":{"value":2,"type":"number"}},{"type":"get_attr","value":"private_key"}]]`,
},
"multiple paths": {
paths: []cty.Path{cty.GetAttrPath("a"), cty.GetAttrPath("b"), cty.GetAttrPath("c")},
json: `[[{"type":"get_attr","value":"a"}],[{"type":"get_attr","value":"b"}],[{"type":"get_attr","value":"c"}]]`,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
json, diags := marshalPaths(tc.paths)
if len(diags) != 0 {
t.Fatalf("expected no diags, got: %#v", diags)
}
if got, want := string(json), tc.json; got != want {
t.Fatalf("wrong JSON output\n got: %s\nwant: %s\n", got, want)
}
})
}
}

View File

@ -11916,3 +11916,82 @@ resource "test_resource" "c" {
t.Fatalf("apply errors: %s", diags.Err()) t.Fatalf("apply errors: %s", diags.Err())
} }
} }
func TestContext2Apply_variableSensitivity(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `
terraform {
experiments = [sensitive_variables]
}
variable "sensitive_var" {
default = "foo"
sensitive = true
}
resource "test_resource" "foo" {
value = var.sensitive_var
}`,
})
p := testProvider("test")
p.ApplyFn = testApplyFn
p.DiffFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Config: m,
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
if _, diags := ctx.Plan(); diags.HasErrors() {
t.Fatalf("plan errors: %s", diags.Err())
}
state, diags := ctx.Apply()
if diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
// Run a second apply with no changes
ctx = testContext2(t, &ContextOpts{
Config: m,
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
State: state,
})
if _, diags := ctx.Plan(); diags.HasErrors() {
t.Fatalf("plan errors: %s", diags.Err())
}
state, diags = ctx.Apply()
if diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
// Now change the variable value
ctx = testContext2(t, &ContextOpts{
Config: m,
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
Variables: InputValues{
"sensitive_var": &InputValue{
Value: cty.StringVal("bar"),
},
},
State: state,
})
if _, diags := ctx.Plan(); diags.HasErrors() {
t.Fatalf("plan errors: %s", diags.Err())
}
_, diags = ctx.Apply()
if diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
}

View File

@ -105,21 +105,28 @@ func (n *EvalApply) Eval(ctx EvalContext) (interface{}, error) {
log.Printf("[DEBUG] %s: applying the planned %s change", n.Addr.Absolute(ctx.Path()), change.Action) log.Printf("[DEBUG] %s: applying the planned %s change", n.Addr.Absolute(ctx.Path()), change.Action)
// If our config or After value contain any marked values, // If our config, Before or After value contain any marked values,
// ensure those are stripped out before sending // ensure those are stripped out before sending
// this to the provider // this to the provider
unmarkedConfigVal := configVal unmarkedConfigVal := configVal
if configVal.ContainsMarked() { if configVal.ContainsMarked() {
unmarkedConfigVal, _ = configVal.UnmarkDeep() unmarkedConfigVal, _ = configVal.UnmarkDeep()
} }
unmarkedBefore := change.Before
if change.Before.ContainsMarked() {
unmarkedBefore, _ = change.Before.UnmarkDeep()
}
unmarkedAfter := change.After unmarkedAfter := change.After
var afterPaths []cty.PathValueMarks
if change.After.ContainsMarked() { if change.After.ContainsMarked() {
unmarkedAfter, _ = change.After.UnmarkDeep() unmarkedAfter, afterPaths = change.After.UnmarkDeepWithPaths()
} }
resp := provider.ApplyResourceChange(providers.ApplyResourceChangeRequest{ resp := provider.ApplyResourceChange(providers.ApplyResourceChangeRequest{
TypeName: n.Addr.Resource.Type, TypeName: n.Addr.Resource.Type,
PriorState: change.Before, PriorState: unmarkedBefore,
Config: unmarkedConfigVal, Config: unmarkedConfigVal,
PlannedState: unmarkedAfter, PlannedState: unmarkedAfter,
PlannedPrivate: change.Private, PlannedPrivate: change.Private,
@ -138,6 +145,11 @@ func (n *EvalApply) Eval(ctx EvalContext) (interface{}, error) {
// incomplete. // incomplete.
newVal := resp.NewState newVal := resp.NewState
// If we have paths to mark, mark those on this new value
if len(afterPaths) > 0 {
newVal = newVal.MarkWithPaths(afterPaths)
}
if newVal == cty.NilVal { if newVal == cty.NilVal {
// Providers are supposed to return a partial new value even when errors // Providers are supposed to return a partial new value even when errors
// occur, but sometimes they don't and so in that case we'll patch that up // occur, but sometimes they don't and so in that case we'll patch that up

View File

@ -213,7 +213,14 @@ func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) {
priorVal = cty.NullVal(schema.ImpliedType()) priorVal = cty.NullVal(schema.ImpliedType())
} }
proposedNewVal := objchange.ProposedNewObject(schema, priorVal, unmarkedConfigVal) unmarkedPriorVal := priorVal
if priorVal.ContainsMarked() {
// store the marked values so we can re-mark them later after
// we've sent things over the wire.
unmarkedPriorVal, _ = priorVal.UnmarkDeep()
}
proposedNewVal := objchange.ProposedNewObject(schema, unmarkedPriorVal, unmarkedConfigVal)
// Call pre-diff hook // Call pre-diff hook
if !n.Stub { if !n.Stub {
@ -244,7 +251,7 @@ func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) {
// we send back this information, we need to process ignore_changes // we send back this information, we need to process ignore_changes
// so that CustomizeDiff will not act on them // so that CustomizeDiff will not act on them
var ignoreChangeDiags tfdiags.Diagnostics var ignoreChangeDiags tfdiags.Diagnostics
proposedNewVal, ignoreChangeDiags = n.processIgnoreChanges(priorVal, proposedNewVal) proposedNewVal, ignoreChangeDiags = n.processIgnoreChanges(unmarkedPriorVal, proposedNewVal)
diags = diags.Append(ignoreChangeDiags) diags = diags.Append(ignoreChangeDiags)
if ignoreChangeDiags.HasErrors() { if ignoreChangeDiags.HasErrors() {
return nil, diags.Err() return nil, diags.Err()
@ -253,7 +260,7 @@ func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) {
resp := provider.PlanResourceChange(providers.PlanResourceChangeRequest{ resp := provider.PlanResourceChange(providers.PlanResourceChangeRequest{
TypeName: n.Addr.Resource.Type, TypeName: n.Addr.Resource.Type,
Config: unmarkedConfigVal, Config: unmarkedConfigVal,
PriorState: priorVal, PriorState: unmarkedPriorVal,
ProposedNewState: proposedNewVal, ProposedNewState: proposedNewVal,
PriorPrivate: priorPrivate, PriorPrivate: priorPrivate,
ProviderMeta: metaConfigVal, ProviderMeta: metaConfigVal,
@ -274,7 +281,7 @@ func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) {
} }
// Add the marks back to the planned new value // Add the marks back to the planned new value
if configVal.ContainsMarked() { if len(unmarkedPaths) > 0 {
plannedNewVal = plannedNewVal.MarkWithPaths(unmarkedPaths) plannedNewVal = plannedNewVal.MarkWithPaths(unmarkedPaths)
} }
@ -352,7 +359,7 @@ func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) {
continue continue
} }
priorChangedVal, priorPathDiags := hcl.ApplyPath(priorVal, path, nil) priorChangedVal, priorPathDiags := hcl.ApplyPath(unmarkedPriorVal, path, nil)
plannedChangedVal, plannedPathDiags := hcl.ApplyPath(plannedNewVal, path, nil) plannedChangedVal, plannedPathDiags := hcl.ApplyPath(plannedNewVal, path, nil)
if plannedPathDiags.HasErrors() && priorPathDiags.HasErrors() { if plannedPathDiags.HasErrors() && priorPathDiags.HasErrors() {
// This means the path was invalid in both the prior and new // This means the path was invalid in both the prior and new
@ -384,7 +391,10 @@ func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) {
plannedChangedVal = cty.NullVal(priorChangedVal.Type()) plannedChangedVal = cty.NullVal(priorChangedVal.Type())
} }
eqV := plannedChangedVal.Equals(priorChangedVal) // Unmark for this value for the equality test. If only sensitivity has changed,
// this does not require an Update or Replace
unmarkedPlannedChangedVal, _ := plannedChangedVal.UnmarkDeep()
eqV := unmarkedPlannedChangedVal.Equals(priorChangedVal)
if !eqV.IsKnown() || eqV.False() { if !eqV.IsKnown() || eqV.False() {
reqRep.Add(path) reqRep.Add(path)
} }
@ -394,7 +404,10 @@ func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) {
} }
} }
eqV := plannedNewVal.Equals(priorVal) // Unmark for this test for equality. If only sensitivity has changed,
// this does not require an Update or Replace
unmarkedPlannedNewVal, _ := plannedNewVal.UnmarkDeep()
eqV := unmarkedPlannedNewVal.Equals(unmarkedPriorVal)
eq := eqV.IsKnown() && eqV.True() eq := eqV.IsKnown() && eqV.True()
var action plans.Action var action plans.Action
@ -432,11 +445,11 @@ func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) {
nullPriorVal := cty.NullVal(schema.ImpliedType()) nullPriorVal := cty.NullVal(schema.ImpliedType())
// create a new proposed value from the null state and the config // create a new proposed value from the null state and the config
proposedNewVal = objchange.ProposedNewObject(schema, nullPriorVal, configVal) proposedNewVal = objchange.ProposedNewObject(schema, nullPriorVal, unmarkedConfigVal)
resp = provider.PlanResourceChange(providers.PlanResourceChangeRequest{ resp = provider.PlanResourceChange(providers.PlanResourceChangeRequest{
TypeName: n.Addr.Resource.Type, TypeName: n.Addr.Resource.Type,
Config: configVal, Config: unmarkedConfigVal,
PriorState: nullPriorVal, PriorState: nullPriorVal,
ProposedNewState: proposedNewVal, ProposedNewState: proposedNewVal,
PriorPrivate: plannedPrivate, PriorPrivate: plannedPrivate,
@ -453,6 +466,11 @@ func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) {
} }
plannedNewVal = resp.PlannedState plannedNewVal = resp.PlannedState
plannedPrivate = resp.PlannedPrivate plannedPrivate = resp.PlannedPrivate
if len(unmarkedPaths) > 0 {
plannedNewVal = plannedNewVal.MarkWithPaths(unmarkedPaths)
}
for _, err := range plannedNewVal.Type().TestConformance(schema.ImpliedType()) { for _, err := range plannedNewVal.Type().TestConformance(schema.ImpliedType()) {
diags = diags.Append(tfdiags.Sourceless( diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error, tfdiags.Error,

View File

@ -80,6 +80,13 @@ func (n *EvalRefresh) Eval(ctx EvalContext) (interface{}, error) {
// Refresh! // Refresh!
priorVal := state.Value priorVal := state.Value
// Unmarked before sending to provider
var priorPaths []cty.PathValueMarks
if priorVal.ContainsMarked() {
priorVal, priorPaths = priorVal.UnmarkDeepWithPaths()
}
req := providers.ReadResourceRequest{ req := providers.ReadResourceRequest{
TypeName: n.Addr.Resource.Type, TypeName: n.Addr.Resource.Type,
PriorState: priorVal, PriorState: priorVal,
@ -129,6 +136,11 @@ func (n *EvalRefresh) Eval(ctx EvalContext) (interface{}, error) {
return nil, err return nil, err
} }
// Mark the value if necessary
if len(priorPaths) > 0 {
newState.Value = newState.Value.MarkWithPaths(priorPaths)
}
if n.Output != nil { if n.Output != nil {
*n.Output = newState *n.Output = newState
} }