Skip to content

Commit 7fadbe3

Browse files
authored
write-only attributes: internal providers should set write-only attributes to null (#36824)
* write-only attributes: internal providers should set write-only attributes to null * add changelog * fix copywrite headers
1 parent 4338b75 commit 7fadbe3

File tree

22 files changed

+686
-43
lines changed

22 files changed

+686
-43
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: BUG FIXES
2+
body: 'write-only attributes: internal providers should set write-only attributes to null'
3+
time: 2025-04-02T14:39:31.672249+02:00
4+
custom:
5+
Issue: "36824"

internal/command/test_test.go

+23-4
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,18 @@ func TestTest_Runs(t *testing.T) {
322322
expectedErr: []string{"Cannot apply non-applyable plan"},
323323
code: 1,
324324
},
325+
"write-only-attributes": {
326+
expectedOut: []string{"1 passed, 0 failed."},
327+
code: 0,
328+
},
329+
"write-only-attributes-mocked": {
330+
expectedOut: []string{"1 passed, 0 failed."},
331+
code: 0,
332+
},
333+
"write-only-attributes-overridden": {
334+
expectedOut: []string{"1 passed, 0 failed."},
335+
code: 0,
336+
},
325337
}
326338
for name, tc := range tcs {
327339
t.Run(name, func(t *testing.T) {
@@ -1618,6 +1630,7 @@ Terraform will perform the following actions:
16181630
+ destroy_fail = (known after apply)
16191631
+ id = "constant_value"
16201632
+ value = "bar"
1633+
+ write_only = (write-only attribute)
16211634
}
16221635
16231636
Plan: 1 to add, 0 to change, 0 to destroy.
@@ -1629,6 +1642,7 @@ resource "test_resource" "foo" {
16291642
destroy_fail = false
16301643
id = "constant_value"
16311644
value = "bar"
1645+
write_only = (write-only attribute)
16321646
}
16331647
16341648
main.tftest.hcl... tearing down
@@ -1951,6 +1965,7 @@ resource "test_resource" "module_resource" {
19511965
destroy_fail = false
19521966
id = "df6h8as9"
19531967
value = "start"
1968+
write_only = (write-only attribute)
19541969
}
19551970
19561971
run "initial_apply"... pass
@@ -1960,6 +1975,7 @@ resource "test_resource" "resource" {
19601975
destroy_fail = false
19611976
id = "598318e0"
19621977
value = "start"
1978+
write_only = (write-only attribute)
19631979
}
19641980
19651981
run "plan_second_example"... pass
@@ -1975,6 +1991,7 @@ Terraform will perform the following actions:
19751991
+ destroy_fail = (known after apply)
19761992
+ id = "b6a1d8cb"
19771993
+ value = "start"
1994+
+ write_only = (write-only attribute)
19781995
}
19791996
19801997
Plan: 1 to add, 0 to change, 0 to destroy.
@@ -1991,7 +2008,7 @@ Terraform will perform the following actions:
19912008
~ resource "test_resource" "resource" {
19922009
id = "598318e0"
19932010
~ value = "start" -> "update"
1994-
# (1 unchanged attribute hidden)
2011+
# (2 unchanged attributes hidden)
19952012
}
19962013
19972014
Plan: 0 to add, 1 to change, 0 to destroy.
@@ -2008,7 +2025,7 @@ Terraform will perform the following actions:
20082025
~ resource "test_resource" "module_resource" {
20092026
id = "df6h8as9"
20102027
~ value = "start" -> "update"
2011-
# (1 unchanged attribute hidden)
2028+
# (2 unchanged attributes hidden)
20122029
}
20132030
20142031
Plan: 0 to add, 1 to change, 0 to destroy.
@@ -2021,8 +2038,8 @@ Success! 5 passed, 0 failed.
20212038

20222039
actual := output.All()
20232040

2024-
if !strings.Contains(actual, expected) {
2025-
t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s", expected, actual)
2041+
if diff := cmp.Diff(expected, actual); diff != "" {
2042+
t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff)
20262043
}
20272044

20282045
if provider.ResourceCount() > 0 {
@@ -2831,6 +2848,7 @@ resource "test_resource" "resource" {
28312848
destroy_fail = false
28322849
id = "9ddca5a9"
28332850
value = (sensitive value)
2851+
write_only = (write-only attribute)
28342852
}
28352853
28362854
@@ -2845,6 +2863,7 @@ resource "test_resource" "resource" {
28452863
destroy_fail = false
28462864
id = "9ddca5a9"
28472865
value = (sensitive value)
2866+
write_only = (write-only attribute)
28482867
}
28492868
28502869
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
2+
variable "input" {
3+
type = string
4+
}
5+
6+
data "test_data_source" "datasource" {
7+
id = "resource"
8+
write_only = var.input
9+
}
10+
11+
resource "test_resource" "resource" {
12+
value = data.test_data_source.datasource.value
13+
write_only = var.input
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
2+
mock_provider "test" {
3+
mock_resource "test_resource" {
4+
defaults = {
5+
id = "resource"
6+
}
7+
}
8+
9+
mock_data "test_data_source" {
10+
defaults = {
11+
value = "hello"
12+
}
13+
}
14+
}
15+
16+
run "test" {
17+
variables {
18+
input = "input"
19+
}
20+
21+
assert {
22+
condition = data.test_data_source.datasource.value == "hello"
23+
error_message = "wrong value"
24+
}
25+
26+
assert {
27+
condition = test_resource.resource.value == "hello"
28+
error_message = "wrong value"
29+
}
30+
31+
assert {
32+
condition = test_resource.resource.id == "resource"
33+
error_message = "wrong value"
34+
}
35+
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
2+
variable "input" {
3+
type = string
4+
}
5+
6+
data "test_data_source" "datasource" {
7+
id = "resource"
8+
write_only = var.input
9+
}
10+
11+
resource "test_resource" "resource" {
12+
value = data.test_data_source.datasource.value
13+
write_only = var.input
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
2+
provider "test" {}
3+
4+
override_resource {
5+
target = test_resource.resource
6+
values = {
7+
id = "resource"
8+
}
9+
}
10+
11+
override_data {
12+
target = data.test_data_source.datasource
13+
values = {
14+
value = "hello"
15+
}
16+
}
17+
18+
run "test" {
19+
variables {
20+
input = "input"
21+
}
22+
23+
assert {
24+
condition = data.test_data_source.datasource.value == "hello"
25+
error_message = "wrong value"
26+
}
27+
28+
assert {
29+
condition = test_resource.resource.value == "hello"
30+
error_message = "wrong value"
31+
}
32+
33+
assert {
34+
condition = test_resource.resource.id == "resource"
35+
error_message = "wrong value"
36+
}
37+
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
2+
variable "input" {
3+
type = string
4+
}
5+
6+
resource "test_resource" "resource" {
7+
id = "resource"
8+
write_only = var.input
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
2+
provider "test" {}
3+
4+
run "test" {
5+
variables {
6+
input = "input"
7+
}
8+
}

internal/command/testing/test_provider.go

+17-3
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ var (
3939
"destroy_fail": {Type: cty.Bool, Optional: true, Computed: true},
4040
"create_wait_seconds": {Type: cty.Number, Optional: true},
4141
"destroy_wait_seconds": {Type: cty.Number, Optional: true},
42+
"write_only": {Type: cty.String, Optional: true, WriteOnly: true},
4243
},
4344
},
4445
},
@@ -47,8 +48,9 @@ var (
4748
"test_data_source": {
4849
Body: &configschema.Block{
4950
Attributes: map[string]*configschema.Attribute{
50-
"id": {Type: cty.String, Required: true},
51-
"value": {Type: cty.String, Computed: true},
51+
"id": {Type: cty.String, Required: true},
52+
"value": {Type: cty.String, Computed: true},
53+
"write_only": {Type: cty.String, Optional: true, WriteOnly: true},
5254

5355
// We never actually reference these values from a data
5456
// source, but we have tests that use the same cty.Value
@@ -233,12 +235,18 @@ func (provider *TestProvider) PlanResourceChange(request providers.PlanResourceC
233235
resource = cty.ObjectVal(vals)
234236
}
235237

236-
if destryFail := resource.GetAttr("destroy_fail"); !destryFail.IsKnown() || destryFail.IsNull() {
238+
if destroyFail := resource.GetAttr("destroy_fail"); !destroyFail.IsKnown() || destroyFail.IsNull() {
237239
vals := resource.AsValueMap()
238240
vals["destroy_fail"] = cty.UnknownVal(cty.Bool)
239241
resource = cty.ObjectVal(vals)
240242
}
241243

244+
if writeOnly := resource.GetAttr("write_only"); !writeOnly.IsNull() {
245+
vals := resource.AsValueMap()
246+
vals["write_only"] = cty.NullVal(cty.String)
247+
resource = cty.ObjectVal(vals)
248+
}
249+
242250
return providers.PlanResourceChangeResponse{
243251
PlannedState: resource,
244252
}
@@ -335,6 +343,12 @@ func (provider *TestProvider) ReadDataSource(request providers.ReadDataSourceReq
335343
diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "not found", fmt.Sprintf("%s does not exist", id)))
336344
}
337345

346+
if writeOnly := resource.GetAttr("write_only"); !writeOnly.IsNull() {
347+
vals := resource.AsValueMap()
348+
vals["write_only"] = cty.NullVal(cty.String)
349+
resource = cty.ObjectVal(vals)
350+
}
351+
338352
return providers.ReadDataSourceResponse{
339353
State: resource,
340354
Diagnostics: diags,

internal/lang/ephemeral/strip.go

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package ephemeral
5+
6+
import (
7+
"github.com/zclconf/go-cty/cty"
8+
9+
"github.com/hashicorp/terraform/internal/configs/configschema"
10+
)
11+
12+
// StripWriteOnlyAttributes converts all the write-only attributes in value to
13+
// null values.
14+
func StripWriteOnlyAttributes(value cty.Value, schema *configschema.Block) cty.Value {
15+
// writeOnlyTransformer never returns errors, so we don't need to detect
16+
// them here.
17+
updated, _ := cty.TransformWithTransformer(value, &writeOnlyTransformer{
18+
schema: schema,
19+
})
20+
return updated
21+
}
22+
23+
var _ cty.Transformer = (*writeOnlyTransformer)(nil)
24+
25+
type writeOnlyTransformer struct {
26+
schema *configschema.Block
27+
}
28+
29+
func (w *writeOnlyTransformer) Enter(path cty.Path, value cty.Value) (cty.Value, error) {
30+
attr := w.schema.AttributeByPath(path)
31+
if attr == nil {
32+
return value, nil
33+
}
34+
35+
if attr.WriteOnly {
36+
value, marks := value.Unmark()
37+
return cty.NullVal(value.Type()).WithMarks(marks), nil
38+
}
39+
40+
return value, nil
41+
}
42+
43+
func (w *writeOnlyTransformer) Exit(_ cty.Path, value cty.Value) (cty.Value, error) {
44+
return value, nil // no changes
45+
}

0 commit comments

Comments
 (0)