Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/v1.11/BUG FIXES-20250402-143931.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: BUG FIXES
body: 'write-only attributes: internal providers should set write-only attributes to null'
time: 2025-04-02T14:39:31.672249+02:00
custom:
Issue: "36824"
27 changes: 23 additions & 4 deletions internal/command/test_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,18 @@ func TestTest_Runs(t *testing.T) {
expectedErr: []string{"Cannot apply non-applyable plan"},
code: 1,
},
"write-only-attributes": {
expectedOut: []string{"1 passed, 0 failed."},
code: 0,
},
"write-only-attributes-mocked": {
expectedOut: []string{"1 passed, 0 failed."},
code: 0,
},
"write-only-attributes-overridden": {
expectedOut: []string{"1 passed, 0 failed."},
code: 0,
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
Expand Down Expand Up @@ -1618,6 +1630,7 @@ Terraform will perform the following actions:
+ destroy_fail = (known after apply)
+ id = "constant_value"
+ value = "bar"
+ write_only = (write-only attribute)
}

Plan: 1 to add, 0 to change, 0 to destroy.
Expand All @@ -1629,6 +1642,7 @@ resource "test_resource" "foo" {
destroy_fail = false
id = "constant_value"
value = "bar"
write_only = (write-only attribute)
}

main.tftest.hcl... tearing down
Expand Down Expand Up @@ -1951,6 +1965,7 @@ resource "test_resource" "module_resource" {
destroy_fail = false
id = "df6h8as9"
value = "start"
write_only = (write-only attribute)
}

run "initial_apply"... pass
Expand All @@ -1960,6 +1975,7 @@ resource "test_resource" "resource" {
destroy_fail = false
id = "598318e0"
value = "start"
write_only = (write-only attribute)
}

run "plan_second_example"... pass
Expand All @@ -1975,6 +1991,7 @@ Terraform will perform the following actions:
+ destroy_fail = (known after apply)
+ id = "b6a1d8cb"
+ value = "start"
+ write_only = (write-only attribute)
}

Plan: 1 to add, 0 to change, 0 to destroy.
Expand All @@ -1991,7 +2008,7 @@ Terraform will perform the following actions:
~ resource "test_resource" "resource" {
id = "598318e0"
~ value = "start" -> "update"
# (1 unchanged attribute hidden)
# (2 unchanged attributes hidden)
}

Plan: 0 to add, 1 to change, 0 to destroy.
Expand All @@ -2008,7 +2025,7 @@ Terraform will perform the following actions:
~ resource "test_resource" "module_resource" {
id = "df6h8as9"
~ value = "start" -> "update"
# (1 unchanged attribute hidden)
# (2 unchanged attributes hidden)
}

Plan: 0 to add, 1 to change, 0 to destroy.
Expand All @@ -2021,8 +2038,8 @@ Success! 5 passed, 0 failed.

actual := output.All()

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

