mirror of https://github.com/hashicorp/terraform
Backport of write-only attributes: internal providers should set write-only attributes to null into v1.12 (#36827)
* backport of commitpull/36830/head343ed327e1* backport of commit0fd22e0693* backport of commit77c1cfa69e--------- Co-authored-by: Liam Cervante <liam.cervante@hashicorp.com>
parent
d6b311acf4
commit
63bb06abd0
@ -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"
|
||||
@ -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
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
|
||||
variable "input" {
|
||||
type = string
|
||||
}
|
||||
|
||||
resource "test_resource" "resource" {
|
||||
id = "resource"
|
||||
write_only = var.input
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
|
||||
provider "test" {}
|
||||
|
||||
run "test" {
|
||||
variables {
|
||||
input = "input"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
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
|
||||
}
|
||||
@ -0,0 +1,193 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package ephemeral
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/zclconf/go-cty-debug/ctydebug"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/configs/configschema"
|
||||
"github.com/hashicorp/terraform/internal/lang/marks"
|
||||
)
|
||||
|
||||
func TestStripWriteOnlyAttributes(t *testing.T) {
|
||||
tcs := map[string]struct {
|
||||
val cty.Value
|
||||
schema *configschema.Block
|
||||
want cty.Value
|
||||
}{
|
||||
"primitive": {
|
||||
val: cty.ObjectVal(map[string]cty.Value{
|
||||
"value": cty.StringVal("value"),
|
||||
}),
|
||||
schema: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"value": {
|
||||
Type: cty.String,
|
||||
WriteOnly: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: cty.ObjectVal(map[string]cty.Value{
|
||||
"value": cty.NullVal(cty.String),
|
||||
}),
|
||||
},
|
||||
"complex": {
|
||||
val: cty.ObjectVal(map[string]cty.Value{
|
||||
"value": cty.ObjectVal(map[string]cty.Value{
|
||||
"value": cty.StringVal("value"),
|
||||
}),
|
||||
}),
|
||||
schema: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"value": {
|
||||
NestedType: &configschema.Object{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"value": {
|
||||
Type: cty.String,
|
||||
},
|
||||
},
|
||||
Nesting: configschema.NestingSingle,
|
||||
},
|
||||
WriteOnly: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: cty.ObjectVal(map[string]cty.Value{
|
||||
"value": cty.NullVal(cty.Object(map[string]cty.Type{
|
||||
"value": cty.String,
|
||||
})),
|
||||
}),
|
||||
},
|
||||
"nested in object": {
|
||||
val: cty.ObjectVal(map[string]cty.Value{
|
||||
"value": cty.ObjectVal(map[string]cty.Value{
|
||||
"value": cty.StringVal("value"),
|
||||
}),
|
||||
}),
|
||||
schema: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"value": {
|
||||
NestedType: &configschema.Object{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"value": {
|
||||
Type: cty.String,
|
||||
WriteOnly: true,
|
||||
},
|
||||
},
|
||||
Nesting: configschema.NestingSingle,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: cty.ObjectVal(map[string]cty.Value{
|
||||
"value": cty.ObjectVal(map[string]cty.Value{
|
||||
"value": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
"nested in list": {
|
||||
val: cty.ObjectVal(map[string]cty.Value{
|
||||
"value": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"value": cty.StringVal("value"),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"value": cty.StringVal("value"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
schema: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"value": {
|
||||
NestedType: &configschema.Object{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"value": {
|
||||
Type: cty.String,
|
||||
WriteOnly: true,
|
||||
},
|
||||
},
|
||||
Nesting: configschema.NestingList,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: cty.ObjectVal(map[string]cty.Value{
|
||||
"value": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"value": cty.NullVal(cty.String),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"value": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
"nested in map": {
|
||||
val: cty.ObjectVal(map[string]cty.Value{
|
||||
"value": cty.MapVal(map[string]cty.Value{
|
||||
"one": cty.ObjectVal(map[string]cty.Value{
|
||||
"value": cty.StringVal("value"),
|
||||
}),
|
||||
"two": cty.ObjectVal(map[string]cty.Value{
|
||||
"value": cty.StringVal("value"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
schema: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"value": {
|
||||
NestedType: &configschema.Object{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"value": {
|
||||
Type: cty.String,
|
||||
WriteOnly: true,
|
||||
},
|
||||
},
|
||||
Nesting: configschema.NestingMap,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: cty.ObjectVal(map[string]cty.Value{
|
||||
"value": cty.MapVal(map[string]cty.Value{
|
||||
"one": cty.ObjectVal(map[string]cty.Value{
|
||||
"value": cty.NullVal(cty.String),
|
||||
}),
|
||||
"two": cty.ObjectVal(map[string]cty.Value{
|
||||
"value": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
"preserves marks": {
|
||||
val: cty.ObjectVal(map[string]cty.Value{
|
||||
"value": cty.StringVal("value"),
|
||||
}).Mark(marks.Sensitive),
|
||||
schema: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"value": {
|
||||
Type: cty.String,
|
||||
WriteOnly: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: cty.ObjectVal(map[string]cty.Value{
|
||||
"value": cty.NullVal(cty.String),
|
||||
}).Mark(marks.Sensitive),
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tcs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := StripWriteOnlyAttributes(tc.val, tc.schema)
|
||||
if diff := cmp.Diff(got, tc.want, ctydebug.CmpOptions); len(diff) > 0 {
|
||||
t.Errorf("got diff:\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
testing = {
|
||||
source = "hashicorp/testing"
|
||||
version = "0.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "datasource_id" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "resource_id" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "write_only_input" {
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
data "testing_write_only_data_source" "data" {
|
||||
id = var.datasource_id
|
||||
write_only = var.write_only_input
|
||||
}
|
||||
|
||||
resource "testing_write_only_resource" "data" {
|
||||
id = var.resource_id
|
||||
value = data.testing_write_only_data_source.data.value
|
||||
write_only = var.write_only_input
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
required_providers {
|
||||
testing = {
|
||||
source = "hashicorp/testing"
|
||||
version = "0.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
variable "providers" {
|
||||
type = set(string)
|
||||
}
|
||||
|
||||
provider "testing" "main" {
|
||||
for_each = var.providers
|
||||
}
|
||||
|
||||
component "main" {
|
||||
source = "./"
|
||||
|
||||
providers = {
|
||||
testing = provider.testing.main["single"]
|
||||
}
|
||||
|
||||
inputs = {
|
||||
datasource_id = "datasource"
|
||||
resource_id = "resource"
|
||||
write_only_input = "secret"
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue