Ephemeral write only attributes (#3171)

Signed-off-by: Andrei Ciobanu <andrei.ciobanu@opentofu.org>
Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
This commit is contained in:
Andrei Ciobanu
2025-08-25 13:57:11 +03:00
committed by Christian Mesh
parent cbe16d3a5d
commit 7f76707dd0
29 changed files with 2389 additions and 89 deletions

View File

@@ -274,7 +274,6 @@ func TestEphemeralWorkflowAndOutput(t *testing.T) {
if err != nil {
t.Fatalf("unexpected plan error: %s\nstderr:\n%s", err, stderr)
}
// TODO ephemeral - this "value_wo" should be shown something like (write-only attribute). This will be handled during the work on the write-only attributes.
expectedChangesOutput := `OpenTofu used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+ create
@@ -285,18 +284,21 @@ OpenTofu will perform the following actions:
# data.simple_resource.test_data2 will be read during apply
# (depends on a resource or a module with changes pending)
<= data "simple_resource" "test_data2" {
+ id = (known after apply)
+ value = "test"
+ id = (known after apply)
+ value = "test"
+ value_wo = (write-only attribute)
}
# simple_resource.test_res will be created
+ resource "simple_resource" "test_res" {
+ value = "test value"
+ value = "test value"
+ value_wo = (write-only attribute)
}
# simple_resource.test_res_second_provider will be created
+ resource "simple_resource" "test_res_second_provider" {
+ value = "just a simple resource to ensure that the second provider it's working fine"
+ value = "just a simple resource to ensure that the second provider it's working fine"
+ value_wo = (write-only attribute)
}
Plan: 2 to add, 0 to change, 0 to destroy.

View File

@@ -1,10 +1,12 @@
variable "in" {
type = string
type = string
description = "Variable that is marked as ephemeral and doesn't matter what value is given in, ephemeral or not, the value evaluated for this variable will be marked as ephemeral"
ephemeral = true
ephemeral = true
}
output "out1" {
value = var.in
ephemeral = true // NOTE: because
value = var.in
// NOTE: because this output gets its value from referencing an ephemeral variable,
// it needs to be configured as ephemeral too.
ephemeral = true
}

View File

@@ -6,6 +6,8 @@
package renderers
import (
"maps"
"slices"
"sort"
"testing"
@@ -42,6 +44,18 @@ func ValidatePrimitive(before, after interface{}, action plans.Action, replace b
}
}
func ValidateWriteOnly(action plans.Action, replace bool) ValidateDiffFunction {
return func(t *testing.T, diff computed.Diff) {
validateDiff(t, diff, action, replace)
_, ok := diff.Renderer.(*writeOnlyRenderer)
if !ok {
t.Errorf("invalid renderer type: %T", diff.Renderer)
return
}
}
}
func ValidateObject(attributes map[string]ValidateDiffFunction, action plans.Action, replace bool) ValidateDiffFunction {
return func(t *testing.T, diff computed.Diff) {
validateDiff(t, diff, action, replace)
@@ -121,6 +135,12 @@ func validateKeys[C, V any](t *testing.T, actual map[string]C, expected map[stri
if diff := cmp.Diff(actualAttributes, expectedAttributes); len(diff) > 0 {
t.Errorf("actual and expected attributes did not match: %s", diff)
}
} else {
gotKeys := slices.Sorted(maps.Keys(actual))
wantKeys := slices.Sorted(maps.Keys(expected))
if diff := cmp.Diff(wantKeys, gotKeys); len(diff) > 0 {
t.Errorf("keys not match: %s", diff)
}
}
}

View File

@@ -0,0 +1,24 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package renderers
import (
"fmt"
"github.com/opentofu/opentofu/internal/command/jsonformat/computed"
)
func WriteOnly() computed.DiffRenderer {
return &writeOnlyRenderer{}
}
type writeOnlyRenderer struct {
NoWarningsRenderer
}
func (renderer writeOnlyRenderer) RenderHuman(diff computed.Diff, _ int, opts computed.RenderHumanOpts) string {
return fmt.Sprintf("(write-only attribute)%s%s", forcesReplacement(diff.Replace, opts), nullSuffix(diff.Action, opts))
}

View File

@@ -6,7 +6,9 @@
package differ
import (
"github.com/opentofu/opentofu/internal/command/jsonformat/computed/renderers"
"github.com/opentofu/opentofu/internal/command/jsonformat/structured"
"github.com/opentofu/opentofu/internal/plans"
"github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json"
@@ -15,7 +17,20 @@ import (
"github.com/opentofu/opentofu/internal/command/jsonprovider"
)
func ComputeDiffForAttribute(change structured.Change, attribute *jsonprovider.Attribute) computed.Diff {
// ComputeDiffForAttribute generates the diff for the change.
// It handles 3 specific cases:
// - When the attribute for which the change is generated is a nested object,
// it generates the diff for each attribute
// of the nested object.
// - If the attribute is write-only, due to the fact that its changes will always be null, we want
// to return a diff with the same action as the parent's.
// If we use change.CalculateAction(), then the action will always be NoOp because of the
// which will skip from showing this in the diff.
// - If none above, it tries to generate the diff by using the specific generator for the attr type.
func ComputeDiffForAttribute(change structured.Change, attribute *jsonprovider.Attribute, parentAction plans.Action) computed.Diff {
if attribute.WriteOnly {
return computeAttributeDiffAsWriteOnly(change, parentAction)
}
if attribute.AttributeNestedType != nil {
return computeDiffForNestedAttribute(change, attribute.AttributeNestedType)
}
@@ -87,3 +102,7 @@ func unmarshalAttribute(attribute *jsonprovider.Attribute) cty.Type {
}
return ctyType
}
func computeAttributeDiffAsWriteOnly(change structured.Change, parentAction plans.Action) computed.Diff {
return asDiffWithInheritedAction(change, parentAction, renderers.WriteOnly())
}

View File

@@ -49,7 +49,11 @@ func ComputeDiffForBlock(change structured.Change, block *jsonprovider.Block) co
childValue.BeforeExplicit = false
childValue.AfterExplicit = false
childChange := ComputeDiffForAttribute(childValue, attr)
// Because we want to print also the write-only attributes, we need to pass in the parent block
// action instead of the child one.
// This is because the child action will always result in NoOp since for write-only attributes, the
// values returned will be null.
childChange := ComputeDiffForAttribute(childValue, attr, current)
if childChange.Action == plans.NoOp && childValue.Before == nil && childValue.After == nil {
// Don't record nil values at all in blocks.
continue

View File

@@ -8,6 +8,7 @@ package differ
import (
"github.com/opentofu/opentofu/internal/command/jsonformat/computed"
"github.com/opentofu/opentofu/internal/command/jsonformat/structured"
"github.com/opentofu/opentofu/internal/plans"
)
// asDiff is a helper function to abstract away some simple and common
@@ -15,3 +16,10 @@ import (
func asDiff(change structured.Change, renderer computed.DiffRenderer) computed.Diff {
return computed.NewDiff(renderer, change.CalculateAction(), change.ReplacePaths.Matches())
}
// asDiffWithInheritedAction is a specific implementation of asDiff that gets also a parentAction plans.Action.
// This is used when the given change is known to always generate a NoOp diff, but it still should be shown
// in the printed diff.
func asDiffWithInheritedAction(change structured.Change, parentAction plans.Action, renderer computed.DiffRenderer) computed.Diff {
return computed.NewDiff(renderer, parentAction, change.ReplacePaths.Matches())
}

View File

@@ -101,6 +101,71 @@ func TestValue_SimpleBlocks(t *testing.T) {
"normal_attribute": renderers.ValidatePrimitive(nil, "some value", plans.Create, false),
}, nil, nil, nil, nil, plans.Create, false),
},
"delete_with_write_only_value": {
input: structured.Change{
Before: map[string]any{},
After: nil,
BeforeSensitive: false,
AfterSensitive: false,
},
block: &jsonprovider.Block{
Attributes: map[string]*jsonprovider.Attribute{
"write_only_attribute": {
AttributeType: unmarshalType(t, cty.String),
WriteOnly: true,
},
},
BlockTypes: map[string]*jsonprovider.BlockType{
"nested_with_write_only": {
NestingMode: "single",
Block: &jsonprovider.Block{
Attributes: map[string]*jsonprovider.Attribute{
"inner_write_only": {
AttributeType: unmarshalType(t, cty.String),
WriteOnly: true,
},
},
},
},
},
},
validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{
"write_only_attribute": renderers.ValidateWriteOnly(plans.Delete, false),
}, nil, nil, nil, nil, plans.Delete, false),
},
"create_with_write_only_value": {
input: structured.Change{
Before: nil,
After: map[string]any{},
BeforeSensitive: false,
AfterSensitive: false,
},
block: &jsonprovider.Block{
Attributes: map[string]*jsonprovider.Attribute{
"write_only_attribute": {
AttributeType: unmarshalType(t, cty.String),
WriteOnly: true,
},
},
BlockTypes: map[string]*jsonprovider.BlockType{
"nested_with_write_only": {
NestingMode: "single",
Block: &jsonprovider.Block{
Attributes: map[string]*jsonprovider.Attribute{
"inner_write_only": {
AttributeType: unmarshalType(t, cty.String),
WriteOnly: true,
},
},
},
},
},
},
validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{
"write_only_attribute": renderers.ValidateWriteOnly(plans.Create, false),
}, nil, nil, nil, nil, plans.Create, false),
},
}
for name, tc := range tcs {
// Set some default values
@@ -746,17 +811,17 @@ func TestValue_ObjectAttributes(t *testing.T) {
}
if tc.validateObject != nil {
tc.validateObject(t, ComputeDiffForAttribute(tc.input, attribute))
tc.validateObject(t, ComputeDiffForAttribute(tc.input, attribute, tc.input.CalculateAction()))
return
}
if tc.validateSingleDiff != nil {
tc.validateSingleDiff(t, ComputeDiffForAttribute(tc.input, attribute))
tc.validateSingleDiff(t, ComputeDiffForAttribute(tc.input, attribute, tc.input.CalculateAction()))
return
}
validate := renderers.ValidateObject(tc.validateDiffs, tc.validateAction, tc.validateReplace)
validate(t, ComputeDiffForAttribute(tc.input, attribute))
validate(t, ComputeDiffForAttribute(tc.input, attribute, tc.input.CalculateAction()))
})
t.Run("map", func(t *testing.T) {
@@ -770,7 +835,7 @@ func TestValue_ObjectAttributes(t *testing.T) {
validate := renderers.ValidateMap(map[string]renderers.ValidateDiffFunction{
"element": tc.validateObject,
}, collectionDefaultAction, false)
validate(t, ComputeDiffForAttribute(input, attribute))
validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction()))
return
}
@@ -778,14 +843,14 @@ func TestValue_ObjectAttributes(t *testing.T) {
validate := renderers.ValidateMap(map[string]renderers.ValidateDiffFunction{
"element": tc.validateSingleDiff,
}, collectionDefaultAction, false)
validate(t, ComputeDiffForAttribute(input, attribute))
validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction()))
return
}
validate := renderers.ValidateMap(map[string]renderers.ValidateDiffFunction{
"element": renderers.ValidateObject(tc.validateDiffs, tc.validateAction, tc.validateReplace),
}, collectionDefaultAction, false)
validate(t, ComputeDiffForAttribute(input, attribute))
validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction()))
})
t.Run("list", func(t *testing.T) {
@@ -796,7 +861,7 @@ func TestValue_ObjectAttributes(t *testing.T) {
input := wrapChangeInSlice(tc.input)
if tc.validateList != nil {
tc.validateList(t, ComputeDiffForAttribute(input, attribute))
tc.validateList(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction()))
return
}
@@ -804,7 +869,7 @@ func TestValue_ObjectAttributes(t *testing.T) {
validate := renderers.ValidateList([]renderers.ValidateDiffFunction{
tc.validateObject,
}, collectionDefaultAction, false)
validate(t, ComputeDiffForAttribute(input, attribute))
validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction()))
return
}
@@ -812,14 +877,14 @@ func TestValue_ObjectAttributes(t *testing.T) {
validate := renderers.ValidateList([]renderers.ValidateDiffFunction{
tc.validateSingleDiff,
}, collectionDefaultAction, false)
validate(t, ComputeDiffForAttribute(input, attribute))
validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction()))
return
}
validate := renderers.ValidateList([]renderers.ValidateDiffFunction{
renderers.ValidateObject(tc.validateDiffs, tc.validateAction, tc.validateReplace),
}, collectionDefaultAction, false)
validate(t, ComputeDiffForAttribute(input, attribute))
validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction()))
})
t.Run("set", func(t *testing.T) {
@@ -836,7 +901,7 @@ func TestValue_ObjectAttributes(t *testing.T) {
ret = append(ret, tc.validateSetDiffs.After.Validate(renderers.ValidateObject))
return ret
}(), collectionDefaultAction, false)
validate(t, ComputeDiffForAttribute(input, attribute))
validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction()))
return
}
@@ -844,7 +909,7 @@ func TestValue_ObjectAttributes(t *testing.T) {
validate := renderers.ValidateSet([]renderers.ValidateDiffFunction{
tc.validateObject,
}, collectionDefaultAction, false)
validate(t, ComputeDiffForAttribute(input, attribute))
validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction()))
return
}
@@ -852,14 +917,14 @@ func TestValue_ObjectAttributes(t *testing.T) {
validate := renderers.ValidateSet([]renderers.ValidateDiffFunction{
tc.validateSingleDiff,
}, collectionDefaultAction, false)
validate(t, ComputeDiffForAttribute(input, attribute))
validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction()))
return
}
validate := renderers.ValidateSet([]renderers.ValidateDiffFunction{
renderers.ValidateObject(tc.validateDiffs, tc.validateAction, tc.validateReplace),
}, collectionDefaultAction, false)
validate(t, ComputeDiffForAttribute(input, attribute))
validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction()))
})
})
@@ -881,17 +946,17 @@ func TestValue_ObjectAttributes(t *testing.T) {
}
if tc.validateNestedObject != nil {
tc.validateNestedObject(t, ComputeDiffForAttribute(tc.input, attribute))
tc.validateNestedObject(t, ComputeDiffForAttribute(tc.input, attribute, tc.input.CalculateAction()))
return
}
if tc.validateSingleDiff != nil {
tc.validateSingleDiff(t, ComputeDiffForAttribute(tc.input, attribute))
tc.validateSingleDiff(t, ComputeDiffForAttribute(tc.input, attribute, tc.input.CalculateAction()))
return
}
validate := renderers.ValidateNestedObject(tc.validateDiffs, tc.validateAction, tc.validateReplace)
validate(t, ComputeDiffForAttribute(tc.input, attribute))
validate(t, ComputeDiffForAttribute(tc.input, attribute, tc.input.CalculateAction()))
})
t.Run("map", func(t *testing.T) {
@@ -916,7 +981,7 @@ func TestValue_ObjectAttributes(t *testing.T) {
validate := renderers.ValidateMap(map[string]renderers.ValidateDiffFunction{
"element": tc.validateNestedObject,
}, collectionDefaultAction, false)
validate(t, ComputeDiffForAttribute(input, attribute))
validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction()))
return
}
@@ -924,14 +989,14 @@ func TestValue_ObjectAttributes(t *testing.T) {
validate := renderers.ValidateMap(map[string]renderers.ValidateDiffFunction{
"element": tc.validateSingleDiff,
}, collectionDefaultAction, false)
validate(t, ComputeDiffForAttribute(input, attribute))
validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction()))
return
}
validate := renderers.ValidateMap(map[string]renderers.ValidateDiffFunction{
"element": renderers.ValidateNestedObject(tc.validateDiffs, tc.validateAction, tc.validateReplace),
}, collectionDefaultAction, false)
validate(t, ComputeDiffForAttribute(input, attribute))
validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction()))
})
t.Run("list", func(t *testing.T) {
@@ -956,7 +1021,7 @@ func TestValue_ObjectAttributes(t *testing.T) {
validate := renderers.ValidateNestedList([]renderers.ValidateDiffFunction{
tc.validateNestedObject,
}, collectionDefaultAction, false)
validate(t, ComputeDiffForAttribute(input, attribute))
validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction()))
return
}
@@ -964,14 +1029,14 @@ func TestValue_ObjectAttributes(t *testing.T) {
validate := renderers.ValidateNestedList([]renderers.ValidateDiffFunction{
tc.validateSingleDiff,
}, collectionDefaultAction, false)
validate(t, ComputeDiffForAttribute(input, attribute))
validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction()))
return
}
validate := renderers.ValidateNestedList([]renderers.ValidateDiffFunction{
renderers.ValidateNestedObject(tc.validateDiffs, tc.validateAction, tc.validateReplace),
}, collectionDefaultAction, false)
validate(t, ComputeDiffForAttribute(input, attribute))
validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction()))
})
t.Run("set", func(t *testing.T) {
@@ -999,7 +1064,7 @@ func TestValue_ObjectAttributes(t *testing.T) {
ret = append(ret, tc.validateSetDiffs.After.Validate(renderers.ValidateNestedObject))
return ret
}(), collectionDefaultAction, false)
validate(t, ComputeDiffForAttribute(input, attribute))
validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction()))
return
}
@@ -1007,7 +1072,7 @@ func TestValue_ObjectAttributes(t *testing.T) {
validate := renderers.ValidateSet([]renderers.ValidateDiffFunction{
tc.validateNestedObject,
}, collectionDefaultAction, false)
validate(t, ComputeDiffForAttribute(input, attribute))
validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction()))
return
}
@@ -1015,14 +1080,14 @@ func TestValue_ObjectAttributes(t *testing.T) {
validate := renderers.ValidateSet([]renderers.ValidateDiffFunction{
tc.validateSingleDiff,
}, collectionDefaultAction, false)
validate(t, ComputeDiffForAttribute(input, attribute))
validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction()))
return
}
validate := renderers.ValidateSet([]renderers.ValidateDiffFunction{
renderers.ValidateNestedObject(tc.validateDiffs, tc.validateAction, tc.validateReplace),
}, collectionDefaultAction, false)
validate(t, ComputeDiffForAttribute(input, attribute))
validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction()))
})
})
}
@@ -1840,9 +1905,9 @@ func TestValue_PrimitiveAttributes(t *testing.T) {
t.Run(name, func(t *testing.T) {
t.Run("direct", func(t *testing.T) {
tc.validateDiff(t, ComputeDiffForAttribute(tc.input, &jsonprovider.Attribute{
AttributeType: unmarshalType(t, tc.attribute),
}))
attr := &jsonprovider.Attribute{AttributeType: unmarshalType(t, tc.attribute)}
diff := ComputeDiffForAttribute(tc.input, attr, tc.input.CalculateAction())
tc.validateDiff(t, diff)
})
t.Run("map", func(t *testing.T) {
@@ -1854,7 +1919,7 @@ func TestValue_PrimitiveAttributes(t *testing.T) {
validate := renderers.ValidateMap(map[string]renderers.ValidateDiffFunction{
"element": tc.validateDiff,
}, defaultCollectionsAction, false)
validate(t, ComputeDiffForAttribute(input, attribute))
validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction()))
})
t.Run("list", func(t *testing.T) {
@@ -1865,14 +1930,14 @@ func TestValue_PrimitiveAttributes(t *testing.T) {
if tc.validateSliceDiffs != nil {
validate := renderers.ValidateList(tc.validateSliceDiffs, defaultCollectionsAction, false)
validate(t, ComputeDiffForAttribute(input, attribute))
validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction()))
return
}
validate := renderers.ValidateList([]renderers.ValidateDiffFunction{
tc.validateDiff,
}, defaultCollectionsAction, false)
validate(t, ComputeDiffForAttribute(input, attribute))
validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction()))
})
t.Run("set", func(t *testing.T) {
@@ -1883,14 +1948,14 @@ func TestValue_PrimitiveAttributes(t *testing.T) {
if tc.validateSliceDiffs != nil {
validate := renderers.ValidateSet(tc.validateSliceDiffs, defaultCollectionsAction, false)
validate(t, ComputeDiffForAttribute(input, attribute))
validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction()))
return
}
validate := renderers.ValidateSet([]renderers.ValidateDiffFunction{
tc.validateDiff,
}, defaultCollectionsAction, false)
validate(t, ComputeDiffForAttribute(input, attribute))
validate(t, ComputeDiffForAttribute(input, attribute, input.CalculateAction()))
})
})
}
@@ -2261,7 +2326,7 @@ func TestValue_CollectionAttributes(t *testing.T) {
}
t.Run(name, func(t *testing.T) {
tc.validateDiff(t, ComputeDiffForAttribute(tc.input, tc.attribute))
tc.validateDiff(t, ComputeDiffForAttribute(tc.input, tc.attribute, tc.input.CalculateAction()))
})
}
}
@@ -3005,6 +3070,404 @@ func TestSpecificCases(t *testing.T) {
}
}
func TestWriteOnly_ComputeDiffForBlock(t *testing.T) {
// NOTE: The write-only change will *always* have the after change null.
// We set specific values in this test for the after change to ensure that,
// based on the attribute's schema, we use the correct renderers for these attributes.
cases := map[string]struct {
block jsonprovider.Block
change structured.Change
validator renderers.ValidateDiffFunction
}{
// ---> Attributes with basic types
// attributes with basic types
"write_only_primitive_attribute": {
block: jsonprovider.Block{
Attributes: map[string]*jsonprovider.Attribute{
"write_only_attribute": {
AttributeType: unmarshalType(t, cty.String),
Optional: true,
WriteOnly: true,
},
},
},
change: structured.Change{
After: map[string]any{
"write_only_attribute": "dummy value", // NOTE: since this is WO attr, IRL it's nil
},
},
validator: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{
"write_only_attribute": renderers.ValidateWriteOnly(plans.Create, false),
}, nil, nil, nil, nil, plans.Create, false),
},
"write_only_list": {
block: jsonprovider.Block{
Attributes: map[string]*jsonprovider.Attribute{
"write_only_list": {
AttributeType: unmarshalType(t, cty.List(cty.Object(map[string]cty.Type{"val": cty.String}))),
Optional: true,
WriteOnly: true,
},
},
},
change: structured.Change{
After: map[string]any{
"write_only_list": []string{"val", "b"}, // NOTE: since this is WO attr, IRL it's nil
},
},
validator: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{
"write_only_list": renderers.ValidateWriteOnly(plans.Create, false),
}, nil, nil, nil, nil, plans.Create, false),
},
"write_only_set": {
block: jsonprovider.Block{
Attributes: map[string]*jsonprovider.Attribute{
"write_only_set": {
AttributeType: unmarshalType(t, cty.Set(cty.Object(map[string]cty.Type{"val": cty.String}))),
Optional: true,
WriteOnly: true,
},
},
},
change: structured.Change{
After: map[string]any{
"write_only_set": []string{"val", "b"}, // NOTE: since this is WO attr, IRL it's nil
},
},
validator: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{
"write_only_set": renderers.ValidateWriteOnly(plans.Create, false),
}, nil, nil, nil, nil, plans.Create, false),
},
"write_only_map": {
block: jsonprovider.Block{
Attributes: map[string]*jsonprovider.Attribute{
"write_only_map": {
AttributeType: unmarshalType(t, cty.Map(cty.String)),
Optional: true,
WriteOnly: true,
},
},
},
change: structured.Change{
After: map[string]any{
"write_only_map": map[string]string{"bar": "barv", "baz": "bazv"}, // NOTE: since this is WO attr, IRL it's nil
},
},
validator: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{
"write_only_map": renderers.ValidateWriteOnly(plans.Create, false),
}, nil, nil, nil, nil, plans.Create, false),
},
"write_only_object": {
block: jsonprovider.Block{
Attributes: map[string]*jsonprovider.Attribute{
"write_only_obj": {
AttributeType: unmarshalType(t, cty.Object(map[string]cty.Type{"val": cty.String})),
Optional: true,
WriteOnly: true,
},
},
},
change: structured.Change{
After: map[string]any{
"write_only_obj": map[string]string{"bar": "barv"}, // NOTE: since this is WO attr, IRL it's nil
},
},
validator: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{
"write_only_obj": renderers.ValidateWriteOnly(plans.Create, false),
}, nil, nil, nil, nil, plans.Create, false),
},
// ---> Attributes with nested types
"attribute_single_nested_contains_write_only_attribute": {
block: jsonprovider.Block{
Attributes: map[string]*jsonprovider.Attribute{
"write_only_obj": {
AttributeNestedType: &jsonprovider.NestedType{
Attributes: map[string]*jsonprovider.Attribute{
"val": {
AttributeType: unmarshalType(t, cty.Object(map[string]cty.Type{"val": cty.String})),
Optional: true,
WriteOnly: true,
},
},
NestingMode: "single",
},
},
},
},
change: structured.Change{
After: map[string]any{
"write_only_obj": map[string]any{"val": "foo"}, // NOTE: since this is WO attr, IRL it's nil
},
},
validator: renderers.ValidateBlock(
map[string]renderers.ValidateDiffFunction{
"write_only_obj": renderers.ValidateNestedObject(
map[string]renderers.ValidateDiffFunction{
"val": renderers.ValidateWriteOnly(plans.Create, false),
},
plans.Create, false),
},
nil, nil, nil, nil, plans.Create, false),
},
"attribute_map_nested_contains_write_only_attribute": {
block: jsonprovider.Block{
Attributes: map[string]*jsonprovider.Attribute{
"write_only_map": {
AttributeNestedType: &jsonprovider.NestedType{
Attributes: map[string]*jsonprovider.Attribute{
"val": {
AttributeType: unmarshalType(t, cty.Object(map[string]cty.Type{"val": cty.String})),
Optional: true,
WriteOnly: true,
},
},
NestingMode: "map",
},
},
},
},
change: structured.Change{
After: map[string]any{
"write_only_map": map[string]any{"val": "val value"}, // NOTE: since this is WO attr, IRL it's nil
},
},
validator: renderers.ValidateBlock(
map[string]renderers.ValidateDiffFunction{
"write_only_map": renderers.ValidateMap(
map[string]renderers.ValidateDiffFunction{
"val": renderers.ValidateNestedObject(
map[string]renderers.ValidateDiffFunction{
"val": renderers.ValidateWriteOnly(plans.Create, false),
},
plans.Create, false),
},
plans.Create, false),
},
nil, nil, nil, nil, plans.Create, false),
},
"attribute_list_nested_contains_write_only_attribute": {
block: jsonprovider.Block{
Attributes: map[string]*jsonprovider.Attribute{
"write_only_list": {
AttributeNestedType: &jsonprovider.NestedType{
Attributes: map[string]*jsonprovider.Attribute{
"val": {
AttributeType: unmarshalType(t, cty.Object(map[string]cty.Type{"val": cty.String})),
Optional: true,
WriteOnly: true,
},
},
NestingMode: "list",
},
},
},
},
change: structured.Change{
After: map[string]any{
"write_only_list": []any{map[string]any{"val": "foo value"}}, // NOTE: since this is WO attr, IRL it's nil
},
},
validator: renderers.ValidateBlock(
map[string]renderers.ValidateDiffFunction{
"write_only_list": renderers.ValidateNestedList(
[]renderers.ValidateDiffFunction{
renderers.ValidateNestedObject(
map[string]renderers.ValidateDiffFunction{
"val": renderers.ValidateWriteOnly(plans.Create, false),
},
plans.Create, false),
},
plans.Create, false),
},
nil, nil, nil, nil, plans.Create, false),
},
"attribute_set_nested_contains_write_only_attribute": {
block: jsonprovider.Block{
Attributes: map[string]*jsonprovider.Attribute{
"write_only_set": {
AttributeNestedType: &jsonprovider.NestedType{
Attributes: map[string]*jsonprovider.Attribute{
"val": {
AttributeType: unmarshalType(t, cty.Object(map[string]cty.Type{"val": cty.String})),
Optional: true,
WriteOnly: true,
},
},
NestingMode: "set",
},
},
},
},
change: structured.Change{
After: map[string]any{
"write_only_set": []any{map[string]any{"val": "foo value"}}, // NOTE: since this is WO attr, IRL it's nil
},
},
validator: renderers.ValidateBlock(
map[string]renderers.ValidateDiffFunction{
"write_only_set": renderers.ValidateSet(
[]renderers.ValidateDiffFunction{
renderers.ValidateNestedObject(
map[string]renderers.ValidateDiffFunction{
"val": renderers.ValidateWriteOnly(plans.Create, false),
},
plans.Create, false),
},
plans.Create, false),
},
nil, nil, nil, nil, plans.Create, false),
},
// ---> Nested blocks
"single_nested_block": {
block: jsonprovider.Block{
BlockTypes: map[string]*jsonprovider.BlockType{
"nested_block": {
NestingMode: "single",
Block: &jsonprovider.Block{
Attributes: map[string]*jsonprovider.Attribute{
"write_only_attr": {
AttributeType: unmarshalType(t, cty.String),
WriteOnly: true,
},
},
},
},
},
},
change: structured.Change{
After: map[string]any{
"nested_block": map[string]any{"write_only_attr": "foo"}, // NOTE: since this is WO attr, IRL it's nil
},
},
validator: renderers.ValidateBlock(
nil,
map[string]renderers.ValidateDiffFunction{
"nested_block": renderers.ValidateBlock(
map[string]renderers.ValidateDiffFunction{
"write_only_attr": renderers.ValidateWriteOnly(plans.Create, false),
},
nil, nil, nil, nil, plans.Create, false),
},
nil, nil, nil, plans.Create, false),
},
"map_nested_block": {
block: jsonprovider.Block{
BlockTypes: map[string]*jsonprovider.BlockType{
"nested_block": {
NestingMode: "map",
Block: &jsonprovider.Block{
Attributes: map[string]*jsonprovider.Attribute{
"write_only_attr": {
AttributeType: unmarshalType(t, cty.String),
WriteOnly: true,
},
},
},
},
},
},
change: structured.Change{
After: map[string]any{
"nested_block": map[string]any{"write_only_attr": "foo"}, // NOTE: since this is WO attr, IRL it's nil
},
},
validator: renderers.ValidateBlock(
nil, nil, nil,
map[string]map[string]renderers.ValidateDiffFunction{
"nested_block": {
"write_only_attr": renderers.ValidateBlock(
map[string]renderers.ValidateDiffFunction{
"write_only_attr": renderers.ValidateWriteOnly(plans.Create, false),
},
nil, nil, nil, nil, plans.Create, false),
},
},
nil, plans.Create, false),
},
"list_nested_block": {
block: jsonprovider.Block{
BlockTypes: map[string]*jsonprovider.BlockType{
"nested_block": {
NestingMode: "list",
Block: &jsonprovider.Block{
Attributes: map[string]*jsonprovider.Attribute{
"write_only_attr": {
AttributeType: unmarshalType(t, cty.String),
WriteOnly: true,
},
},
},
},
},
},
change: structured.Change{
After: map[string]any{
"nested_block": []any{"value"}, // NOTE: since this is WO attr, IRL it's nil
},
},
validator: renderers.ValidateBlock(
nil, nil,
map[string][]renderers.ValidateDiffFunction{
"nested_block": {
renderers.ValidateBlock(
map[string]renderers.ValidateDiffFunction{
"write_only_attr": renderers.ValidateWriteOnly(plans.Create, false),
},
nil, nil, nil, nil, plans.Create, false),
},
},
nil, nil, plans.Create, false),
},
"set_nested_block": {
block: jsonprovider.Block{
BlockTypes: map[string]*jsonprovider.BlockType{
"nested_block": {
NestingMode: "set",
Block: &jsonprovider.Block{
Attributes: map[string]*jsonprovider.Attribute{
"write_only_attr": {
AttributeType: unmarshalType(t, cty.String),
WriteOnly: true,
},
},
},
},
},
},
change: structured.Change{
After: map[string]any{
"nested_block": []any{"value"}, // NOTE: since this is WO attr, IRL it's nil
},
},
validator: renderers.ValidateBlock(
nil, nil, nil, nil,
map[string][]renderers.ValidateDiffFunction{
"nested_block": {
renderers.ValidateBlock(
map[string]renderers.ValidateDiffFunction{
"write_only_attr": renderers.ValidateWriteOnly(plans.Create, false),
},
nil, nil, nil, nil, plans.Create, false),
},
}, plans.Create, false),
},
}
for name, tt := range cases {
t.Run(name, func(t *testing.T) {
if tt.change.ReplacePaths == nil {
tt.change.ReplacePaths = &attribute_path.PathMatcher{}
}
if tt.change.RelevantAttributes == nil {
tt.change.RelevantAttributes = attribute_path.AlwaysMatcher()
}
diff := ComputeDiffForBlock(tt.change, &tt.block)
tt.validator(t, diff)
})
}
}
// unmarshalType converts a cty.Type into a json.RawMessage understood by the
// schema. It also lets the testing framework handle any errors to keep the API
// clean.

View File

@@ -17,15 +17,15 @@ import (
)
func computeAttributeDiffAsObject(change structured.Change, attributes map[string]cty.Type) computed.Diff {
attributeDiffs, action := processObject(change, attributes, func(value structured.Change, ctype cty.Type) computed.Diff {
attributeDiffs, action := processObject(change, attributes, func(value structured.Change, ctype cty.Type, _ plans.Action) computed.Diff {
return ComputeDiffForType(value, ctype)
})
return computed.NewDiff(renderers.Object(attributeDiffs), action, change.ReplacePaths.Matches())
}
func computeAttributeDiffAsNestedObject(change structured.Change, attributes map[string]*jsonprovider.Attribute) computed.Diff {
attributeDiffs, action := processObject(change, attributes, func(value structured.Change, attribute *jsonprovider.Attribute) computed.Diff {
return ComputeDiffForAttribute(value, attribute)
attributeDiffs, action := processObject(change, attributes, func(value structured.Change, attribute *jsonprovider.Attribute, parentAction plans.Action) computed.Diff {
return ComputeDiffForAttribute(value, attribute, parentAction)
})
return computed.NewDiff(renderers.NestedObject(attributeDiffs), action, change.ReplacePaths.Matches())
}
@@ -41,7 +41,7 @@ func computeAttributeDiffAsNestedObject(change structured.Change, attributes map
// Also, as it generic we cannot make this function a method on Change as you
// can't create generic methods on structs. Instead, we make this a generic
// function that receives the value as an argument.
func processObject[T any](v structured.Change, attributes map[string]T, computeDiff func(structured.Change, T) computed.Diff) (map[string]computed.Diff, plans.Action) {
func processObject[T any](v structured.Change, attributes map[string]T, computeDiff func(structured.Change, T, plans.Action) computed.Diff) (map[string]computed.Diff, plans.Action) {
attributeDiffs := make(map[string]computed.Diff)
mapValue := v.AsMap()
@@ -58,7 +58,7 @@ func processObject[T any](v structured.Change, attributes map[string]T, computeD
attributeValue.BeforeExplicit = false
attributeValue.AfterExplicit = false
attributeDiff := computeDiff(attributeValue, attribute)
attributeDiff := computeDiff(attributeValue, attribute, currentAction)
if attributeDiff.Action == plans.NoOp && attributeValue.Before == nil && attributeValue.After == nil {
// We skip attributes of objects that are null both before and
// after. We don't even count these as unchanged attributes.

View File

@@ -22,6 +22,7 @@ type Attribute struct {
Optional bool `json:"optional,omitempty"`
Computed bool `json:"computed,omitempty"`
Sensitive bool `json:"sensitive,omitempty"`
WriteOnly bool `json:"write_only,omitempty"`
}
type NestedType struct {
@@ -47,6 +48,7 @@ func marshalAttribute(attr *configschema.Attribute) *Attribute {
Computed: attr.Computed,
Sensitive: attr.Sensitive,
Deprecated: attr.Deprecated,
WriteOnly: attr.WriteOnly,
}
// we're not concerned about errors because at this point the schema has

View File

@@ -21,11 +21,13 @@ func TestMarshalAttribute(t *testing.T) {
Want *Attribute
}{
{
&configschema.Attribute{Type: cty.String, Optional: true, Computed: true},
&configschema.Attribute{Type: cty.String, Optional: true, Computed: true, Sensitive: true, WriteOnly: true},
&Attribute{
AttributeType: json.RawMessage(`"string"`),
Optional: true,
Computed: true,
Sensitive: true,
WriteOnly: true,
DescriptionKind: "plain",
},
},