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. // 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 // Check also the configuration files for comments.
// 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.
// //
// We want to validate that the plan file, state file and the output contain // We want to validate that the plan file, state file and the output contain
// only the things that are needed: // only the things that are needed:

View File

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

View File

@@ -46,6 +46,22 @@ func TestObjectValueIDOrName(t *testing.T) {
[...]string{"", ""}, [...]string{"", ""},
[...]string{"id", "foo-123"}, [...]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{ cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("foo-123"), "id": cty.StringVal("foo-123"),
@@ -71,6 +87,14 @@ func TestObjectValueIDOrName(t *testing.T) {
[...]string{"", ""}, [...]string{"", ""},
[...]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{ cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("awesome-foo"), "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. // 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 // Therefore, for situations like this, we will return no diagnostic diff, making
// the rendering of this to skip the diff part. // the rendering of this to skip the diff part.
// TODO ephemeral - later we can find a better solution for this, like changing the type // Later edit: As decided in opentofu/opentofu#3151, we decided
// of the Diagnostic.Difference so that it can hold a generic type that can do this. // 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) { if marks.Contains(lhs, marks.Ephemeral) || marks.Contains(rhs, marks.Ephemeral) {
return nil return nil
} }

View File

@@ -55,8 +55,13 @@ func ComputeDiffForBlock(change structured.Change, block *jsonprovider.Block) co
// values returned will be null. // values returned will be null.
childChange := ComputeDiffForAttribute(childValue, attr, current) childChange := ComputeDiffForAttribute(childValue, attr, current)
if childChange.Action == plans.NoOp && childValue.Before == nil && childValue.After == nil { if childChange.Action == plans.NoOp && childValue.Before == nil && childValue.After == nil {
// Don't record nil values at all in blocks. // This validation is specifically added for `tofu show`.
continue // 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 attributes[key] = childChange

View File

@@ -3451,6 +3451,34 @@ func TestWriteOnly_ComputeDiffForBlock(t *testing.T) {
}, },
}, plans.Create, false), }, 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 { for name, tt := range cases {

View File

@@ -79,7 +79,7 @@ func (plan Plan) renderHuman(renderer Renderer, mode plans.Mode, opts ...plans.Q
continue continue
} }
if diff.change.Mode == jsonstate.EphemeralResourceMode { if diff.change.Mode == jsonstate.EphemeralResourceMode {
// Do not render ephemeral changes. // TODO ephemeral add e2e test for this // Do not render ephemeral changes.
continue 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() defer h.Unlock()
// We don't count anything for data resources and neither for the ephemeral ones. // 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 { if addr.Resource.Resource.Mode == addrs.DataResourceMode || addr.Resource.Resource.Mode == addrs.EphemeralResourceMode {
return tofu.HookActionContinue, nil return tofu.HookActionContinue, nil
} }

View File

@@ -1096,12 +1096,12 @@ func (c *Config) transformOverriddenResourcesForTest(run *TestRun, file *TestFil
} }
if res.Mode != overrideRes.Mode { 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" blockName, targetMode := blockNameOverrideResource, "data"
if overrideRes.Mode == addrs.DataResourceMode { if overrideRes.Mode == addrs.DataResourceMode {
blockName, targetMode = blockNameOverrideData, "resource" 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{ diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError, Severity: hcl.DiagError,
Summary: fmt.Sprintf("Unsupported `%v` target in `%v` block", targetMode, blockName), Summary: fmt.Sprintf("Unsupported `%v` target in `%v` block", targetMode, blockName),

View File

@@ -263,7 +263,8 @@ type TestRunOptions struct {
const ( const (
blockNameOverrideResource = "override_resource" blockNameOverrideResource = "override_resource"
blockNameOverrideData = "override_data" 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. // 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) { if !hclsyntax.ValidIdentifier(name) {
name = string(hclwrite.TokensForValue(cty.StringVal(name)).Bytes()) name = string(hclwrite.TokensForValue(cty.StringVal(name)).Bytes())
} }
fmt.Fprintf(buf, "%s = ", name) _, _ = fmt.Fprintf(buf, "%s = ", name)
tok := hclwrite.TokensForValue(attrS.EmptyValue()) tok := hclwrite.TokensForValue(attrS.EmptyValue())
if _, err := tok.WriteTo(buf); err != nil { if _, err := tok.WriteTo(buf); err != nil {
diags = diags.Append(&hcl.Diagnostic{ diags = diags.Append(&hcl.Diagnostic{
@@ -113,7 +113,7 @@ func writeConfigAttributes(addr addrs.AbsResourceInstance, buf *strings.Builder,
if !hclsyntax.ValidIdentifier(name) { if !hclsyntax.ValidIdentifier(name) {
name = string(hclwrite.TokensForValue(cty.StringVal(name)).Bytes()) name = string(hclwrite.TokensForValue(cty.StringVal(name)).Bytes())
} }
fmt.Fprintf(buf, "%s = ", name) _, _ = fmt.Fprintf(buf, "%s = ", name)
tok := hclwrite.TokensForValue(attrS.EmptyValue()) tok := hclwrite.TokensForValue(attrS.EmptyValue())
if _, err := tok.WriteTo(buf); err != nil { if _, err := tok.WriteTo(buf); err != nil {
diags = diags.Append(&hcl.Diagnostic{ diags = diags.Append(&hcl.Diagnostic{
@@ -130,7 +130,6 @@ func writeConfigAttributes(addr addrs.AbsResourceInstance, buf *strings.Builder,
return diags 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 { func writeConfigAttributesFromExisting(addr addrs.AbsResourceInstance, buf *strings.Builder, stateVal cty.Value, attrs map[string]*configschema.Attribute, indent int) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics var diags tfdiags.Diagnostics
if len(attrs) == 0 { if len(attrs) == 0 {
@@ -155,7 +154,7 @@ func writeConfigAttributesFromExisting(addr addrs.AbsResourceInstance, buf *stri
// Exclude computed-only attributes // Exclude computed-only attributes
if attrS.Required || attrS.Optional { if attrS.Required || attrS.Optional {
buf.WriteString(strings.Repeat(" ", indent)) buf.WriteString(strings.Repeat(" ", indent))
fmt.Fprintf(buf, "%s = ", name) _, _ = fmt.Fprintf(buf, "%s = ", name)
var val cty.Value var val cty.Value
if !stateVal.IsNull() && stateVal.Type().HasAttribute(name) { if !stateVal.IsNull() && stateVal.Type().HasAttribute(name) {
@@ -164,10 +163,10 @@ func writeConfigAttributesFromExisting(addr addrs.AbsResourceInstance, buf *stri
val = attrS.EmptyValue() val = attrS.EmptyValue()
} }
if attrS.Sensitive || val.HasMark(marks.Sensitive) { if attrS.Sensitive || val.HasMark(marks.Sensitive) {
buf.WriteString("null # sensitive") _, _ = fmt.Fprintf(buf, "null # sensitive%s", writeOnlyComment(attrS, false))
} else { } else {
if val.Type() == cty.String { if val.Type() == cty.String {
unmarked, marks := val.Unmark() unmarked, valMarks := val.Unmark()
// SHAMELESS HACK: If we have "" for an optional value, assume // SHAMELESS HACK: If we have "" for an optional value, assume
// it is actually null, due to the legacy SDK. // 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 // re-mark the value if it was marked originally
if len(marks) > 0 { if len(valMarks) > 0 {
val = unmarked.Mark(marks) val = unmarked.Mark(valMarks)
} }
} }
@@ -192,6 +191,7 @@ func writeConfigAttributesFromExisting(addr addrs.AbsResourceInstance, buf *stri
}) })
continue continue
} }
_, _ = fmt.Fprintf(buf, "%s", writeOnlyComment(attrS, true))
} }
buf.WriteString("\n") buf.WriteString("\n")
@@ -228,7 +228,7 @@ func writeConfigNestedBlock(addr addrs.AbsResourceInstance, buf *strings.Builder
switch schema.Nesting { switch schema.Nesting {
case configschema.NestingSingle, configschema.NestingGroup: case configschema.NestingSingle, configschema.NestingGroup:
buf.WriteString(strings.Repeat(" ", indent)) buf.WriteString(strings.Repeat(" ", indent))
fmt.Fprintf(buf, "%s {", name) _, _ = fmt.Fprintf(buf, "%s {", name)
writeBlockTypeConstraint(buf, schema) writeBlockTypeConstraint(buf, schema)
diags = diags.Append(writeConfigAttributes(addr, buf, schema.Attributes, indent+2)) diags = diags.Append(writeConfigAttributes(addr, buf, schema.Attributes, indent+2))
diags = diags.Append(writeConfigBlocks(addr, buf, schema.BlockTypes, 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 return diags
case configschema.NestingList, configschema.NestingSet: case configschema.NestingList, configschema.NestingSet:
buf.WriteString(strings.Repeat(" ", indent)) buf.WriteString(strings.Repeat(" ", indent))
fmt.Fprintf(buf, "%s {", name) _, _ = fmt.Fprintf(buf, "%s {", name)
writeBlockTypeConstraint(buf, schema) writeBlockTypeConstraint(buf, schema)
diags = diags.Append(writeConfigAttributes(addr, buf, schema.Attributes, indent+2)) diags = diags.Append(writeConfigAttributes(addr, buf, schema.Attributes, indent+2))
diags = diags.Append(writeConfigBlocks(addr, buf, schema.BlockTypes, 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: case configschema.NestingMap:
buf.WriteString(strings.Repeat(" ", indent)) buf.WriteString(strings.Repeat(" ", indent))
// we use an arbitrary placeholder key (block label) "key" // we use an arbitrary placeholder key (block label) "key"
fmt.Fprintf(buf, "%s \"key\" {", name) _, _ = fmt.Fprintf(buf, "%s \"key\" {", name)
writeBlockTypeConstraint(buf, schema) writeBlockTypeConstraint(buf, schema)
diags = diags.Append(writeConfigAttributes(addr, buf, schema.Attributes, indent+2)) diags = diags.Append(writeConfigAttributes(addr, buf, schema.Attributes, indent+2))
diags = diags.Append(writeConfigBlocks(addr, buf, schema.BlockTypes, 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 var diags tfdiags.Diagnostics
buf.WriteString(strings.Repeat(" ", indent)) buf.WriteString(strings.Repeat(" ", indent))
fmt.Fprintf(buf, "%s = ", name) _, _ = fmt.Fprintf(buf, "%s = ", name)
switch schema.NestedType.Nesting { switch schema.NestedType.Nesting {
case configschema.NestingSingle: case configschema.NestingSingle:
@@ -326,7 +326,6 @@ func writeConfigBlocksFromExisting(addr addrs.AbsResourceInstance, buf *strings.
return diags 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 { func writeConfigNestedTypeAttributeFromExisting(addr addrs.AbsResourceInstance, buf *strings.Builder, name string, schema *configschema.Attribute, stateVal cty.Value, indent int) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics var diags tfdiags.Diagnostics
@@ -334,7 +333,7 @@ func writeConfigNestedTypeAttributeFromExisting(addr addrs.AbsResourceInstance,
case configschema.NestingSingle: case configschema.NestingSingle:
if schema.Sensitive || stateVal.HasMark(marks.Sensitive) { if schema.Sensitive || stateVal.HasMark(marks.Sensitive) {
buf.WriteString(strings.Repeat(" ", indent)) 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 return diags
} }
@@ -350,12 +349,12 @@ func writeConfigNestedTypeAttributeFromExisting(addr addrs.AbsResourceInstance,
// There is a difference between a null object, and an object with // There is a difference between a null object, and an object with
// no attributes. // no attributes.
buf.WriteString(strings.Repeat(" ", indent)) 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 return diags
} }
buf.WriteString(strings.Repeat(" ", indent)) 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)) diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, nestedVal, schema.NestedType.Attributes, indent+2))
buf.WriteString("}\n") buf.WriteString("}\n")
return diags return diags
@@ -364,7 +363,7 @@ func writeConfigNestedTypeAttributeFromExisting(addr addrs.AbsResourceInstance,
if schema.Sensitive || stateVal.HasMark(marks.Sensitive) { if schema.Sensitive || stateVal.HasMark(marks.Sensitive) {
buf.WriteString(strings.Repeat(" ", indent)) 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 return diags
} }
@@ -372,15 +371,14 @@ func writeConfigNestedTypeAttributeFromExisting(addr addrs.AbsResourceInstance,
if listVals == nil { if listVals == nil {
// There is a difference between an empty list and a null list // There is a difference between an empty list and a null list
buf.WriteString(strings.Repeat(" ", indent)) 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 return diags
} }
buf.WriteString(strings.Repeat(" ", indent)) buf.WriteString(strings.Repeat(" ", indent))
fmt.Fprintf(buf, "%s = [\n", name) _, _ = fmt.Fprintf(buf, "%s = [\n", name)
for i := range listVals { for i := range listVals {
buf.WriteString(strings.Repeat(" ", indent+2)) buf.WriteString(strings.Repeat(" ", indent+2))
// The entire element is marked. // The entire element is marked.
if listVals[i].HasMark(marks.Sensitive) { if listVals[i].HasMark(marks.Sensitive) {
buf.WriteString("{}, # sensitive\n") buf.WriteString("{}, # sensitive\n")
@@ -399,7 +397,7 @@ func writeConfigNestedTypeAttributeFromExisting(addr addrs.AbsResourceInstance,
case configschema.NestingMap: case configschema.NestingMap:
if schema.Sensitive || stateVal.HasMark(marks.Sensitive) { if schema.Sensitive || stateVal.HasMark(marks.Sensitive) {
buf.WriteString(strings.Repeat(" ", indent)) 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 return diags
} }
@@ -407,7 +405,7 @@ func writeConfigNestedTypeAttributeFromExisting(addr addrs.AbsResourceInstance,
if attr.IsNull() { if attr.IsNull() {
// There is a difference between an empty map and a null map. // There is a difference between an empty map and a null map.
buf.WriteString(strings.Repeat(" ", indent)) 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 return diags
} }
@@ -420,10 +418,10 @@ func writeConfigNestedTypeAttributeFromExisting(addr addrs.AbsResourceInstance,
sort.Strings(keys) sort.Strings(keys)
buf.WriteString(strings.Repeat(" ", indent)) buf.WriteString(strings.Repeat(" ", indent))
fmt.Fprintf(buf, "%s = {\n", name) _, _ = fmt.Fprintf(buf, "%s = {\n", name)
for _, key := range keys { for _, key := range keys {
buf.WriteString(strings.Repeat(" ", indent+2)) buf.WriteString(strings.Repeat(" ", indent+2))
fmt.Fprintf(buf, "%s = {", key) _, _ = fmt.Fprintf(buf, "%s = {", key)
// This entire value is marked // This entire value is marked
if vals[key].HasMark(marks.Sensitive) { 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 { func writeConfigNestedBlockFromExisting(addr addrs.AbsResourceInstance, buf *strings.Builder, name string, schema *configschema.NestedBlock, stateVal cty.Value, indent int) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics var diags tfdiags.Diagnostics
@@ -456,7 +453,7 @@ func writeConfigNestedBlockFromExisting(addr addrs.AbsResourceInstance, buf *str
return diags return diags
} }
buf.WriteString(strings.Repeat(" ", indent)) 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 the entire value is marked, don't print any nested attributes
if stateVal.HasMark(marks.Sensitive) { if stateVal.HasMark(marks.Sensitive) {
@@ -471,13 +468,13 @@ func writeConfigNestedBlockFromExisting(addr addrs.AbsResourceInstance, buf *str
case configschema.NestingList, configschema.NestingSet: case configschema.NestingList, configschema.NestingSet:
if stateVal.HasMark(marks.Sensitive) { if stateVal.HasMark(marks.Sensitive) {
buf.WriteString(strings.Repeat(" ", indent)) buf.WriteString(strings.Repeat(" ", indent))
fmt.Fprintf(buf, "%s {} # sensitive\n", name) _, _ = fmt.Fprintf(buf, "%s {} # sensitive\n", name)
return diags return diags
} }
listVals := ctyCollectionValues(stateVal) listVals := ctyCollectionValues(stateVal)
for i := range listVals { for i := range listVals {
buf.WriteString(strings.Repeat(" ", indent)) 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(writeConfigAttributesFromExisting(addr, buf, listVals[i], schema.Attributes, indent+2))
diags = diags.Append(writeConfigBlocksFromExisting(addr, buf, listVals[i], schema.BlockTypes, indent+2)) diags = diags.Append(writeConfigBlocksFromExisting(addr, buf, listVals[i], schema.BlockTypes, indent+2))
buf.WriteString("}\n") buf.WriteString("}\n")
@@ -486,7 +483,7 @@ func writeConfigNestedBlockFromExisting(addr addrs.AbsResourceInstance, buf *str
case configschema.NestingMap: case configschema.NestingMap:
// If the entire value is marked, don't print any nested attributes // If the entire value is marked, don't print any nested attributes
if stateVal.HasMark(marks.Sensitive) { if stateVal.HasMark(marks.Sensitive) {
fmt.Fprintf(buf, "%s {} # sensitive\n", name) _, _ = fmt.Fprintf(buf, "%s {} # sensitive\n", name)
return diags return diags
} }
@@ -499,7 +496,7 @@ func writeConfigNestedBlockFromExisting(addr addrs.AbsResourceInstance, buf *str
sort.Strings(keys) sort.Strings(keys)
for _, key := range keys { for _, key := range keys {
buf.WriteString(strings.Repeat(" ", indent)) 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 // This entire map element is marked
if vals[key].HasMark(marks.Sensitive) { if vals[key].HasMark(marks.Sensitive) {
buf.WriteString("} # sensitive\n") buf.WriteString("} # sensitive\n")
@@ -526,9 +523,9 @@ func writeAttrTypeConstraint(buf *strings.Builder, schema *configschema.Attribut
} }
if schema.NestedType != nil { if schema.NestedType != nil {
fmt.Fprintf(buf, "%s\n", schema.NestedType.ImpliedType().FriendlyName()) _, _ = fmt.Fprintf(buf, "%s\n", schema.NestedType.ImpliedType().FriendlyName())
} else { } 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 return nil
} }
var len int var length int
if val.IsMarked() { if val.IsMarked() {
val, _ = val.Unmark() val, _ = val.Unmark()
len = val.LengthInt() length = val.LengthInt()
} else { } 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(); { for it := val.ElementIterator(); it.Next(); {
_, value := it.Element() _, value := it.Element()
ret = append(ret, value) ret = append(ret, value)
@@ -580,7 +577,7 @@ func omitUnknowns(val cty.Value) cty.Value {
case ty.IsPrimitiveType(): case ty.IsPrimitiveType():
return val return val
case ty.IsListType() || ty.IsTupleType() || ty.IsSetType(): case ty.IsListType() || ty.IsTupleType() || ty.IsSetType():
unmarked, marks := val.Unmark() unmarked, valMarks := val.Unmark()
var vals []cty.Value var vals []cty.Value
it := unmarked.ElementIterator() it := unmarked.ElementIterator()
for it.Next() { for it.Next() {
@@ -598,9 +595,9 @@ func omitUnknowns(val cty.Value) cty.Value {
// may have caused the individual elements to have different types, // may have caused the individual elements to have different types,
// and we're doing this work to produce JSON anyway and JSON marshalling // and we're doing this work to produce JSON anyway and JSON marshalling
// represents all of these sequence types as an array. // 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(): case ty.IsMapType() || ty.IsObjectType():
unmarked, marks := val.Unmark() unmarked, valMarks := val.Unmark()
vals := make(map[string]cty.Value) vals := make(map[string]cty.Value)
it := unmarked.ElementIterator() it := unmarked.ElementIterator()
for it.Next() { for it.Next() {
@@ -614,7 +611,7 @@ func omitUnknowns(val cty.Value) cty.Value {
// may have caused the individual elements to have different types, // may have caused the individual elements to have different types,
// and we're doing this work to produce JSON anyway and JSON marshalling // and we're doing this work to produce JSON anyway and JSON marshalling
// represents both of these mapping types as an object. // represents both of these mapping types as an object.
return cty.ObjectVal(vals).WithMarks(marks) return cty.ObjectVal(vals).WithMarks(valMarks)
default: default:
// Should never happen, since the above should cover all types // Should never happen, since the above should cover all types
panic(fmt.Sprintf("omitUnknowns cannot handle %#v", val)) panic(fmt.Sprintf("omitUnknowns cannot handle %#v", val))
@@ -661,3 +658,13 @@ func wrapAsJSONEncodeFunctionCall(v cty.Value) (hclwrite.Tokens, error) {
return tokens, nil 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, Computed: computed,
} }
} }
writeOnlyAttr := func(attribute *configschema.Attribute) *configschema.Attribute {
attribute.WriteOnly = true
return attribute
}
tcs := map[string]struct { tcs := map[string]struct {
schema *configschema.Block schema *configschema.Block
@@ -724,6 +728,59 @@ resource "tfcoremock_simple_resource" "example" {
sensitive_number = null # sensitive sensitive_number = null # sensitive
sensitive_object = null # sensitive sensitive_object = null # sensitive
sensitive_string = 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": { "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) { 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 var diags tfdiags.Diagnostics
ty := forEachVal.Type() ty := forEachVal.Type()
@@ -186,6 +183,18 @@ func performValueChecks(expr hcl.Expression, hclCtx *hcl.EvalContext, allowUnkno
}) })
resultVal = cty.NullVal(ty) 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 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": { "marked tuple": {
hcltest.MockExprLiteral(cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.StringVal("a")}).Mark(marks.Sensitive)), hcltest.MockExprLiteral(cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.StringVal("a")}).Mark(marks.Sensitive)),
[]struct { []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": { "marked string": {
hcltest.MockExprLiteral(cty.StringVal("a").Mark(marks.Sensitive)), hcltest.MockExprLiteral(cty.StringVal("a").Mark(marks.Sensitive)),
[]struct { []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 { 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": { "unknown tuple": {
Value: cty.UnknownVal(cty.Tuple([]cty.Type{cty.String})), Value: cty.UnknownVal(cty.Tuple([]cty.Type{cty.String})),
expectedErrs: []expectedErr{ expectedErrs: []expectedErr{
@@ -782,6 +850,77 @@ func TestEvaluateForEach(t *testing.T) {
}}, }},
PlanReturnValue: map[string]cty.Value{}, 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": { "string": {
Input: cty.StringVal("i am definitely a set"), Input: cty.StringVal("i am definitely a set"),
ValidateExpectedErrs: []expectedErr{ ValidateExpectedErrs: []expectedErr{

View File

@@ -69,7 +69,6 @@ func assertObjectCompatible(schema *configschema.Block, planned, actual cty.Valu
return errs 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 { func assertAttributeCompatible(plannedV, actualV cty.Value, attrS *configschema.Attribute, path cty.Path) []error {
var errs []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) 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 // planning data resources in particular. Specifically, it replaces the values
// of any Computed attributes not set in the configuration with an unknown // 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 // 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 // passing the proposedNewBlock result into a provider's PlanResourceChange
// function, assuming a fixed implementation of PlanResourceChange that just // function, assuming a fixed implementation of PlanResourceChange that just
// fills in unknown values as needed. // 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 // Our trick here is to run the proposedNewBlock logic with an
// entirely-unknown prior value. Because of cty's unknown short-circuit // entirely-unknown prior value. Because of cty's unknown short-circuit
// behavior, any operation on prior returns another unknown, and so // 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) 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 { func proposedNew(schema *configschema.Block, prior, config cty.Value) cty.Value {
if config.IsNull() || !config.IsKnown() { if config.IsNull() || !config.IsKnown() {
// A block config should never be null at this point. The only nullable // 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) 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) { func (p *GRPCProvider) GetProviderSchema(ctx context.Context) (resp providers.GetProviderSchemaResponse) {
logger.Trace("GRPCProvider.v6: GetProviderSchema") logger.Trace("GRPCProvider.v6: GetProviderSchema")
p.mu.Lock() p.mu.Lock()

View File

@@ -251,7 +251,7 @@ func (s simple) Close(_ context.Context) error {
func waitIfRequested(m map[string]cty.Value) { 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. // 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. // 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") { if v, ok := m["value_wo"]; ok && !v.IsNull() && strings.Contains(v.AsString(), "with-renew") {
<-time.After(time.Second) <-time.After(time.Second)

View File

@@ -242,7 +242,7 @@ func (s simple) Close(_ context.Context) error {
func waitIfRequested(m map[string]cty.Value) { 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. // 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. // 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") { if v, ok := m["value_wo"]; ok && !v.IsNull() && strings.Contains(v.AsString(), "with-renew") {
<-time.After(time.Second) <-time.After(time.Second)

View File

@@ -14,8 +14,6 @@ import (
"github.com/zclconf/go-cty/cty" "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 // FormatValue formats a value in a way that resembles OpenTofu language syntax
// and uses the type conversion functions where necessary to indicate exactly // and uses the type conversion functions where necessary to indicate exactly
// what type it is given, so that equality test failures can be quickly // 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) { if v.HasMark(marks.Sensitive) {
return "(sensitive value)" return "(sensitive value)"
} }
if v.HasMark(marks.Ephemeral) {
return "(ephemeral value)"
}
if v.IsNull() { if v.IsNull() {
return formatNullValue(v.Type()) return formatNullValue(v.Type())
} }

View File

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

View File

@@ -1069,7 +1069,7 @@ func (d *evaluationStateData) GetOutput(_ context.Context, addr addrs.OutputValu
if output.Sensitive { if output.Sensitive {
val = val.Mark(marks.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 // 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. // values with ephemeral. Reanalyse this when implementing the testing support.
// if config.Ephemeral { // if config.Ephemeral {

View File

@@ -196,7 +196,7 @@ func TestEvaluatorGetOutputValue(t *testing.T) {
t.Errorf("wrong result %#v; want %#v", got, want) 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").Mark(marks.Ephemeral)
want = cty.StringVal("third") want = cty.StringVal("third")
got, diags = scope.Data.GetOutput(t.Context(), addrs.OutputValue{ 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() unmarkedConfigVal, configMarkPaths := configVal.UnmarkDeepWithPaths()
proposedNewVal := objchange.PlannedDataResourceObject(schema, unmarkedConfigVal) proposedNewVal := objchange.PlannedUnknownObject(schema, unmarkedConfigVal)
proposedNewVal = proposedNewVal.MarkWithPaths(configMarkPaths) proposedNewVal = proposedNewVal.MarkWithPaths(configMarkPaths)
// Apply detects that the data source will need to be read by the After // 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 // If we had errors, then we can cover that up by marking the new
// state as unknown. // state as unknown.
unmarkedConfigVal, configMarkPaths := configVal.UnmarkDeepWithPaths() unmarkedConfigVal, configMarkPaths := configVal.UnmarkDeepWithPaths()
newVal = objchange.PlannedDataResourceObject(schema, unmarkedConfigVal) newVal = objchange.PlannedUnknownObject(schema, unmarkedConfigVal)
newVal = newVal.MarkWithPaths(configMarkPaths) newVal = newVal.MarkWithPaths(configMarkPaths)
// We still want to report the check as failed even if we are still // 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() unmarkedConfigVal, configMarkPaths := configVal.UnmarkDeepWithPaths()
proposedNewVal := objchange.PlannedEphemeralResourceObject(schema, unmarkedConfigVal) proposedNewVal := objchange.PlannedUnknownObject(schema, unmarkedConfigVal)
proposedNewVal = proposedNewVal.MarkWithPaths(configMarkPaths) proposedNewVal = proposedNewVal.MarkWithPaths(configMarkPaths)
plannedChange = &plans.ResourceInstanceChange{ 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) { 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") panic("implement me")
} }
func (p providerForTest) RenewEphemeralResource(_ context.Context, _ providers.RenewEphemeralResourceRequest) (resp providers.RenewEphemeralResourceResponse) { 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") panic("implement me")
} }
func (p providerForTest) CloseEphemeralResource(_ context.Context, _ providers.CloseEphemeralResourceRequest) (resp providers.CloseEphemeralResourceResponse) { 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") panic("implement me")
} }