Skip to content

Commit

Permalink
fix: Allow WriteOnly attributes to require replacement (#36313)
Browse files Browse the repository at this point in the history
* fix: Allow WriteOnly attributes to require replacement

* address feedback

* add test
  • Loading branch information
radeksimko authored Jan 15, 2025
1 parent 8e1d366 commit 0188be7
Show file tree
Hide file tree
Showing 6 changed files with 658 additions and 8 deletions.
37 changes: 37 additions & 0 deletions internal/configs/configschema/implied_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,29 @@ func (b *Block) ContainsSensitive() bool {
return false
}

// ContainsWriteOnly returns true if any of the attributes of the receiving
// block or any of its descendant blocks are considered write only
// based on the declarations in the schema.
//
// Blocks themselves cannot be write only as a whole -- write only is a
// per-attribute idea.
func (b *Block) ContainsWriteOnly() bool {
for _, attrS := range b.Attributes {
if attrS.WriteOnly {
return true
}
if attrS.NestedType != nil && attrS.NestedType.ContainsWriteOnly() {
return true
}
}
for _, blockS := range b.BlockTypes {
if blockS.ContainsWriteOnly() {
return true
}
}
return false
}

// ImpliedType returns the cty.Type that would result from decoding a Block's
// ImpliedType and getting the resulting AttributeType.
//
Expand Down Expand Up @@ -134,3 +157,17 @@ func (o *Object) ContainsSensitive() bool {
}
return false
}

// ContainsWriteOnly returns true if any of the attributes of the receiving
// Object are considered write only based on the declarations in the schema.
func (o *Object) ContainsWriteOnly() bool {
for _, attrS := range o.Attributes {
if attrS.WriteOnly {
return true
}
if attrS.NestedType != nil && attrS.NestedType.ContainsWriteOnly() {
return true
}
}
return false
}
158 changes: 158 additions & 0 deletions internal/configs/configschema/implied_type_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,69 @@ func TestBlockContainsSensitive(t *testing.T) {
}
})
}
}

