diff --git a/internal/command/e2etest/primary_test.go b/internal/command/e2etest/primary_test.go index a3bb5916b8..f380b05a4a 100644 --- a/internal/command/e2etest/primary_test.go +++ b/internal/command/e2etest/primary_test.go @@ -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: diff --git a/internal/command/format/object_id.go b/internal/command/format/object_id.go index c93827b830..ad75901b2c 100644 --- a/internal/command/format/object_id.go +++ b/internal/command/format/object_id.go @@ -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() diff --git a/internal/command/format/object_id_test.go b/internal/command/format/object_id_test.go index 8e3d80ad27..e1af9e27c7 100644 --- a/internal/command/format/object_id_test.go +++ b/internal/command/format/object_id_test.go @@ -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"), diff --git a/internal/command/jsonentities/diagnostic.go b/internal/command/jsonentities/diagnostic.go index 84f42d23f2..c79eda728d 100644 --- a/internal/command/jsonentities/diagnostic.go +++ b/internal/command/jsonentities/diagnostic.go @@ -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 } diff --git a/internal/command/jsonformat/differ/block.go b/internal/command/jsonformat/differ/block.go index 4fafcf0f75..2fdc159e9a 100644 --- a/internal/command/jsonformat/differ/block.go +++ b/internal/command/jsonformat/differ/block.go @@ -55,8 +55,13 @@ 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 { - // Don't record nil values at all in blocks. - continue + // 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 diff --git a/internal/command/jsonformat/differ/differ_test.go b/internal/command/jsonformat/differ/differ_test.go index 2d1f8fceb7..3ca9fc82b8 100644 --- a/internal/command/jsonformat/differ/differ_test.go +++ b/internal/command/jsonformat/differ/differ_test.go @@ -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 { diff --git a/internal/command/jsonformat/plan.go b/internal/command/jsonformat/plan.go index 2ec8556f5c..ac228546ed 100644 --- a/internal/command/jsonformat/plan.go +++ b/internal/command/jsonformat/plan.go @@ -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 } diff --git a/internal/command/testing/test_provider.go b/internal/command/testing/test_provider.go index 72dcc6f846..64184fbb07 100644 --- a/internal/command/testing/test_provider.go +++ b/internal/command/testing/test_provider.go @@ -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 } ) diff --git a/internal/command/views/hook_count.go b/internal/command/views/hook_count.go index 165cb40af1..929b3e253e 100644 --- a/internal/command/views/hook_count.go +++ b/internal/command/views/hook_count.go @@ -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 } diff --git a/internal/configs/config.go b/internal/configs/config.go index 906f9a5fd2..db03548cc1 100644 --- a/internal/configs/config.go +++ b/internal/configs/config.go @@ -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), diff --git a/internal/configs/test_file.go b/internal/configs/test_file.go index 4930345418..bd7206e72f 100644 --- a/internal/configs/test_file.go +++ b/internal/configs/test_file.go @@ -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. diff --git a/internal/genconfig/generate_config.go b/internal/genconfig/generate_config.go index cbfb38a234..09e2e329d4 100644 --- a/internal/genconfig/generate_config.go +++ b/internal/genconfig/generate_config.go @@ -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" +} diff --git a/internal/genconfig/generate_config_test.go b/internal/genconfig/generate_config_test.go index 1f952b1a3c..21f9095a1c 100644 --- a/internal/genconfig/generate_config_test.go +++ b/internal/genconfig/generate_config_test.go @@ -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": { diff --git a/internal/lang/evalchecks/eval_for_each.go b/internal/lang/evalchecks/eval_for_each.go index 3559866c60..dd697148a6 100644 --- a/internal/lang/evalchecks/eval_for_each.go +++ b/internal/lang/evalchecks/eval_for_each.go @@ -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 } diff --git a/internal/lang/evalchecks/eval_for_each_test.go b/internal/lang/evalchecks/eval_for_each_test.go index e765947498..01b376d53b 100644 --- a/internal/lang/evalchecks/eval_for_each_test.go +++ b/internal/lang/evalchecks/eval_for_each_test.go @@ -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{ diff --git a/internal/plans/objchange/compatible.go b/internal/plans/objchange/compatible.go index 199ad65807..dae212d284 100644 --- a/internal/plans/objchange/compatible.go +++ b/internal/plans/objchange/compatible.go @@ -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 diff --git a/internal/plans/objchange/objchange.go b/internal/plans/objchange/objchange.go index d8442f6624..98a5f1e460 100644 --- a/internal/plans/objchange/objchange.go +++ b/internal/plans/objchange/objchange.go @@ -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 diff --git a/internal/plugin6/grpc_provider.go b/internal/plugin6/grpc_provider.go index 1b0c7c7e22..8477df79ed 100644 --- a/internal/plugin6/grpc_provider.go +++ b/internal/plugin6/grpc_provider.go @@ -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() diff --git a/internal/provider-simple-v6/provider.go b/internal/provider-simple-v6/provider.go index c8e43efedc..edc043a53c 100644 --- a/internal/provider-simple-v6/provider.go +++ b/internal/provider-simple-v6/provider.go @@ -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) diff --git a/internal/provider-simple/provider.go b/internal/provider-simple/provider.go index 8929c29c47..14fcce01c6 100644 --- a/internal/provider-simple/provider.go +++ b/internal/provider-simple/provider.go @@ -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) diff --git a/internal/repl/format.go b/internal/repl/format.go index 398d4e62f1..8b85101fa1 100644 --- a/internal/repl/format.go +++ b/internal/repl/format.go @@ -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()) } diff --git a/internal/repl/format_test.go b/internal/repl/format_test.go index 7cc2037acf..c2031b40c0 100644 --- a/internal/repl/format_test.go +++ b/internal/repl/format_test.go @@ -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 { diff --git a/internal/tofu/evaluate.go b/internal/tofu/evaluate.go index 34e667493d..17c327a644 100644 --- a/internal/tofu/evaluate.go +++ b/internal/tofu/evaluate.go @@ -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 { diff --git a/internal/tofu/evaluate_test.go b/internal/tofu/evaluate_test.go index 5a3ae8a7fc..57e05b0375 100644 --- a/internal/tofu/evaluate_test.go +++ b/internal/tofu/evaluate_test.go @@ -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{ diff --git a/internal/tofu/node_resource_abstract_instance.go b/internal/tofu/node_resource_abstract_instance.go index 34b128891b..b84c2f81ea 100644 --- a/internal/tofu/node_resource_abstract_instance.go +++ b/internal/tofu/node_resource_abstract_instance.go @@ -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{ diff --git a/internal/tofu/provider_for_test_framework.go b/internal/tofu/provider_for_test_framework.go index 7131d74206..7b2dd26d60 100644 --- a/internal/tofu/provider_for_test_framework.go +++ b/internal/tofu/provider_for_test_framework.go @@ -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") }