Ephemeral todos handling (#3177)

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-28 14:21:59 +03:00
committed by Christian Mesh
parent ccfeb83889
commit 1bab9aff46
26 changed files with 358 additions and 91 deletions

View File

@@ -241,10 +241,7 @@ func TestPrimaryChdirOption(t *testing.T) {
}
// This test is checking the workflow of the ephemeral resources.
// Check also the configuration files for comments. The idea is that at the time of
// writing, the configuration was done in such a way to fail later when the
// marks will be introduced for ephemeral values. Therefore, this test will
// fail later and will require adjustments.
// Check also the configuration files for comments.
//
// We want to validate that the plan file, state file and the output contain
// only the things that are needed:

View File

@@ -25,7 +25,6 @@ import (
// should generally be suitable for display to an end-user anyway.
//
// This function will panic if the given value is not of an object type.
// TODO ephemeral - check that ephemeral is not going to impact this
func ObjectValueID(obj cty.Value) (k, v string) {
if obj.IsNull() || !obj.IsKnown() {
return "", ""
@@ -37,7 +36,7 @@ func ObjectValueID(obj cty.Value) (k, v string) {
case atys["id"] == cty.String:
v := obj.GetAttr("id")
if v.HasMark(marks.Sensitive) {
if v.HasMark(marks.Sensitive) || v.HasMark(marks.Ephemeral) {
break
}
v, _ = v.Unmark()
@@ -50,7 +49,7 @@ func ObjectValueID(obj cty.Value) (k, v string) {
// "name" isn't always globally unique, but if there isn't also an
// "id" then it _often_ is, in practice.
v := obj.GetAttr("name")
if v.HasMark(marks.Sensitive) {
if v.HasMark(marks.Sensitive) || v.HasMark(marks.Ephemeral) {
break
}
v, _ = v.Unmark()
@@ -83,7 +82,6 @@ func ObjectValueID(obj cty.Value) (k, v string) {
// name-extraction heuristics.
//
// This function will panic if the given value is not of an object type.
// TODO ephemeral - check how (and if) ephemeral marks should have their own logic here. Check unit tests too.
func ObjectValueName(obj cty.Value) (k, v string) {
if obj.IsNull() || !obj.IsKnown() {
return "", ""
@@ -95,7 +93,7 @@ func ObjectValueName(obj cty.Value) (k, v string) {
case atys["name"] == cty.String:
v := obj.GetAttr("name")
if v.HasMark(marks.Sensitive) {
if v.HasMark(marks.Sensitive) || v.HasMark(marks.Ephemeral) {
break
}
v, _ = v.Unmark()
@@ -106,7 +104,7 @@ func ObjectValueName(obj cty.Value) (k, v string) {
case atys["tags"].IsMapType() && atys["tags"].ElementType() == cty.String:
tags := obj.GetAttr("tags")
if tags.IsNull() || !tags.IsWhollyKnown() || tags.HasMark(marks.Sensitive) {
if tags.IsNull() || !tags.IsWhollyKnown() || tags.HasMark(marks.Sensitive) || tags.HasMark(marks.Ephemeral) {
break
}
tags, _ = tags.Unmark()
@@ -114,7 +112,7 @@ func ObjectValueName(obj cty.Value) (k, v string) {
switch {
case tags.HasIndex(cty.StringVal("name")).RawEquals(cty.True):
v := tags.Index(cty.StringVal("name"))
if v.HasMark(marks.Sensitive) {
if v.HasMark(marks.Sensitive) || v.HasMark(marks.Ephemeral) {
break
}
v, _ = v.Unmark()
@@ -125,7 +123,7 @@ func ObjectValueName(obj cty.Value) (k, v string) {
case tags.HasIndex(cty.StringVal("Name")).RawEquals(cty.True):
// AWS-style naming convention
v := tags.Index(cty.StringVal("Name"))
if v.HasMark(marks.Sensitive) {
if v.HasMark(marks.Sensitive) || v.HasMark(marks.Ephemeral) {
break
}
v, _ = v.Unmark()

View File

@@ -46,6 +46,22 @@ func TestObjectValueIDOrName(t *testing.T) {
[...]string{"", ""},
[...]string{"id", "foo-123"},
},
{
cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("foo-123").Mark(marks.Sensitive),
}),
[...]string{"", ""},
[...]string{"", ""},
[...]string{"", ""},
},
{
cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("foo-123").Mark(marks.Ephemeral),
}),
[...]string{"", ""},
[...]string{"", ""},
[...]string{"", ""},
},
{
cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("foo-123"),
@@ -71,6 +87,14 @@ func TestObjectValueIDOrName(t *testing.T) {
[...]string{"", ""},
[...]string{"", ""},
},
{
cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("awesome-bar").Mark(marks.Ephemeral),
}),
[...]string{"", ""},
[...]string{"", ""},
[...]string{"", ""},
},
{
cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("awesome-foo"),

View File

@@ -369,8 +369,9 @@ func newDiagnosticDifference(diag tfdiags.Diagnostic) *jsonplan.Change {
// we cannot generate the same diff when the values involved are marked as ephemeral.
// Therefore, for situations like this, we will return no diagnostic diff, making
// the rendering of this to skip the diff part.
// TODO ephemeral - later we can find a better solution for this, like changing the type
// of the Diagnostic.Difference so that it can hold a generic type that can do this.
// Later edit: As decided in opentofu/opentofu#3151, we decided
// on not improving this diff rendering since it will show just a hint nonetheless.
// If later will be needed, we can reconsider.
if marks.Contains(lhs, marks.Ephemeral) || marks.Contains(rhs, marks.Ephemeral) {
return nil
}

View File

@@ -55,9 +55,14 @@ func ComputeDiffForBlock(change structured.Change, block *jsonprovider.Block) co
// values returned will be null.
childChange := ComputeDiffForAttribute(childValue, attr, current)
if childChange.Action == plans.NoOp && childValue.Before == nil && childValue.After == nil {
// This validation is specifically added for `tofu show`.
// Since "current" will be NoOp during rendering the output for `tofu show`,
// we need this validation to include the write-only attributes in the output.
if !attr.WriteOnly {
// Don't record nil values at all in blocks.
continue
}
}
attributes[key] = childChange
current = collections.CompareActions(current, childChange.Action)

View File

@@ -3451,6 +3451,34 @@ func TestWriteOnly_ComputeDiffForBlock(t *testing.T) {
},
}, plans.Create, false),
},
"write-only are shown during no-op too": {
block: jsonprovider.Block{
Attributes: map[string]*jsonprovider.Attribute{
"write_only_attr": {
WriteOnly: true,
AttributeType: unmarshalType(t, cty.String),
},
"regular_attr": {
AttributeType: unmarshalType(t, cty.String),
},
},
},
change: structured.Change{
After: map[string]any{
"write_only_attr": nil,
"regular_attr": "test",
},
Before: map[string]any{
"write_only_attr": nil,
"regular_attr": "test",
},
},
validator: renderers.ValidateBlock(
map[string]renderers.ValidateDiffFunction{
"write_only_attr": renderers.ValidateWriteOnly(plans.NoOp, false),
"regular_attr": renderers.ValidatePrimitive("test", "test", plans.NoOp, false),
}, nil, nil, nil, nil, plans.NoOp, false),
},
}
for name, tt := range cases {

View File

@@ -79,7 +79,7 @@ func (plan Plan) renderHuman(renderer Renderer, mode plans.Mode, opts ...plans.Q
continue
}
if diff.change.Mode == jsonstate.EphemeralResourceMode {
// Do not render ephemeral changes. // TODO ephemeral add e2e test for this
// Do not render ephemeral changes.
continue
}

View File

@@ -65,7 +65,7 @@ var (
},
},
},
// TODO ephemeral - when implementing testing support for ephemeral resources, consider configuring ephemeral schema here
// TODO ephemeral testing support - when implementing testing support for ephemeral resources, consider configuring ephemeral schema here
}
)

View File

@@ -97,7 +97,6 @@ func (h *countHook) PostDiff(addr addrs.AbsResourceInstance, gen states.Generati
defer h.Unlock()
// We don't count anything for data resources and neither for the ephemeral ones.
// TODO ephemeral - test this after the ephemeral resources are introduced entirely
if addr.Resource.Resource.Mode == addrs.DataResourceMode || addr.Resource.Resource.Mode == addrs.EphemeralResourceMode {
return tofu.HookActionContinue, nil
}

View File

@@ -1096,12 +1096,12 @@ func (c *Config) transformOverriddenResourcesForTest(run *TestRun, file *TestFil
}
if res.Mode != overrideRes.Mode {
// TODO ephemeral - include also the ephemeral resource and the test_file.go#override_ephemeral
// TODO ephemeral testing support - include also the ephemeral resource and the test_file.go#override_ephemeral
blockName, targetMode := blockNameOverrideResource, "data"
if overrideRes.Mode == addrs.DataResourceMode {
blockName, targetMode = blockNameOverrideData, "resource"
}
//It could be a warning, but for the sake of consistent UX let's make it an error
// It could be a warning, but for the sake of consistent UX let's make it an error
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Unsupported `%v` target in `%v` block", targetMode, blockName),

View File

@@ -263,7 +263,8 @@ type TestRunOptions struct {
const (
blockNameOverrideResource = "override_resource"
blockNameOverrideData = "override_data"
//blockNameOverrideEphemeral = "override_ephemeral" // TODO ephemeral uncomment this when testing support will be added for ephemerals
// TODO ephemeral testing support - uncomment this when testing support will be added for ephemerals
// blockNameOverrideEphemeral = "override_ephemeral"
)
// OverrideResource contains information about a resource or data block to be overridden.

View File

@@ -95,7 +95,7 @@ func writeConfigAttributes(addr addrs.AbsResourceInstance, buf *strings.Builder,
if !hclsyntax.ValidIdentifier(name) {
name = string(hclwrite.TokensForValue(cty.StringVal(name)).Bytes())
}
fmt.Fprintf(buf, "%s = ", name)
_, _ = fmt.Fprintf(buf, "%s = ", name)
tok := hclwrite.TokensForValue(attrS.EmptyValue())
if _, err := tok.WriteTo(buf); err != nil {
diags = diags.Append(&hcl.Diagnostic{
@@ -113,7 +113,7 @@ func writeConfigAttributes(addr addrs.AbsResourceInstance, buf *strings.Builder,
if !hclsyntax.ValidIdentifier(name) {
name = string(hclwrite.TokensForValue(cty.StringVal(name)).Bytes())
}
fmt.Fprintf(buf, "%s = ", name)
_, _ = fmt.Fprintf(buf, "%s = ", name)
tok := hclwrite.TokensForValue(attrS.EmptyValue())
if _, err := tok.WriteTo(buf); err != nil {
diags = diags.Append(&hcl.Diagnostic{
@@ -130,7 +130,6 @@ func writeConfigAttributes(addr addrs.AbsResourceInstance, buf *strings.Builder,
return diags
}
// TODO ephemeral - check how ephemeral should be integrated in this function. Check also the unit tests
func writeConfigAttributesFromExisting(addr addrs.AbsResourceInstance, buf *strings.Builder, stateVal cty.Value, attrs map[string]*configschema.Attribute, indent int) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
if len(attrs) == 0 {
@@ -155,7 +154,7 @@ func writeConfigAttributesFromExisting(addr addrs.AbsResourceInstance, buf *stri
// Exclude computed-only attributes
if attrS.Required || attrS.Optional {
buf.WriteString(strings.Repeat(" ", indent))
fmt.Fprintf(buf, "%s = ", name)
_, _ = fmt.Fprintf(buf, "%s = ", name)
var val cty.Value
if !stateVal.IsNull() && stateVal.Type().HasAttribute(name) {
@@ -164,10 +163,10 @@ func writeConfigAttributesFromExisting(addr addrs.AbsResourceInstance, buf *stri
val = attrS.EmptyValue()
}
if attrS.Sensitive || val.HasMark(marks.Sensitive) {
buf.WriteString("null # sensitive")
_, _ = fmt.Fprintf(buf, "null # sensitive%s", writeOnlyComment(attrS, false))
} else {
if val.Type() == cty.String {
unmarked, marks := val.Unmark()
unmarked, valMarks := val.Unmark()
// SHAMELESS HACK: If we have "" for an optional value, assume
// it is actually null, due to the legacy SDK.
@@ -176,8 +175,8 @@ func writeConfigAttributesFromExisting(addr addrs.AbsResourceInstance, buf *stri
}
// re-mark the value if it was marked originally
if len(marks) > 0 {
val = unmarked.Mark(marks)
if len(valMarks) > 0 {
val = unmarked.Mark(valMarks)
}
}
@@ -192,6 +191,7 @@ func writeConfigAttributesFromExisting(addr addrs.AbsResourceInstance, buf *stri
})
continue
}
_, _ = fmt.Fprintf(buf, "%s", writeOnlyComment(attrS, true))
}
buf.WriteString("\n")
@@ -228,7 +228,7 @@ func writeConfigNestedBlock(addr addrs.AbsResourceInstance, buf *strings.Builder
switch schema.Nesting {
case configschema.NestingSingle, configschema.NestingGroup:
buf.WriteString(strings.Repeat(" ", indent))
fmt.Fprintf(buf, "%s {", name)
_, _ = fmt.Fprintf(buf, "%s {", name)
writeBlockTypeConstraint(buf, schema)
diags = diags.Append(writeConfigAttributes(addr, buf, schema.Attributes, indent+2))
diags = diags.Append(writeConfigBlocks(addr, buf, schema.BlockTypes, indent+2))
@@ -236,7 +236,7 @@ func writeConfigNestedBlock(addr addrs.AbsResourceInstance, buf *strings.Builder
return diags
case configschema.NestingList, configschema.NestingSet:
buf.WriteString(strings.Repeat(" ", indent))
fmt.Fprintf(buf, "%s {", name)
_, _ = fmt.Fprintf(buf, "%s {", name)
writeBlockTypeConstraint(buf, schema)
diags = diags.Append(writeConfigAttributes(addr, buf, schema.Attributes, indent+2))
diags = diags.Append(writeConfigBlocks(addr, buf, schema.BlockTypes, indent+2))
@@ -245,7 +245,7 @@ func writeConfigNestedBlock(addr addrs.AbsResourceInstance, buf *strings.Builder
case configschema.NestingMap:
buf.WriteString(strings.Repeat(" ", indent))
// we use an arbitrary placeholder key (block label) "key"
fmt.Fprintf(buf, "%s \"key\" {", name)
_, _ = fmt.Fprintf(buf, "%s \"key\" {", name)
writeBlockTypeConstraint(buf, schema)
diags = diags.Append(writeConfigAttributes(addr, buf, schema.Attributes, indent+2))
diags = diags.Append(writeConfigBlocks(addr, buf, schema.BlockTypes, indent+2))
@@ -262,7 +262,7 @@ func writeConfigNestedTypeAttribute(addr addrs.AbsResourceInstance, buf *strings
var diags tfdiags.Diagnostics
buf.WriteString(strings.Repeat(" ", indent))
fmt.Fprintf(buf, "%s = ", name)
_, _ = fmt.Fprintf(buf, "%s = ", name)
switch schema.NestedType.Nesting {
case configschema.NestingSingle:
@@ -326,7 +326,6 @@ func writeConfigBlocksFromExisting(addr addrs.AbsResourceInstance, buf *strings.
return diags
}
// TODO ephemeral - check how ephemeral should be integrated in this function. Check also the unit tests
func writeConfigNestedTypeAttributeFromExisting(addr addrs.AbsResourceInstance, buf *strings.Builder, name string, schema *configschema.Attribute, stateVal cty.Value, indent int) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
@@ -334,7 +333,7 @@ func writeConfigNestedTypeAttributeFromExisting(addr addrs.AbsResourceInstance,
case configschema.NestingSingle:
if schema.Sensitive || stateVal.HasMark(marks.Sensitive) {
buf.WriteString(strings.Repeat(" ", indent))
fmt.Fprintf(buf, "%s = {} # sensitive\n", name)
_, _ = fmt.Fprintf(buf, "%s = {} # sensitive%s\n", name, writeOnlyComment(schema, false))
return diags
}
@@ -350,12 +349,12 @@ func writeConfigNestedTypeAttributeFromExisting(addr addrs.AbsResourceInstance,
// There is a difference between a null object, and an object with
// no attributes.
buf.WriteString(strings.Repeat(" ", indent))
fmt.Fprintf(buf, "%s = null\n", name)
_, _ = fmt.Fprintf(buf, "%s = null%s\n", name, writeOnlyComment(schema, true))
return diags
}
buf.WriteString(strings.Repeat(" ", indent))
fmt.Fprintf(buf, "%s = {\n", name)
_, _ = fmt.Fprintf(buf, "%s = {\n", name)
diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, nestedVal, schema.NestedType.Attributes, indent+2))
buf.WriteString("}\n")
return diags
@@ -364,7 +363,7 @@ func writeConfigNestedTypeAttributeFromExisting(addr addrs.AbsResourceInstance,
if schema.Sensitive || stateVal.HasMark(marks.Sensitive) {
buf.WriteString(strings.Repeat(" ", indent))
fmt.Fprintf(buf, "%s = [] # sensitive\n", name)
_, _ = fmt.Fprintf(buf, "%s = [] # sensitive%s\n", name, writeOnlyComment(schema, false))
return diags
}
@@ -372,15 +371,14 @@ func writeConfigNestedTypeAttributeFromExisting(addr addrs.AbsResourceInstance,
if listVals == nil {
// There is a difference between an empty list and a null list
buf.WriteString(strings.Repeat(" ", indent))
fmt.Fprintf(buf, "%s = null\n", name)
_, _ = fmt.Fprintf(buf, "%s = null%s\n", name, writeOnlyComment(schema, true))
return diags
}
buf.WriteString(strings.Repeat(" ", indent))
fmt.Fprintf(buf, "%s = [\n", name)
_, _ = fmt.Fprintf(buf, "%s = [\n", name)
for i := range listVals {
buf.WriteString(strings.Repeat(" ", indent+2))
// The entire element is marked.
if listVals[i].HasMark(marks.Sensitive) {
buf.WriteString("{}, # sensitive\n")
@@ -399,7 +397,7 @@ func writeConfigNestedTypeAttributeFromExisting(addr addrs.AbsResourceInstance,
case configschema.NestingMap:
if schema.Sensitive || stateVal.HasMark(marks.Sensitive) {
buf.WriteString(strings.Repeat(" ", indent))
fmt.Fprintf(buf, "%s = {} # sensitive\n", name)
_, _ = fmt.Fprintf(buf, "%s = {} # sensitive%s\n", name, writeOnlyComment(schema, false))
return diags
}
@@ -407,7 +405,7 @@ func writeConfigNestedTypeAttributeFromExisting(addr addrs.AbsResourceInstance,
if attr.IsNull() {
// There is a difference between an empty map and a null map.
buf.WriteString(strings.Repeat(" ", indent))
fmt.Fprintf(buf, "%s = null\n", name)
_, _ = fmt.Fprintf(buf, "%s = null%s\n", name, writeOnlyComment(schema, true))
return diags
}
@@ -420,10 +418,10 @@ func writeConfigNestedTypeAttributeFromExisting(addr addrs.AbsResourceInstance,
sort.Strings(keys)
buf.WriteString(strings.Repeat(" ", indent))
fmt.Fprintf(buf, "%s = {\n", name)
_, _ = fmt.Fprintf(buf, "%s = {\n", name)
for _, key := range keys {
buf.WriteString(strings.Repeat(" ", indent+2))
fmt.Fprintf(buf, "%s = {", key)
_, _ = fmt.Fprintf(buf, "%s = {", key)
// This entire value is marked
if vals[key].HasMark(marks.Sensitive) {
@@ -446,7 +444,6 @@ func writeConfigNestedTypeAttributeFromExisting(addr addrs.AbsResourceInstance,
}
}
// TODO ephemeral - check how ephemeral should be integrated in this function. Check also the unit tests
func writeConfigNestedBlockFromExisting(addr addrs.AbsResourceInstance, buf *strings.Builder, name string, schema *configschema.NestedBlock, stateVal cty.Value, indent int) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
@@ -456,7 +453,7 @@ func writeConfigNestedBlockFromExisting(addr addrs.AbsResourceInstance, buf *str
return diags
}
buf.WriteString(strings.Repeat(" ", indent))
fmt.Fprintf(buf, "%s {", name)
_, _ = fmt.Fprintf(buf, "%s {", name)
// If the entire value is marked, don't print any nested attributes
if stateVal.HasMark(marks.Sensitive) {
@@ -471,13 +468,13 @@ func writeConfigNestedBlockFromExisting(addr addrs.AbsResourceInstance, buf *str
case configschema.NestingList, configschema.NestingSet:
if stateVal.HasMark(marks.Sensitive) {
buf.WriteString(strings.Repeat(" ", indent))
fmt.Fprintf(buf, "%s {} # sensitive\n", name)
_, _ = fmt.Fprintf(buf, "%s {} # sensitive\n", name)
return diags
}
listVals := ctyCollectionValues(stateVal)
for i := range listVals {
buf.WriteString(strings.Repeat(" ", indent))
fmt.Fprintf(buf, "%s {\n", name)
_, _ = fmt.Fprintf(buf, "%s {\n", name)
diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, listVals[i], schema.Attributes, indent+2))
diags = diags.Append(writeConfigBlocksFromExisting(addr, buf, listVals[i], schema.BlockTypes, indent+2))
buf.WriteString("}\n")
@@ -486,7 +483,7 @@ func writeConfigNestedBlockFromExisting(addr addrs.AbsResourceInstance, buf *str
case configschema.NestingMap:
// If the entire value is marked, don't print any nested attributes
if stateVal.HasMark(marks.Sensitive) {
fmt.Fprintf(buf, "%s {} # sensitive\n", name)
_, _ = fmt.Fprintf(buf, "%s {} # sensitive\n", name)
return diags
}
@@ -499,7 +496,7 @@ func writeConfigNestedBlockFromExisting(addr addrs.AbsResourceInstance, buf *str
sort.Strings(keys)
for _, key := range keys {
buf.WriteString(strings.Repeat(" ", indent))
fmt.Fprintf(buf, "%s %q {", name, key)
_, _ = fmt.Fprintf(buf, "%s %q {", name, key)
// This entire map element is marked
if vals[key].HasMark(marks.Sensitive) {
buf.WriteString("} # sensitive\n")
@@ -526,9 +523,9 @@ func writeAttrTypeConstraint(buf *strings.Builder, schema *configschema.Attribut
}
if schema.NestedType != nil {
fmt.Fprintf(buf, "%s\n", schema.NestedType.ImpliedType().FriendlyName())
_, _ = fmt.Fprintf(buf, "%s\n", schema.NestedType.ImpliedType().FriendlyName())
} else {
fmt.Fprintf(buf, "%s\n", schema.Type.FriendlyName())
_, _ = fmt.Fprintf(buf, "%s\n", schema.Type.FriendlyName())
}
}
@@ -546,15 +543,15 @@ func ctyCollectionValues(val cty.Value) []cty.Value {
return nil
}
var len int
var length int
if val.IsMarked() {
val, _ = val.Unmark()
len = val.LengthInt()
length = val.LengthInt()
} else {
len = val.LengthInt()
length = val.LengthInt()
}
ret := make([]cty.Value, 0, len)
ret := make([]cty.Value, 0, length)
for it := val.ElementIterator(); it.Next(); {
_, value := it.Element()
ret = append(ret, value)
@@ -580,7 +577,7 @@ func omitUnknowns(val cty.Value) cty.Value {
case ty.IsPrimitiveType():
return val
case ty.IsListType() || ty.IsTupleType() || ty.IsSetType():
unmarked, marks := val.Unmark()
unmarked, valMarks := val.Unmark()
var vals []cty.Value
it := unmarked.ElementIterator()
for it.Next() {
@@ -598,9 +595,9 @@ func omitUnknowns(val cty.Value) cty.Value {
// may have caused the individual elements to have different types,
// and we're doing this work to produce JSON anyway and JSON marshalling
// represents all of these sequence types as an array.
return cty.TupleVal(vals).WithMarks(marks)
return cty.TupleVal(vals).WithMarks(valMarks)
case ty.IsMapType() || ty.IsObjectType():
unmarked, marks := val.Unmark()
unmarked, valMarks := val.Unmark()
vals := make(map[string]cty.Value)
it := unmarked.ElementIterator()
for it.Next() {
@@ -614,7 +611,7 @@ func omitUnknowns(val cty.Value) cty.Value {
// may have caused the individual elements to have different types,
// and we're doing this work to produce JSON anyway and JSON marshalling
// represents both of these mapping types as an object.
return cty.ObjectVal(vals).WithMarks(marks)
return cty.ObjectVal(vals).WithMarks(valMarks)
default:
// Should never happen, since the above should cover all types
panic(fmt.Sprintf("omitUnknowns cannot handle %#v", val))
@@ -661,3 +658,13 @@ func wrapAsJSONEncodeFunctionCall(v cty.Value) (hclwrite.Tokens, error) {
return tokens, nil
}
func writeOnlyComment(attr *configschema.Attribute, startComment bool) string {
if !attr.WriteOnly {
return ""
}
if startComment {
return " # write-only"
}
return " write-only"
}

View File

@@ -27,6 +27,10 @@ func TestConfigGeneration(t *testing.T) {
Computed: computed,
}
}
writeOnlyAttr := func(attribute *configschema.Attribute) *configschema.Attribute {
attribute.WriteOnly = true
return attribute
}
tcs := map[string]struct {
schema *configschema.Block
@@ -724,6 +728,59 @@ resource "tfcoremock_simple_resource" "example" {
sensitive_number = null # sensitive
sensitive_object = null # sensitive
sensitive_string = null # sensitive
}`,
},
"simple_resource_with_write_only_and_sensitive": {
// Write-only adds a new comment so we want to check that too
schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"sensitive_string": writeOnlyAttr(sensitiveVal(cty.String, false, true, false)),
"sensitive_list": writeOnlyAttr(sensitiveVal(cty.List(cty.String), false, true, false)),
"sensitive_map": writeOnlyAttr(sensitiveVal(cty.Map(cty.String), false, true, false)),
"sensitive_object": writeOnlyAttr(sensitiveVal(cty.Object(map[string]cty.Type{}), false, true, false)),
"single_nested_attribute": {
NestedType: &configschema.Object{
Nesting: configschema.NestingSingle,
Attributes: map[string]*configschema.Attribute{
"single_nested_attribute_string": writeOnlyAttr(sensitiveVal(cty.String, false, true, false)),
},
},
Optional: true,
},
},
},
addr: addrs.AbsResourceInstance{
Module: nil,
Resource: addrs.ResourceInstance{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "tfcoremock_simple_resource",
Name: "example",
},
Key: nil,
},
},
provider: addrs.LocalProviderConfig{
LocalName: "tfcoremock",
},
value: cty.ObjectVal(map[string]cty.Value{
"sensitive_string": cty.StringVal("sensitive").Mark(marks.Sensitive),
"sensitive_list": cty.ListVal([]cty.Value{cty.StringVal("sensitive")}).Mark(marks.Sensitive),
"sensitive_map": cty.MapVal(map[string]cty.Value{"key": cty.StringVal("sensitive")}).Mark(marks.Sensitive),
"sensitive_object": cty.ObjectVal(map[string]cty.Value{}).Mark(marks.Sensitive),
"single_nested_attribute": cty.ObjectVal(map[string]cty.Value{
"single_nested_attribute_string": cty.StringVal("random"),
})}),
expected: `
resource "tfcoremock_simple_resource" "example" {
sensitive_list = null # sensitive write-only
sensitive_map = null # sensitive write-only
sensitive_object = null # sensitive write-only
sensitive_string = null # sensitive write-only
single_nested_attribute = {
single_nested_attribute_string = null # sensitive write-only
}
}`,
},
"simple_resource_with_all_sensitive_computed_values": {

View File

@@ -135,9 +135,6 @@ func performTypeAndValueChecks(expr hcl.Expression, hclCtx *hcl.EvalContext, all
}
// TODO ephemeral - check how ephemeral should impact this function. Check also the unit tests
//
// Christian: it should follow the same conventions as Sensitive and only be allowed in each.value and not in each.key.
func performValueChecks(expr hcl.Expression, hclCtx *hcl.EvalContext, allowUnknown bool, forEachVal cty.Value, typeCheckVal cty.Value, errInvalidUnknownDetail string, excludableAddr addrs.Targetable) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
ty := forEachVal.Type()
@@ -186,6 +183,18 @@ func performValueChecks(expr hcl.Expression, hclCtx *hcl.EvalContext, allowUnkno
})
resultVal = cty.NullVal(ty)
}
if forEachVal.HasMark(marks.Ephemeral) {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid for_each argument",
Detail: "Ephemeral values, or values derived from ephemeral values, cannot be used as for_each arguments. If used, the ephemeral value could be exposed as a resource instance key.",
Subject: expr.Range().Ptr(),
Expression: expr,
EvalContext: hclCtx,
Extra: DiagnosticCausedByConfidentialValues(true),
})
resultVal = cty.NullVal(ty)
}
return resultVal, diags
}

View File

@@ -56,6 +56,25 @@ func TestEvaluateForEachExpression_multi_errors(t *testing.T) {
},
},
},
"ephemeral marked list": {
hcltest.MockExprLiteral(cty.ListVal([]cty.Value{cty.StringVal("a"), cty.StringVal("a")}).Mark(marks.Ephemeral)),
[]struct {
Summary string
DetailSubstring string
CausedBySensitive bool
}{
{
"Invalid for_each argument",
"Ephemeral values, or values derived from ephemeral values, cannot be used as for_each arguments. If used, the ephemeral value could be exposed as a resource instance key.",
true,
},
{
"Invalid for_each argument",
"must be a map, or set of strings, and you have provided a value of type list",
false,
},
},
},
"marked tuple": {
hcltest.MockExprLiteral(cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.StringVal("a")}).Mark(marks.Sensitive)),
[]struct {
@@ -75,6 +94,25 @@ func TestEvaluateForEachExpression_multi_errors(t *testing.T) {
},
},
},
"ephemeral marked tuple": {
hcltest.MockExprLiteral(cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.StringVal("a")}).Mark(marks.Ephemeral)),
[]struct {
Summary string
DetailSubstring string
CausedBySensitive bool
}{
{
"Invalid for_each argument",
"Ephemeral values, or values derived from ephemeral values, cannot be used as for_each arguments. If used, the ephemeral value could be exposed as a resource instance key.",
true,
},
{
"Invalid for_each argument",
"must be a map, or set of strings, and you have provided a value of type tuple",
false,
},
},
},
"marked string": {
hcltest.MockExprLiteral(cty.StringVal("a").Mark(marks.Sensitive)),
[]struct {
@@ -94,6 +132,25 @@ func TestEvaluateForEachExpression_multi_errors(t *testing.T) {
},
},
},
"ephemeral marked string": {
hcltest.MockExprLiteral(cty.StringVal("a").Mark(marks.Ephemeral)),
[]struct {
Summary string
DetailSubstring string
CausedBySensitive bool
}{
{
"Invalid for_each argument",
"Ephemeral values, or values derived from ephemeral values, cannot be used as for_each arguments. If used, the ephemeral value could be exposed as a resource instance key.",
true,
},
{
"Invalid for_each argument",
"must be a map, or set of strings, and you have provided a value of type string",
false,
},
},
},
}
for name, test := range tests {
@@ -165,6 +222,17 @@ func TestEvaluateForEachExpressionValueTuple(t *testing.T) {
},
},
},
"ephemeral tuple": {
Value: cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.StringVal("b")}).Mark(marks.Ephemeral),
expectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "Ephemeral values, or values derived from ephemeral values, cannot be used as for_each arguments",
CausedByUnknown: false,
CausedBySensitive: true,
},
},
},
"unknown tuple": {
Value: cty.UnknownVal(cty.Tuple([]cty.Type{cty.String})),
expectedErrs: []expectedErr{
@@ -782,6 +850,77 @@ func TestEvaluateForEach(t *testing.T) {
}},
PlanReturnValue: map[string]cty.Value{},
},
"ephemeral_tuple": {
Input: cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.StringVal("b")}).Mark(marks.Ephemeral),
ValidateExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "Ephemeral values, or values derived from ephemeral values, cannot be used as for_each arguments",
CausedByUnknown: false,
CausedBySensitive: true,
},
{
Summary: "Invalid for_each argument",
Detail: "argument must be a map, or set of strings, and you have provided a value of type tuple.",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
ValidateReturnValue: cty.NullVal(cty.Tuple([]cty.Type{cty.String, cty.String})),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "Ephemeral values, or values derived from ephemeral values, cannot be used as for_each arguments",
CausedByUnknown: false,
CausedBySensitive: true,
},
{
Summary: "Invalid for_each argument",
Detail: "argument must be a map, or set of strings, and you have provided a value of type tuple.",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
PlanReturnValue: map[string]cty.Value{},
},
"ephemeral_set": {
Input: cty.SetVal([]cty.Value{cty.StringVal("a")}).Mark(marks.Ephemeral),
ValidateExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "Ephemeral values, or values derived from ephemeral values, cannot be used as for_each arguments",
CausedByUnknown: false,
CausedBySensitive: true,
}},
ValidateReturnValue: cty.NullVal(cty.Set(cty.String)),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "Ephemeral values, or values derived from ephemeral values, cannot be used as for_each arguments",
CausedByUnknown: false,
CausedBySensitive: true,
}},
PlanReturnValue: map[string]cty.Value{},
},
"ephemeral_set_elements": {
Input: cty.SetVal([]cty.Value{cty.StringVal("a").Mark(marks.Ephemeral)}),
ValidateExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "Ephemeral values, or values derived from ephemeral values, cannot be used as for_each arguments",
CausedByUnknown: false,
CausedBySensitive: true,
}},
ValidateReturnValue: cty.NullVal(cty.Set(cty.String)),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "Ephemeral values, or values derived from ephemeral values, cannot be used as for_each arguments",
CausedByUnknown: false,
CausedBySensitive: true,
}},
PlanReturnValue: map[string]cty.Value{},
},
"string": {
Input: cty.StringVal("i am definitely a set"),
ValidateExpectedErrs: []expectedErr{

View File

@@ -69,7 +69,6 @@ func assertObjectCompatible(schema *configschema.Block, planned, actual cty.Valu
return errs
}
// TODO ephemeral - should this function be aware of the ephemeral mark? This needs proper testing and investigation
func assertAttributeCompatible(plannedV, actualV cty.Value, attrS *configschema.Attribute, path cty.Path) []error {
var errs []error

View File

@@ -51,7 +51,7 @@ func ProposedNew(schema *configschema.Block, prior, config cty.Value) cty.Value
return proposedNew(schema, prior, config)
}
// PlannedDataResourceObject is similar to proposedNewBlock but tailored for
// PlannedUnknownObject is similar to proposedNewBlock but tailored for
// planning data resources in particular. Specifically, it replaces the values
// of any Computed attributes not set in the configuration with an unknown
// value, which serves as a placeholder for a value to be filled in by the
@@ -63,7 +63,16 @@ func ProposedNew(schema *configschema.Block, prior, config cty.Value) cty.Value
// passing the proposedNewBlock result into a provider's PlanResourceChange
// function, assuming a fixed implementation of PlanResourceChange that just
// fills in unknown values as needed.
func PlannedDataResourceObject(schema *configschema.Block, config cty.Value) cty.Value {
//
// This is also used for generating the planned value of an ephemeral resource when
// that is deferred. By design, ephemeral resources opening requires the configuration
// of the block to be fully known. Therefore, when there is at least one unknown
// attribute, we defer the execution until we resolve all the unknowns, and only
// then we open the ephemeral. Until then, this particular function helps with
// generating a placeholder for the ephemeral resource based on its schema.
// Ephemeral resources are not stored into the state, so every newly planned value
// is based only on the configuration and its schema.
func PlannedUnknownObject(schema *configschema.Block, config cty.Value) cty.Value {
// Our trick here is to run the proposedNewBlock logic with an
// entirely-unknown prior value. Because of cty's unknown short-circuit
// behavior, any operation on prior returns another unknown, and so
@@ -73,16 +82,6 @@ func PlannedDataResourceObject(schema *configschema.Block, config cty.Value) cty
return proposedNew(schema, prior, config)
}
// PlannedEphemeralResourceObject is exactly as PlannedDataResourceObject, but we
// want to have a different copy of it to emphasize the special handling of
// this type of resource.
// Ephemeral resources are not stored into the state, so every newly planned value
// is based only on the configuration and its schema.
func PlannedEphemeralResourceObject(schema *configschema.Block, config cty.Value) cty.Value {
prior := cty.UnknownVal(schema.ImpliedType())
return proposedNew(schema, prior, config)
}
func proposedNew(schema *configschema.Block, prior, config cty.Value) cty.Value {
if config.IsNull() || !config.IsKnown() {
// A block config should never be null at this point. The only nullable

View File

@@ -102,7 +102,6 @@ type GRPCProvider struct {
var _ providers.Interface = new(GRPCProvider)
// TODO ephemeral - double check all of the usages of this to be sure that the block.ephemeral for ephemeral resources is used accordingly.
func (p *GRPCProvider) GetProviderSchema(ctx context.Context) (resp providers.GetProviderSchemaResponse) {
logger.Trace("GRPCProvider.v6: GetProviderSchema")
p.mu.Lock()

View File

@@ -251,7 +251,7 @@ func (s simple) Close(_ context.Context) error {
func waitIfRequested(m map[string]cty.Value) {
// This is a special case that can be used together with ephemeral resources to be able to test the renewal process.
// When the "value" attribute of the resource is containing "with-renew" it will return later to allow
// When the "value_wo" attribute of the resource is containing "with-renew" it will return later to allow
// the ephemeral resource to call renew at least once. Check also OpenEphemeralResource.
if v, ok := m["value_wo"]; ok && !v.IsNull() && strings.Contains(v.AsString(), "with-renew") {
<-time.After(time.Second)

View File

@@ -242,7 +242,7 @@ func (s simple) Close(_ context.Context) error {
func waitIfRequested(m map[string]cty.Value) {
// This is a special case that can be used together with ephemeral resources to be able to test the renewal process.
// When the "value" attribute of the resource is containing "with-renew" it will return later to allow
// When the "value_wo" attribute of the resource is containing "with-renew" it will return later to allow
// the ephemeral resource to call renew at least once. Check also OpenEphemeralResource.
if v, ok := m["value_wo"]; ok && !v.IsNull() && strings.Contains(v.AsString(), "with-renew") {
<-time.After(time.Second)

View File

@@ -14,8 +14,6 @@ import (
"github.com/zclconf/go-cty/cty"
)
// TODO ephemeral - check how ephemeral marks can be integrated in this function. Don't forget about unit tests
// FormatValue formats a value in a way that resembles OpenTofu language syntax
// and uses the type conversion functions where necessary to indicate exactly
// what type it is given, so that equality test failures can be quickly
@@ -27,6 +25,9 @@ func FormatValue(v cty.Value, indent int) string {
if v.HasMark(marks.Sensitive) {
return "(sensitive value)"
}
if v.HasMark(marks.Ephemeral) {
return "(ephemeral value)"
}
if v.IsNull() {
return formatNullValue(v.Type())
}

View File

@@ -179,6 +179,10 @@ EOT_`,
cty.StringVal("a sensitive value").Mark(marks.Sensitive),
"(sensitive value)",
},
{
cty.StringVal("an ephemeral value").Mark(marks.Ephemeral),
"(ephemeral value)",
},
}
for _, test := range tests {

View File

@@ -1069,7 +1069,7 @@ func (d *evaluationStateData) GetOutput(_ context.Context, addr addrs.OutputValu
if output.Sensitive {
val = val.Mark(marks.Sensitive)
}
// TODO ephemeral - this GetOutput is used only during `tofu test` against root module outputs.
// TODO ephemeral testing support - this GetOutput is used only during `tofu test` against root module outputs.
// Therefore, since only the root module outputs can get in here, there is no reason to mark
// values with ephemeral. Reanalyse this when implementing the testing support.
// if config.Ephemeral {

View File

@@ -196,7 +196,7 @@ func TestEvaluatorGetOutputValue(t *testing.T) {
t.Errorf("wrong result %#v; want %#v", got, want)
}
// TODO ephemeral - uncomment the line with the ephemeral mark once the testing support implementation is done
// TODO ephemeral testing support - uncomment the line with the ephemeral mark once the testing support implementation is done
// want = cty.StringVal("third").Mark(marks.Ephemeral)
want = cty.StringVal("third")
got, diags = scope.Data.GetOutput(t.Context(), addrs.OutputValue{

View File

@@ -2080,7 +2080,7 @@ func (n *NodeAbstractResourceInstance) planDataSource(ctx context.Context, evalC
}
unmarkedConfigVal, configMarkPaths := configVal.UnmarkDeepWithPaths()
proposedNewVal := objchange.PlannedDataResourceObject(schema, unmarkedConfigVal)
proposedNewVal := objchange.PlannedUnknownObject(schema, unmarkedConfigVal)
proposedNewVal = proposedNewVal.MarkWithPaths(configMarkPaths)
// Apply detects that the data source will need to be read by the After
@@ -2141,7 +2141,7 @@ func (n *NodeAbstractResourceInstance) planDataSource(ctx context.Context, evalC
// If we had errors, then we can cover that up by marking the new
// state as unknown.
unmarkedConfigVal, configMarkPaths := configVal.UnmarkDeepWithPaths()
newVal = objchange.PlannedDataResourceObject(schema, unmarkedConfigVal)
newVal = objchange.PlannedUnknownObject(schema, unmarkedConfigVal)
newVal = newVal.MarkWithPaths(configMarkPaths)
// We still want to report the check as failed even if we are still
@@ -3346,7 +3346,7 @@ func (n *NodeAbstractResourceInstance) deferEphemeralResource(evalCtx EvalContex
) {
unmarkedConfigVal, configMarkPaths := configVal.UnmarkDeepWithPaths()
proposedNewVal := objchange.PlannedEphemeralResourceObject(schema, unmarkedConfigVal)
proposedNewVal := objchange.PlannedUnknownObject(schema, unmarkedConfigVal)
proposedNewVal = proposedNewVal.MarkWithPaths(configMarkPaths)
plannedChange = &plans.ResourceInstanceChange{

View File

@@ -114,17 +114,17 @@ func (p providerForTest) ReadDataSource(_ context.Context, r providers.ReadDataS
}
func (p providerForTest) OpenEphemeralResource(_ context.Context, _ providers.OpenEphemeralResourceRequest) (resp providers.OpenEphemeralResourceResponse) {
//TODO ephemeral - implement me when adding testing support
// TODO ephemeral testing support - implement me when adding testing support
panic("implement me")
}
func (p providerForTest) RenewEphemeralResource(_ context.Context, _ providers.RenewEphemeralResourceRequest) (resp providers.RenewEphemeralResourceResponse) {
//TODO ephemeral - implement me when adding testing support
// TODO ephemeral testing support - implement me when adding testing support
panic("implement me")
}
func (p providerForTest) CloseEphemeralResource(_ context.Context, _ providers.CloseEphemeralResourceRequest) (resp providers.CloseEphemeralResourceResponse) {
//TODO ephemeral - implement me when adding testing support
// TODO ephemeral testing support - implement me when adding testing support
panic("implement me")
}