Improve comparison of sensitive marks on resources, and propagate the sensitive_attributes correctly (#1640)

Signed-off-by: James Humphries <james@james-humphries.co.uk>
This commit is contained in:
James Humphries 2024-07-09 13:42:02 +01:00 committed by GitHub
parent bb1176d3a6
commit 12d9380982
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 355 additions and 116 deletions

View File

@ -29,6 +29,7 @@ BUG FIXES:
* Fix race condition on locking in gcs backend ([#1342](https://github.com/opentofu/opentofu/pull/1342))
* Fix bug where provider functions were unusable in variables and outputs ([#1689](https://github.com/opentofu/opentofu/pull/1689))
* Fix bug where lower-case `http_proxy`/`https_proxy` env variables were no longer supported in the S3 backend ([#1594](https://github.com/opentofu/opentofu/issues/1594))
* Fixed issue with migration between versions can cause an update in-place for resources when no changes are needed. ([#1640](https://github.com/opentofu/opentofu/pull/1640))
## Previous Releases

View File

@ -28,7 +28,8 @@
"password": "secret"
},
"sensitive_values": {
"ami": true
"ami": true,
"password": true
}
},
{
@ -44,7 +45,8 @@
"password": "secret"
},
"sensitive_values": {
"ami": true
"ami": true,
"password": true
}
},
{
@ -60,7 +62,8 @@
"password": "secret"
},
"sensitive_values": {
"ami": true
"ami": true,
"password": true
}
}
]

View File

@ -16,7 +16,15 @@
"attributes": {
"ami": "ami-test",
"id": "placeholder"
}
},
"sensitive_attributes": [
[
{
"type": "get_attr",
"value": "password"
}
]
]
}
]
},
@ -31,7 +39,15 @@
"attributes": {
"ami": "ami-test",
"id": "placeheld"
}
},
"sensitive_attributes": [
[
{
"type": "get_attr",
"value": "password"
}
]
]
}
]
}

View File