func TestBlockContainsWriteOnly(t *testing.T) {
tests := map[string]struct {
Schema *Block
Want bool
}{
"object contains write only": {
&Block{
Attributes: map[string]*Attribute{
"wo": {WriteOnly: true},
},
},
true,
},
"no write only attrs": {
&Block{
Attributes: map[string]*Attribute{
"not_wo": {},
},
},
false,
},
"nested object contains write only": {
&Block{
Attributes: map[string]*Attribute{
"nested": {
NestedType: &Object{
Nesting: NestingSingle,
Attributes: map[string]*Attribute{
"wo": {WriteOnly: true},
},
},
},
},
},
true,
},
"nested obj, no write only attrs": {
&Block{
Attributes: map[string]*Attribute{
"nested": {
NestedType: &Object{
Nesting: NestingSingle,
Attributes: map[string]*Attribute{
"public": {},
},
},
},
},
},
false,
},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
got := test.Schema.ContainsWriteOnly()
if got != test.Want {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}

}

Expand Down Expand Up @@ -464,6 +527,101 @@ func TestObjectContainsSensitive(t *testing.T) {

}

func TestObjectContainsWriteOnly(t *testing.T) {
tests := map[string]struct {
Schema *Object
Want bool
}{
"object contains write only": {
&Object{
Attributes: map[string]*Attribute{
"wo": {WriteOnly: true},
},
},
true,
},
"no write only attrs": {
&Object{
Attributes: map[string]*Attribute{
"not_wo": {},
},
},
false,
},
"nested object contains write only": {
&Object{
Attributes: map[string]*Attribute{
"nested": {
NestedType: &Object{
Nesting: NestingSingle,
Attributes: map[string]*Attribute{
"wo": {WriteOnly: true},
},
},
},
},
},
true,
},
"nested obj, no write only attrs": {
&Object{
Attributes: map[string]*Attribute{
"nested": {
NestedType: &Object{
Nesting: NestingSingle,
Attributes: map[string]*Attribute{
"public": {},
},
},
},
},
},
false,
},
"several nested objects, one contains write only": {
&Object{
Attributes: map[string]*Attribute{
"alpha": {
NestedType: &Object{
Nesting: NestingSingle,
Attributes: map[string]*Attribute{
"not_wo": {},
},
},
},
"beta": {
NestedType: &Object{
Nesting: NestingSingle,
Attributes: map[string]*Attribute{
"wo": {WriteOnly: true},
},
},
},
"gamma": {
NestedType: &Object{
Nesting: NestingSingle,
Attributes: map[string]*Attribute{
"not_wo": {},
},
},
},
},
},
true,
},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
got := test.Schema.ContainsWriteOnly()
if got != test.Want {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}

}

// Nested attribute should return optional object attributes for decoding.
func TestObjectSpecType(t *testing.T) {
tests := map[string]struct {
Expand Down
101 changes: 101 additions & 0 deletions internal/configs/configschema/write_only.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package configschema

import (
"fmt"

"github.com/zclconf/go-cty/cty"
)

// WriteOnlyPaths returns a set of paths into the given value that
// are considered write only based on the declarations in the schema.
func (b *Block) WriteOnlyPaths(val cty.Value, basePath cty.Path) []cty.Path {
var ret []cty.Path

for name, attrS := range b.Attributes {
if attrS.WriteOnly {
attrPath := copyAndExtendPath(basePath, cty.GetAttrStep{Name: name})
ret = append(ret, attrPath)
}

if attrS.NestedType == nil || !attrS.NestedType.ContainsWriteOnly() {
continue
}

attrPath := copyAndExtendPath(basePath, cty.GetAttrStep{Name: name})
ret = append(ret, attrS.NestedType.WriteOnlyPaths(attrPath)...)
}

// Extract from nested blocks
for name, blockS := range b.BlockTypes {
// If our block doesn't contain any write only attributes, skip inspecting it
if !blockS.Block.ContainsWriteOnly() {
continue
}

if val.IsNull() {
return ret
}

blockV := val.GetAttr(name)

// Create a copy of the path, with this step added, to add to our PathValueMarks slice
blockPath := copyAndExtendPath(basePath, cty.GetAttrStep{Name: name})

switch blockS.Nesting {
case NestingSingle, NestingGroup:
ret = append(ret, blockS.Block.WriteOnlyPaths(blockV, blockPath)...)
case NestingList, NestingMap, NestingSet:
blockV, _ = blockV.Unmark() // peel off one level of marking so we can iterate

if blockV.IsNull() || !blockV.IsKnown() {
return ret
}

for it := blockV.ElementIterator(); it.Next(); {
idx, blockEV := it.Element()

blockInstancePath := copyAndExtendPath(blockPath, cty.IndexStep{Key: idx})
morePaths := blockS.Block.WriteOnlyPaths(blockEV, blockInstancePath)
ret = append(ret, morePaths...)
}
default:
panic(fmt.Sprintf("unsupported nesting mode %s", blockS.Nesting))
}
}
return ret
}

// WriteOnlyPaths returns a set of paths into the given value that
// are considered write only based on the declarations in the schema.
func (o *Object) WriteOnlyPaths(basePath cty.Path) []cty.Path {
var ret []cty.Path

for name, attrS := range o.Attributes {
// Create a path to this attribute
attrPath := copyAndExtendPath(basePath, cty.GetAttrStep{Name: name})

switch o.Nesting {
case NestingSingle, NestingGroup:
if attrS.WriteOnly {
ret = append(ret, attrPath)
} else {
// The attribute has a nested type which contains write only
// attributes, so recurse
ret = append(ret, attrS.NestedType.WriteOnlyPaths(attrPath)...)
}
case NestingList, NestingMap, NestingSet:
// If the attribute is iterable type we assume that
// it is write only in its entirety since we cannot
// construct indexes from a null value.
if attrS.WriteOnly {
ret = append(ret, attrPath)
}
default:
panic(fmt.Sprintf("unsupported nesting mode %s", attrS.NestedType.Nesting))
}
}
return ret
}
Loading

0 comments on commit 0188be7

Please sign in to comment.