mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-23 07:33:32 -06:00
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:
parent
bb1176d3a6
commit
12d9380982
@ -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
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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))
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
|
@ -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\"}",
|
||||
|
@ -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=="
|
||||
}
|
||||
]
|
||||
|
@ -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=="
|
||||
}
|
||||
]
|
||||
|
Loading…
Reference in New Issue
Block a user