@ -11963,22 +11963,35 @@ resource "test_resource" "foo" {
t.Fatalf("plan errors: %s", diags.Err())
}
verifySensitiveValue := func(pvms []cty.PathValueMarks) {
if len(pvms) != 1 {
t.Fatalf("expected 1 sensitive path, got %d", len(pvms))
wantedAttrPaths := []cty.Path{cty.GetAttrPath("sensitive_value"), cty.GetAttrPath("value")}
isAWantedAttrPath := func(p cty.Path) bool {
for _, wanted := range wantedAttrPaths {
if p.Equals(wanted) {
return true
}
}
pvm := pvms[0]
if gotPath, wantPath := pvm.Path, cty.GetAttrPath("value"); !gotPath.Equals(wantPath) {
t.Errorf("wrong path\n got: %#v\nwant: %#v", gotPath, wantPath)
return false
}
verifySensitiveValues := func(pvms []cty.PathValueMarks) {
if len(pvms) != len(wantedAttrPaths) {
t.Fatalf("expected %d sensitive paths, got %d", len(wantedAttrPaths), len(pvms))
}
if gotMarks, wantMarks := pvm.Marks, cty.NewValueMarks(marks.Sensitive); !gotMarks.Equal(wantMarks) {
t.Errorf("wrong marks\n got: %#v\nwant: %#v", gotMarks, wantMarks)
for _, pvm := range pvms {
if !isAWantedAttrPath(pvm.Path) {
t.Errorf("unexpected path\n got: %#v\n", pvm.Path)
}
if !pvm.Marks.Equal(cty.NewValueMarks(marks.Sensitive)) {
t.Errorf("wrong marks\n got: %#v\nwant: %#v", pvm.Marks, cty.NewValueMarks(marks.Sensitive))
}
}
}
addr := mustResourceInstanceAddr("test_resource.foo")
fooChangeSrc := plan.Changes.ResourceInstance(addr)
verifySensitiveValue(fooChangeSrc.AfterValMarks)
verifySensitiveValues(fooChangeSrc.AfterValMarks)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
@ -11986,7 +11999,7 @@ resource "test_resource" "foo" {
}
fooState := state.ResourceInstance(addr)
verifySensitiveValue(fooState.Current.AttrSensitivePaths)
verifySensitiveValues(fooState.Current.AttrSensitivePaths)
}
func TestContext2Apply_variableSensitivityProviders(t *testing.T) {
@ -12026,29 +12039,54 @@ resource "test_resource" "baz" {
t.Fatalf("plan errors: %s", diags.Err())
}
verifySensitiveValue := func(pvms []cty.PathValueMarks) {
if len(pvms) != 1 {
t.Fatalf("expected 1 sensitive path, got %d", len(pvms))
isAWantedAttrPath := func(p cty.Path, wantedAttrPaths []cty.Path) bool {
for _, wanted := range wantedAttrPaths {
if p.Equals(wanted) {
return true
}
}
pvm := pvms[0]
if gotPath, wantPath := pvm.Path, cty.GetAttrPath("value"); !gotPath.Equals(wantPath) {
t.Errorf("wrong path\n got: %#v\nwant: %#v", gotPath, wantPath)
return false
}
verifySensitiveValues := func(pvms []cty.PathValueMarks, wantedAttrPaths []cty.Path) {
if len(pvms) != len(wantedAttrPaths) {
t.Fatalf("expected %d sensitive paths, got %d", len(wantedAttrPaths), len(pvms))
}
if gotMarks, wantMarks := pvm.Marks, cty.NewValueMarks(marks.Sensitive); !gotMarks.Equal(wantMarks) {
t.Errorf("wrong marks\n got: %#v\nwant: %#v", gotMarks, wantMarks)
for _, pvm := range pvms {
if !isAWantedAttrPath(pvm.Path, wantedAttrPaths) {
t.Errorf("unexpected path\n got: %#v\n", pvm.Path)
}
if !pvm.Marks.Equal(cty.NewValueMarks(marks.Sensitive)) {
t.Errorf("wrong marks\n got: %#v\nwant: %#v", pvm.Marks, cty.NewValueMarks(marks.Sensitive))
}
}
}
wantedBarPaths := []cty.Path{
{
cty.GetAttrStep{Name: "nesting_single"},
cty.GetAttrStep{Name: "sensitive_value"},
},
cty.GetAttrPath("sensitive_value"),
cty.GetAttrPath("value"),
}
wantedBazPaths := []cty.Path{
cty.GetAttrPath("sensitive_value"),
cty.GetAttrPath("value"),
}
// Sensitive attributes (defined by the provider) are marked
// as sensitive when referenced from another resource
// "bar" references sensitive resources in "foo"
barAddr := mustResourceInstanceAddr("test_resource.bar")
barChangeSrc := plan.Changes.ResourceInstance(barAddr)
verifySensitiveValue(barChangeSrc.AfterValMarks)
verifySensitiveValues(barChangeSrc.AfterValMarks, wantedBarPaths)
bazAddr := mustResourceInstanceAddr("test_resource.baz")
bazChangeSrc := plan.Changes.ResourceInstance(bazAddr)
verifySensitiveValue(bazChangeSrc.AfterValMarks)
verifySensitiveValues(bazChangeSrc.AfterValMarks, wantedBazPaths)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
@ -12056,10 +12094,10 @@ resource "test_resource" "baz" {
}
barState := state.ResourceInstance(barAddr)
verifySensitiveValue(barState.Current.AttrSensitivePaths)
verifySensitiveValues(barState.Current.AttrSensitivePaths, wantedBarPaths)
bazState := state.ResourceInstance(bazAddr)
verifySensitiveValue(bazState.Current.AttrSensitivePaths)
verifySensitiveValues(bazState.Current.AttrSensitivePaths, wantedBazPaths)
}
func TestContext2Apply_variableSensitivityChange(t *testing.T) {
@ -12113,20 +12151,37 @@ resource "test_resource" "foo" {
fooState := state.ResourceInstance(addr)
if len(fooState.Current.AttrSensitivePaths) != 1 {
t.Fatalf("wrong number of sensitive paths, expected 1, got, %v", len(fooState.Current.AttrSensitivePaths))
}
got := fooState.Current.AttrSensitivePaths[0]
want := cty.PathValueMarks{
Path: cty.GetAttrPath("value"),
Marks: cty.NewValueMarks(marks.Sensitive),
wantedPathValueMarks := []cty.PathValueMarks{
{
Path: cty.GetAttrPath("sensitive_value"),
Marks: cty.NewValueMarks(marks.Sensitive),
},
{
Path: cty.GetAttrPath("value"),
Marks: cty.NewValueMarks(marks.Sensitive),
},
}
if !got.Equal(want) {
t.Fatalf("wrong value marks; got:\n%#v\n\nwant:\n%#v\n", got, want)
if len(fooState.Current.AttrSensitivePaths) != len(wantedPathValueMarks) {
t.Fatalf("wrong number of sensitive paths, expected %d, got, %d", len(wantedPathValueMarks), len(fooState.Current.AttrSensitivePaths))
}
m2 := testModuleInline(t, map[string]string{
for _, path := range fooState.Current.AttrSensitivePaths {
found := false
for _, wanted := range wantedPathValueMarks {
if path.Path.Equals(wanted.Path) {
found = true
if !path.Marks.Equal(wanted.Marks) {
t.Errorf("wrong marks\n got: %#v\nwant: %#v", path.Marks, wanted.Marks)
}
}
}
if !found {
t.Errorf("unexpected path\n got: %#v\n", path)
}
}
newModule := testModuleInline(t, map[string]string{
"main.tf": `
variable "sensitive_var" {
default = "hello"
@ -12138,34 +12193,31 @@ resource "test_resource" "foo" {
}`,
})
ctx2 := testContext2(t, &ContextOpts{
newCtx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
// NOTE: Prior to our refactoring to make the state an explicit argument
// of Plan, as opposed to hidden state inside Context, this test was
// calling ctx.Apply instead of ctx2.Apply and thus using the previous
// plan instead of this new plan. "Fixing" it to use the new plan seems
// to break the test, so we've preserved that oddity here by saving the
// old plan as oldPlan and essentially discarding the new plan entirely,
// but this seems rather suspicious and we should ideally figure out what
// this test was originally intending to do and make it do that.
oldPlan := plan
_, diags = ctx2.Plan(m2, state, SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)))
_, diags = newCtx.Plan(newModule, state, SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)))
assertNoErrors(t, diags)
stateWithoutSensitive, diags := ctx.Apply(oldPlan, m)
stateWithoutSensitive, diags := newCtx.Apply(plan, newModule)
assertNoErrors(t, diags)
fooState2 := stateWithoutSensitive.ResourceInstance(addr)
if len(fooState2.Current.AttrSensitivePaths) > 0 {
newFooState := stateWithoutSensitive.ResourceInstance(addr)
// The sensitive value that was previously applied should still be sensitive, but nothing else
if len(newFooState.Current.AttrSensitivePaths) != 1 {
t.Fatalf(
"wrong number of sensitive paths, expected 0, got, %v\n%s",
len(fooState2.Current.AttrSensitivePaths),
spew.Sdump(fooState2.Current.AttrSensitivePaths),
"wrong number of sensitive paths, expected 1, got, %v\n%s",
len(newFooState.Current.AttrSensitivePaths),
spew.Sdump(newFooState.Current.AttrSensitivePaths),
)
}
if !newFooState.Current.AttrSensitivePaths[0].Path.Equals(cty.GetAttrPath("sensitive_value")) {
t.Fatalf("wrong sensitive path, got %v", newFooState.Current.AttrSensitivePaths[0].Path)
}
}
func TestContext2Apply_moduleVariableOptionalAttributes(t *testing.T) {

View File

@ -742,8 +742,8 @@ func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.Sourc
// Decode all instances in the current state
instances := map[addrs.InstanceKey]cty.Value{}
pendingDestroy := d.Operation == walkDestroy
for key, is := range rs.Instances {
if is == nil || is.Current == nil {
for key, instance := range rs.Instances {
if instance == nil || instance.Current == nil {
// Assume we're dealing with an instance that hasn't been created yet.
instances[key] = cty.UnknownVal(ty)
continue
@ -766,7 +766,7 @@ func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.Sourc
// Planned resources are temporarily stored in state with empty values,
// and need to be replaced by the planned value here.
if is.Current.Status == states.ObjectPlanned {
if instance.Current.Status == states.ObjectPlanned {
if change == nil {
// If the object is in planned status then we should not get
// here, since we should have found a pending value in the plan
@ -790,17 +790,20 @@ func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.Sourc
continue
}
// If our provider schema contains sensitive values, mark those as sensitive
afterMarks := change.AfterValMarks
if schema.ContainsSensitive() {
afterMarks = append(afterMarks, schema.ValueMarks(val, nil)...)
// Now that we know that the schema contains sensitive marks,
// Combine those marks together to ensure that the value is marked correctly but not double marked
schemaMarks := schema.ValueMarks(val, nil)
afterMarks = combinePathValueMarks(afterMarks, schemaMarks)
}
instances[key] = val.MarkWithPaths(afterMarks)
continue
}
ios, err := is.Current.Decode(ty)
instanceObjectSrc, err := instance.Current.Decode(ty)
if err != nil {
// This shouldn't happen, since by the time we get here we
// should have upgraded the state data already.
@ -813,17 +816,17 @@ func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.Sourc
continue
}
val := ios.Value
val := instanceObjectSrc.Value
// If our schema contains sensitive values, mark those as sensitive.
// Since decoding the instance object can also apply sensitivity marks,
// we must remove and combine those before remarking to avoid a double-
// mark error.
if schema.ContainsSensitive() {
var marks []cty.PathValueMarks
// Now that we know that the schema contains sensitive marks,
// Combine those marks together to ensure that the value is marked correctly but not double marked
val, marks = val.UnmarkDeepWithPaths()
marks = append(marks, schema.ValueMarks(val, nil)...)
val = val.MarkWithPaths(marks)
schemaMarks := schema.ValueMarks(val, nil)
combined := combinePathValueMarks(marks, schemaMarks)
val = val.MarkWithPaths(combined)
}
instances[key] = val
}

View File

@ -12,22 +12,6 @@ import (
"github.com/zclconf/go-cty/cty"
)
// filterMarks removes any PathValueMarks from marks which cannot be applied to
// the given value. When comparing existing marks to those from a map or other
// dynamic value, we may not have values at the same paths and need to strip
// out irrelevant marks.
func filterMarks(val cty.Value, marks []cty.PathValueMarks) []cty.PathValueMarks {
var res []cty.PathValueMarks
for _, mark := range marks {
// any error here just means the path cannot apply to this value, so we
// don't need this mark for comparison.
if _, err := mark.Path.Apply(val); err == nil {
res = append(res, mark)
}
}
return res
}
// marksEqual compares 2 unordered sets of PathValue marks for equality, with
// the comparison using the cty.PathValueMarks.Equal method.
func marksEqual(a, b []cty.PathValueMarks) bool {
@ -59,10 +43,55 @@ func marksEqual(a, b []cty.PathValueMarks) bool {
return true
}
func copyMarksFromValue(dst, src cty.Value) cty.Value {
_, pvm := src.UnmarkDeepWithPaths()
if len(pvm) == 0 {
return dst
func copyPathValueMarks(marks cty.PathValueMarks) cty.PathValueMarks {
newMarks := make(cty.ValueMarks, len(marks.Marks))
result := cty.PathValueMarks{Path: marks.Path}
for k, v := range marks.Marks {
newMarks[k] = v
}
return dst.MarkWithPaths(pvm)
result.Marks = newMarks
return result
}
// combinePathValueMarks will combine the marks from two sets of marks with paths, ensuring that we don't duplicate marks
// for the same path, but instead combine the marks for the same path
// This ensures that we don't lose user marks when combining 2 different sets of marks for the same path
func combinePathValueMarks(marks []cty.PathValueMarks, other []cty.PathValueMarks) []cty.PathValueMarks {
// skip some work if we don't have any marks in either of the lists
if len(marks) == 0 {
return other
}
if len(other) == 0 {
return marks
}
combined := make([]cty.PathValueMarks, 0, len(marks))
// construct the initial set of marks
combined = append(combined, marks...)
// check if we've already inserted this by looping over and calling .Equals().
// This isn't so nice but there is no nice comparison for cty.PathValueMarks
// so we have to do it this way
for _, mark := range other {
exists := false
for i, existing := range combined {
if mark.Path.Equals(existing.Path) {
// if we found a matching path, we should combine the marks and update the existing item
dupe := copyPathValueMarks(existing)
for k, v := range mark.Marks {
dupe.Marks[k] = v
}
combined[i] = dupe
exists = true
break
}
}
// Otherwise we haven't seen this path before, so we should add it to the list
// no merging required
if !exists {
combined = append(combined, mark)
}
}
return combined
}

View File

@ -9,8 +9,9 @@ import (
"fmt"
"testing"
"github.com/opentofu/opentofu/internal/lang/marks"
"github.com/zclconf/go-cty/cty"
"github.com/opentofu/opentofu/internal/lang/marks"
)
func TestMarksEqual(t *testing.T) {
@ -108,3 +109,98 @@ func TestMarksEqual(t *testing.T) {
})
}
}
func TestCombinePathValueMarks(t *testing.T) {
paths := map[string]cty.PathValueMarks{
"a.b": {
Path: cty.Path{cty.GetAttrStep{Name: "a"}, cty.GetAttrStep{Name: "b"}},
Marks: cty.NewValueMarks(marks.Sensitive),
},
"a.c": {
Path: cty.Path{cty.GetAttrStep{Name: "a"}, cty.GetAttrStep{Name: "c"}},
Marks: cty.NewValueMarks(marks.Sensitive),
},
"[0]": {
Path: cty.Path{cty.IndexStep{Key: cty.NumberIntVal(0)}},
Marks: cty.NewValueMarks("a"),
},
"a.b<alt>": {
Path: cty.Path{cty.GetAttrStep{Name: "a"}, cty.GetAttrStep{Name: "b"}},
Marks: cty.NewValueMarks("a"),
},
}
tests := []struct {
name string
LHS []cty.PathValueMarks
RHS []cty.PathValueMarks
Want []cty.PathValueMarks
}{
{
name: "no marks",
LHS: []cty.PathValueMarks{},
RHS: []cty.PathValueMarks{},
Want: []cty.PathValueMarks{},
},
{
name: "one mark",
LHS: []cty.PathValueMarks{paths["a.b"]},
RHS: []cty.PathValueMarks{},
Want: []cty.PathValueMarks{paths["a.b"]},
},
{
name: "one overlapping mark",
LHS: []cty.PathValueMarks{paths["a.b"]},
RHS: []cty.PathValueMarks{paths["a.b"]},
Want: []cty.PathValueMarks{paths["a.b"]},
},
{
name: "one non-overlapping mark",
LHS: []cty.PathValueMarks{paths["a.b"]},
RHS: []cty.PathValueMarks{paths["a.c"]},
Want: []cty.PathValueMarks{paths["a.b"], paths["a.c"]},
},
{
name: "one overlapping and two non-overlapping marks",
LHS: []cty.PathValueMarks{paths["a.b"], paths["a.c"], paths["[0]"]},
RHS: []cty.PathValueMarks{paths["a.c"]},
Want: []cty.PathValueMarks{paths["a.b"], paths["a.c"], paths["[0]"]},
},
{
name: "one overlapping mark with different values",
LHS: []cty.PathValueMarks{
{
Path: cty.Path{cty.GetAttrStep{Name: "a"}, cty.GetAttrStep{Name: "b"}},
Marks: cty.NewValueMarks(marks.Sensitive),
},
},
RHS: []cty.PathValueMarks{
{
Path: cty.Path{cty.GetAttrStep{Name: "a"}, cty.GetAttrStep{Name: "b"}},
Marks: cty.NewValueMarks("OTHERMARK"),
},
},
Want: []cty.PathValueMarks{
{
Path: cty.Path{cty.GetAttrStep{Name: "a"}, cty.GetAttrStep{Name: "b"}},
Marks: cty.NewValueMarks(marks.Sensitive, "OTHERMARK"),
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got := combinePathValueMarks(test.LHS, test.RHS)
if len(got) != len(test.Want) {
t.Fatalf("incorrect result length\ngot: %#v\nwant: %#v", got, test.Want)
}
for i, want := range test.Want {
if !got[i].Equal(want) {
t.Errorf("incorrect result\nindex: %d\ngot: %#v\nwant: %#v", i, got[i], want)
}
}
})
}
}

View File

@ -662,9 +662,14 @@ func (n *NodeAbstractResourceInstance) refresh(ctx EvalContext, deposedKey state
return ret, diags
}
// Mark the value if necessary
if len(priorPaths) > 0 {
ret.Value = ret.Value.MarkWithPaths(priorPaths)
// Bring in the marks from the schema for the value, this will be merged with the marks from the
// previous value to preserve user-marked values, for example: someone passing a sensitive arg to a non-sensitive
// prop on a resource
marks := combinePathValueMarks(priorPaths, schema.ValueMarks(ret.Value, nil))
// we only want to mark the value if it has marks
if len(marks) > 0 {
ret.Value = ret.Value.MarkWithPaths(marks)
}
return ret, diags
@ -814,7 +819,7 @@ func (n *NodeAbstractResourceInstance) plan(
// Store the paths for the config val to re-mark after we've sent things
// over the wire.
unmarkedConfigVal, unmarkedPaths := configValIgnored.UnmarkDeepWithPaths()
unmarkedPriorVal, priorPaths := priorVal.UnmarkDeepWithPaths()
unmarkedPriorVal, _ := priorVal.UnmarkDeepWithPaths()
proposedNewVal := objchange.ProposedNew(schema, unmarkedPriorVal, unmarkedConfigVal)
@ -841,6 +846,8 @@ func (n *NodeAbstractResourceInstance) plan(
}
plannedNewVal := resp.PlannedState
// Store an unmarked version of our planned new value because the `plan` now marks properties correctly with the config marks
unmarkedPlannedNewVal, _ := plannedNewVal.UnmarkDeep()
plannedPrivate := resp.PlannedPrivate
if plannedNewVal == cty.NilVal {
@ -868,7 +875,7 @@ func (n *NodeAbstractResourceInstance) plan(
return nil, nil, keyData, diags
}
if errs := objchange.AssertPlanValid(schema, unmarkedPriorVal, unmarkedConfigVal, plannedNewVal); len(errs) > 0 {
if errs := objchange.AssertPlanValid(schema, unmarkedPriorVal, unmarkedConfigVal, unmarkedPlannedNewVal); len(errs) > 0 {
if resp.LegacyTypeSystem {
// The shimming of the old type system in the legacy SDK is not precise
// enough to pass this consistency check, so we'll give it a pass here,
@ -921,11 +928,15 @@ func (n *NodeAbstractResourceInstance) plan(
// Add the marks back to the planned new value -- this must happen after ignore changes
// have been processed
unmarkedPlannedNewVal := plannedNewVal
if len(unmarkedPaths) > 0 {
plannedNewVal = plannedNewVal.MarkWithPaths(unmarkedPaths)
marks := combinePathValueMarks(unmarkedPaths, schema.ValueMarks(plannedNewVal, nil))
if len(marks) > 0 {
plannedNewVal = plannedNewVal.MarkWithPaths(marks)
}
// The test assertion error handling above could've changed the plannedNewVal
// so we should store the unmarked version before we go ahead and re-mark it again
unmarkedPlannedNewVal, _ = plannedNewVal.UnmarkDeep()
// The provider produces a list of paths to attributes whose changes mean
// that we must replace rather than update an existing remote object.
// However, we only need to do that if the identified attributes _have_
@ -1113,16 +1124,16 @@ func (n *NodeAbstractResourceInstance) plan(
actionReason = plans.ResourceInstanceReplaceBecauseTainted
}
// If we plan to write or delete sensitive paths from state,
// this is an Update action.
//
// We need to filter out any marks which may not apply to the new planned
// value before comparison. The one case where a provider is allowed to
// return a different value from the configuration is when a config change
// is not functionally significant and the prior state can be returned. If a
// new mark was also discarded from that config change, it needs to be
// ignored here to prevent an errant update action.
if action == plans.NoOp && !marksEqual(filterMarks(plannedNewVal, unmarkedPaths), priorPaths) {
// compare the marks between the prior and the new value, there may have been a change of sensitivity
// in the new value that requires an update
_, plannedNewValMarks := plannedNewVal.UnmarkDeepWithPaths()
_, priorValMarks := priorVal.UnmarkDeepWithPaths()
marksAreEqual := marksEqual(plannedNewValMarks, priorValMarks)
// If we plan to update sensitive paths from state,
// this is an Update action instead of a NoOp.
if action == plans.NoOp && !marksAreEqual {
action = plans.Update
}

View File

@ -9,11 +9,12 @@ import (
"fmt"
"testing"
"github.com/zclconf/go-cty/cty"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/configs/configschema"
"github.com/opentofu/opentofu/internal/states"
"github.com/zclconf/go-cty/cty"
)
func TestNodeAbstractResourceInstanceProvider(t *testing.T) {

View File

@ -269,7 +269,11 @@ func (n *graphNodeImportStateSub) Execute(ctx EvalContext, op walkOperation) (di
if n.Config != nil {
// Since the import command allow import resource with incomplete configuration, we ignore diagnostics here
valueWithConfigurationSchemaMarks, _, _ := ctx.EvaluateBlock(n.Config.Config, n.Schema, nil, EvalDataForNoInstanceKey)
state.Value = copyMarksFromValue(state.Value, valueWithConfigurationSchemaMarks)
_, stateValueMarks := state.Value.UnmarkDeepWithPaths()
_, valueWithConfigurationSchemaMarksPaths := valueWithConfigurationSchemaMarks.UnmarkDeepWithPaths()
combined := combinePathValueMarks(stateValueMarks, valueWithConfigurationSchemaMarksPaths)
state.Value = state.Value.MarkWithPaths(combined)
}
diags = diags.Append(riNode.writeResourceInstanceState(ctx, state, workingState))

View File

@ -579,7 +579,12 @@ func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs.
if configDiags.HasErrors() {
return instanceRefreshState, diags
}
instanceRefreshState.Value = copyMarksFromValue(instanceRefreshState.Value, valueWithConfigurationSchemaMarks)
_, marks := instanceRefreshState.Value.UnmarkDeepWithPaths()
_, configSchemaMarks := valueWithConfigurationSchemaMarks.UnmarkDeepWithPaths()
merged := combinePathValueMarks(marks, configSchemaMarks)
instanceRefreshState.Value = instanceRefreshState.Value.MarkWithPaths(merged)
}
// If we're importing and generating config, generate it now.

View File

@ -138,7 +138,9 @@
"name": "data_file",
"provider_name": "registry.opentofu.org/hashicorp/local",
"schema_version": 0,
"sensitive_values": {},
"sensitive_values": {
"sensitive_content": true
},
"type": "local_file",
"values": {
"content_base64": null,

View File

@ -41,7 +41,9 @@
"name": "local_file",
"provider_name": "registry.opentofu.org/hashicorp/local",
"schema_version": 0,
"sensitive_values": {},
"sensitive_values": {
"sensitive_content": true
},
"type": "local_file",
"values": {
"content": "{\"hello\":\"world\"}",

View File

@ -1,6 +1,6 @@
{
"version": 4,
"terraform_version": "1.3.0",
"terraform_version": "1.8.0",
"serial": 2,
"lineage": "e2a94970-ee0e-0eb7-16a5-67e94860dc8e",
"outputs": {},
@ -23,7 +23,14 @@
"sensitive_content": null,
"source": null
},
"sensitive_attributes": [],
"sensitive_attributes": [
[
{
"type":"get_attr",
"value":"sensitive_content"
}
]
],
"private": "bnVsbA=="
}
]

View File

@ -1,6 +1,6 @@
{
"version": 4,
"terraform_version": "1.3.0",
"terraform_version": "1.8.0",
"serial": 2,
"lineage": "e2a94970-ee0e-0eb7-16a5-67e94860dc8e",
"outputs": {},
@ -23,7 +23,14 @@
"sensitive_content": null,
"source": null
},
"sensitive_attributes": [],
"sensitive_attributes": [
[
{
"type": "get_attr",
"value": "sensitive_content"
}
]
],
"private": "bnVsbA=="
}
]