if provider.ResourceCount() > 0 {
Expand Down Expand Up @@ -2831,6 +2848,7 @@ resource "test_resource" "resource" {
destroy_fail = false
id = "9ddca5a9"
value = (sensitive value)
write_only = (write-only attribute)
}


Expand All @@ -2845,6 +2863,7 @@ resource "test_resource" "resource" {
destroy_fail = false
id = "9ddca5a9"
value = (sensitive value)
write_only = (write-only attribute)
}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

variable "input" {
type = string
}

data "test_data_source" "datasource" {
id = "resource"
write_only = var.input
}

resource "test_resource" "resource" {
value = data.test_data_source.datasource.value
write_only = var.input
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@

mock_provider "test" {
mock_resource "test_resource" {
defaults = {
id = "resource"
}
}

mock_data "test_data_source" {
defaults = {
value = "hello"
}
}
}

run "test" {
variables {
input = "input"
}

assert {
condition = data.test_data_source.datasource.value == "hello"
error_message = "wrong value"
}

assert {
condition = test_resource.resource.value == "hello"
error_message = "wrong value"
}

assert {
condition = test_resource.resource.id == "resource"
error_message = "wrong value"
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

variable "input" {
type = string
}

data "test_data_source" "datasource" {
id = "resource"
write_only = var.input
}

resource "test_resource" "resource" {
value = data.test_data_source.datasource.value
write_only = var.input
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@

provider "test" {}

override_resource {
target = test_resource.resource
values = {
id = "resource"
}
}

override_data {
target = data.test_data_source.datasource
values = {
value = "hello"
}
}

run "test" {
variables {
input = "input"
}

assert {
condition = data.test_data_source.datasource.value == "hello"
error_message = "wrong value"
}

assert {
condition = test_resource.resource.value == "hello"
error_message = "wrong value"
}

assert {
condition = test_resource.resource.id == "resource"
error_message = "wrong value"
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

variable "input" {
type = string
}

resource "test_resource" "resource" {
id = "resource"
write_only = var.input
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

provider "test" {}

run "test" {
variables {
input = "input"
}
}
20 changes: 17 additions & 3 deletions internal/command/testing/test_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ var (
"destroy_fail": {Type: cty.Bool, Optional: true, Computed: true},
"create_wait_seconds": {Type: cty.Number, Optional: true},
"destroy_wait_seconds": {Type: cty.Number, Optional: true},
"write_only": {Type: cty.String, Optional: true, WriteOnly: true},
},
},
},
Expand All @@ -47,8 +48,9 @@ var (
"test_data_source": {
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Required: true},
"value": {Type: cty.String, Computed: true},
"id": {Type: cty.String, Required: true},
"value": {Type: cty.String, Computed: true},
"write_only": {Type: cty.String, Optional: true, WriteOnly: true},

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

if destryFail := resource.GetAttr("destroy_fail"); !destryFail.IsKnown() || destryFail.IsNull() {
if destroyFail := resource.GetAttr("destroy_fail"); !destroyFail.IsKnown() || destroyFail.IsNull() {
vals := resource.AsValueMap()
vals["destroy_fail"] = cty.UnknownVal(cty.Bool)
resource = cty.ObjectVal(vals)
}

if writeOnly := resource.GetAttr("write_only"); !writeOnly.IsNull() {
vals := resource.AsValueMap()
vals["write_only"] = cty.NullVal(cty.String)
resource = cty.ObjectVal(vals)
}

return providers.PlanResourceChangeResponse{
PlannedState: resource,
}
Expand Down Expand Up @@ -335,6 +343,12 @@ func (provider *TestProvider) ReadDataSource(request providers.ReadDataSourceReq
diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "not found", fmt.Sprintf("%s does not exist", id)))
}

if writeOnly := resource.GetAttr("write_only"); !writeOnly.IsNull() {
vals := resource.AsValueMap()
vals["write_only"] = cty.NullVal(cty.String)
resource = cty.ObjectVal(vals)
}

return providers.ReadDataSourceResponse{
State: resource,
Diagnostics: diags,
Expand Down
42 changes: 42 additions & 0 deletions internal/lang/ephemeral/strip.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package ephemeral

import (
"github.com/zclconf/go-cty/cty"

"github.com/hashicorp/terraform/internal/configs/configschema"
)

// StripWriteOnlyAttributes converts all the write-only attributes in value to
// null values.
func StripWriteOnlyAttributes(value cty.Value, schema *configschema.Block) cty.Value {
// writeOnlyTransformer never returns errors, so we don't need to detect
// them here.
updated, _ := cty.TransformWithTransformer(value, &writeOnlyTransformer{
schema: schema,
})
return updated
}

var _ cty.Transformer = (*writeOnlyTransformer)(nil)

type writeOnlyTransformer struct {
schema *configschema.Block
}

func (w *writeOnlyTransformer) Enter(path cty.Path, value cty.Value) (cty.Value, error) {
attr := w.schema.AttributeByPath(path)
if attr == nil {
return value, nil
}

if attr.WriteOnly {
value, marks := value.Unmark()
return cty.NullVal(value.Type()).WithMarks(marks), nil
}

return value, nil
}

func (w *writeOnlyTransformer) Exit(_ cty.Path, value cty.Value) (cty.Value, error) {
return value, nil // no changes
}
Loading
Loading