-
Notifications
You must be signed in to change notification settings - Fork 9.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #36031 from hashicorp/TF-18617
ephemeral: support write-only attributes
- Loading branch information
Showing
29 changed files
with
2,506 additions
and
1,334 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
|
||
package ephemeral | ||
|
||
import ( | ||
"github.com/hashicorp/terraform/internal/lang/marks" | ||
"github.com/zclconf/go-cty/cty" | ||
) | ||
|
||
// RemoveEphemeralValues takes a value that possibly contains ephemeral | ||
// values and returns an equal value without ephemeral values. If an attribute contains | ||
// an ephemeral value it will be set to null. | ||
func RemoveEphemeralValues(value cty.Value) cty.Value { | ||
// We currently have no error case, so we can ignore the error | ||
val, _ := cty.Transform(value, func(p cty.Path, v cty.Value) (cty.Value, error) { | ||
_, givenMarks := v.Unmark() | ||
if _, isEphemeral := givenMarks[marks.Ephemeral]; isEphemeral { | ||
// We'll strip the ephemeral mark but retain any other marks | ||
// that might be present on the input. | ||
delete(givenMarks, marks.Ephemeral) | ||
if !v.IsKnown() { | ||
// If the source value is unknown then we must leave it | ||
// unknown because its final type might be more precise | ||
// than the associated type constraint and returning a | ||
// typed null could therefore over-promise on what the | ||
// final result type will be. | ||
// We're deliberately constructing a fresh unknown value | ||
// here, rather than returning the one we were given, | ||
// because we need to discard any refinements that the | ||
// unknown value might be carrying that definitely won't | ||
// be honored when we force the final result to be null. | ||
return cty.UnknownVal(v.Type()).WithMarks(givenMarks), nil | ||
} | ||
return cty.NullVal(v.Type()).WithMarks(givenMarks), nil | ||
} | ||
return v, nil | ||
}) | ||
return val | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
|
||
package ephemeral | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/hashicorp/terraform/internal/lang/marks" | ||
"github.com/zclconf/go-cty/cty" | ||
) | ||
|
||
func TestEphemeral_removeEphemeralValues(t *testing.T) { | ||
for name, tc := range map[string]struct { | ||
input cty.Value | ||
want cty.Value | ||
}{ | ||
"empty case": { | ||
input: cty.NullVal(cty.DynamicPseudoType), | ||
want: cty.NullVal(cty.DynamicPseudoType), | ||
}, | ||
"ephemeral marks case": { | ||
input: cty.ObjectVal(map[string]cty.Value{ | ||
"ephemeral": cty.StringVal("ephemeral_value").Mark(marks.Ephemeral), | ||
"normal": cty.StringVal("normal_value"), | ||
}), | ||
want: cty.ObjectVal(map[string]cty.Value{ | ||
"ephemeral": cty.NullVal(cty.String), | ||
"normal": cty.StringVal("normal_value"), | ||
}), | ||
}, | ||
"sensitive marks case": { | ||
input: cty.ObjectVal(map[string]cty.Value{ | ||
"sensitive": cty.StringVal("sensitive_value").Mark(marks.Sensitive), | ||
"normal": cty.StringVal("normal_value"), | ||
}), | ||
want: cty.ObjectVal(map[string]cty.Value{ | ||
"sensitive": cty.StringVal("sensitive_value").Mark(marks.Sensitive), | ||
"normal": cty.StringVal("normal_value"), | ||
}), | ||
}, | ||
"sensitive and ephemeral marks case": { | ||
input: cty.ObjectVal(map[string]cty.Value{ | ||
"sensitive_and_ephemeral": cty.StringVal("sensitive_and_ephemeral_value").Mark(marks.Sensitive).Mark(marks.Ephemeral), | ||
"normal": cty.StringVal("normal_value"), | ||
}), | ||
want: cty.ObjectVal(map[string]cty.Value{ | ||
"sensitive_and_ephemeral": cty.NullVal(cty.String).Mark(marks.Sensitive), | ||
"normal": cty.StringVal("normal_value"), | ||
}), | ||
}, | ||
} { | ||
t.Run(name, func(t *testing.T) { | ||
got := RemoveEphemeralValues(tc.input) | ||
|
||
if !got.RawEquals(tc.want) { | ||
t.Errorf("got %#v, want %#v", got, tc.want) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
|
||
package ephemeral | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/hashicorp/terraform/internal/addrs" | ||
"github.com/hashicorp/terraform/internal/configs/configschema" | ||
"github.com/hashicorp/terraform/internal/tfdiags" | ||
"github.com/zclconf/go-cty/cty" | ||
) | ||
|
||
func ValidateWriteOnlyAttributes(newVal cty.Value, schema *configschema.Block, provider addrs.AbsProviderConfig, addr addrs.AbsResourceInstance) (diags tfdiags.Diagnostics) { | ||
if writeOnlyPaths := NonNullWriteOnlyPaths(newVal, schema, nil); len(writeOnlyPaths) != 0 { | ||
for _, p := range writeOnlyPaths { | ||
diags = diags.Append(tfdiags.Sourceless( | ||
tfdiags.Error, | ||
"Write-only attribute set", | ||
fmt.Sprintf( | ||
"Provider %q returned a value for the write-only attribute \"%s%s\". Write-only attributes cannot be read back from the provider. This is a bug in the provider, which should be reported in the provider's own issue tracker.", | ||
provider.String(), addr.String(), tfdiags.FormatCtyPath(p), | ||
), | ||
)) | ||
} | ||
} | ||
return diags | ||
} | ||
|
||
// NonNullWriteOnlyPaths returns a list of paths to attributes that are write-only | ||
// and non-null in the given value. | ||
func NonNullWriteOnlyPaths(val cty.Value, schema *configschema.Block, p cty.Path) (paths []cty.Path) { | ||
if schema == nil { | ||
return paths | ||
} | ||
|
||
for name, attr := range schema.Attributes { | ||
attrPath := append(p, cty.GetAttrStep{Name: name}) | ||
attrVal, _ := attrPath.Apply(val) | ||
if attr.WriteOnly && !attrVal.IsNull() { | ||
paths = append(paths, attrPath) | ||
} | ||
} | ||
|
||
for name, blockS := range schema.BlockTypes { | ||
blockPath := append(p, cty.GetAttrStep{Name: name}) | ||
x := NonNullWriteOnlyPaths(val, &blockS.Block, blockPath) | ||
paths = append(paths, x...) | ||
} | ||
|
||
return paths | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
|
||
package ephemeral | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/hashicorp/terraform/internal/configs/configschema" | ||
"github.com/zclconf/go-cty/cty" | ||
) | ||
|
||
func TestNonNullWriteOnlyPaths(t *testing.T) { | ||
for name, tc := range map[string]struct { | ||
val cty.Value | ||
schema *configschema.Block | ||
|
||
expectedPaths []cty.Path | ||
}{ | ||
"no write-only attributes": { | ||
val: cty.ObjectVal(map[string]cty.Value{ | ||
"id": cty.StringVal("i-abc123"), | ||
}), | ||
schema: &configschema.Block{ | ||
Attributes: map[string]*configschema.Attribute{ | ||
"id": { | ||
Type: cty.String, | ||
}, | ||
}, | ||
}, | ||
}, | ||
|
||
"write-only attribute with null value": { | ||
val: cty.ObjectVal(map[string]cty.Value{ | ||
"id": cty.NullVal(cty.String), | ||
}), | ||
schema: &configschema.Block{ | ||
Attributes: map[string]*configschema.Attribute{ | ||
"id": { | ||
Type: cty.String, | ||
Optional: true, | ||
WriteOnly: true, | ||
}, | ||
}, | ||
}, | ||
}, | ||
|
||
"write-only attribute with non-null value": { | ||
val: cty.ObjectVal(map[string]cty.Value{ | ||
"valid": cty.NullVal(cty.String), | ||
"id": cty.StringVal("i-abc123"), | ||
}), | ||
schema: &configschema.Block{ | ||
Attributes: map[string]*configschema.Attribute{ | ||
"valid": { | ||
Type: cty.String, | ||
Optional: true, | ||
WriteOnly: true, | ||
}, | ||
"id": { | ||
Type: cty.String, | ||
Optional: true, | ||
WriteOnly: true, | ||
}, | ||
}, | ||
}, | ||
expectedPaths: []cty.Path{cty.GetAttrPath("id")}, | ||
}, | ||
|
||
"write-only attributes in blocks": { | ||
val: cty.ObjectVal(map[string]cty.Value{ | ||
"foo": cty.ObjectVal(map[string]cty.Value{ | ||
"valid-write-only": cty.NullVal(cty.String), | ||
"valid": cty.StringVal("valid"), | ||
"id": cty.StringVal("i-abc123"), | ||
"bar": cty.ObjectVal(map[string]cty.Value{ | ||
"valid-write-only": cty.NullVal(cty.String), | ||
"valid": cty.StringVal("valid"), | ||
"id": cty.StringVal("i-abc123"), | ||
}), | ||
}), | ||
}), | ||
schema: &configschema.Block{ | ||
BlockTypes: map[string]*configschema.NestedBlock{ | ||
"foo": { | ||
Block: configschema.Block{ | ||
Attributes: map[string]*configschema.Attribute{ | ||
"valid-write-only": { | ||
Type: cty.String, | ||
Optional: true, | ||
WriteOnly: true, | ||
}, | ||
"valid": { | ||
Type: cty.String, | ||
Optional: true, | ||
}, | ||
"id": { | ||
Type: cty.String, | ||
Optional: true, | ||
WriteOnly: true, | ||
}, | ||
}, | ||
BlockTypes: map[string]*configschema.NestedBlock{ | ||
"bar": { | ||
Block: configschema.Block{ | ||
Attributes: map[string]*configschema.Attribute{ | ||
"valid-write-only": { | ||
Type: cty.String, | ||
Optional: true, | ||
WriteOnly: true, | ||
}, | ||
"valid": { | ||
Type: cty.String, | ||
Optional: true, | ||
}, | ||
"id": { | ||
Type: cty.String, | ||
Optional: true, | ||
WriteOnly: true, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
expectedPaths: []cty.Path{cty.GetAttrPath("foo").GetAttr("id"), cty.GetAttrPath("foo").GetAttr("bar").GetAttr("id")}, | ||
}, | ||
} { | ||
t.Run(name, func(t *testing.T) { | ||
paths := NonNullWriteOnlyPaths(tc.val, tc.schema, nil) | ||
|
||
if len(paths) != len(tc.expectedPaths) { | ||
t.Fatalf("expected %d write-only paths, got %d", len(tc.expectedPaths), len(paths)) | ||
} | ||
|
||
for i, path := range paths { | ||
if !path.Equals(tc.expectedPaths[i]) { | ||
t.Fatalf("expected path %#v, got %#v", tc.expectedPaths[i], path) | ||
} | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.