Ephemeral outputs (#3123)

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-18 10:29:08 +03:00
committed by Christian Mesh
parent db39f00583
commit b5d414331f
21 changed files with 406 additions and 39 deletions

View File

@@ -110,3 +110,106 @@ func TestEphemeralErrors_variables(t *testing.T) {
}
})
}
func TestEphemeralErrors_outputs(t *testing.T) {
tf := e2e.NewBinary(t, tofuBin, "testdata/ephemeral-errors/outputs")
buildSimpleProvider(t, "6", tf.WorkDir(), "simple")
with := func(path string, fn func()) {
src := tf.Path(path + ".disabled")
dst := tf.Path(path)
tf.WorkDir()
err := os.Rename(src, dst)
if err != nil {
t.Fatalf("%s", err.Error())
}
fn()
err = os.Rename(dst, src)
if err != nil {
t.Fatalf("%s", err.Error())
}
}
tofuInit := func() {
sout, serr, err := tf.Run("init", "-plugin-dir=cache")
if err != nil {
t.Fatalf("unable to init: %s;\nstderr:\n%s\nstdout:\n%s", err, serr, sout)
}
}
with("ephemeral_output_in_root_module.tf", func() {
sout, serr, err := tf.Run("apply")
if err == nil || !strings.Contains(err.Error(), "exit status 1") {
t.Errorf("unexpected err: %s;\nstderr:\n%s\nstdout:\n%s", err, serr, sout)
}
sanitized := SanitizeStderr(serr)
if !strings.Contains(sanitized, `Error: Invalid output configuration on ephemeral_output_in_root_module.tf`) ||
!strings.Contains(sanitized, `Root modules are not allowed to have outputs defined as ephemeral`) {
t.Errorf("unexpected stderr: %s;\nstderr:\n%s\nstdout:\n%s", err, serr, sout)
t.Logf("sanitized serr: %s", sanitized)
}
})
with("ephemeral_output_in_resource.tf", func() {
tofuInit()
sout, serr, err := tf.Run("apply")
if err == nil || !strings.Contains(err.Error(), "exit status 1") {
t.Errorf("unexpected err: %s;\nstderr:\n%s\nstdout:\n%s", err, serr, sout)
}
sanitized := SanitizeStderr(serr)
if !strings.Contains(sanitized, `Ephemeral value used in non-ephemeral context with simple_resource.test_res`) {
t.Errorf("unexpected stderr: %s;\nstderr:\n%s\nstdout:\n%s", err, serr, sout)
t.Logf("sanitized serr: %s", sanitized)
}
})
with("ephemeral_output_in_data_source.tf", func() {
tofuInit()
sout, serr, err := tf.Run("apply")
if err == nil || !strings.Contains(err.Error(), "exit status 1") {
t.Errorf("unexpected err: %s;\nstderr:\n%s\nstdout:\n%s", err, serr, sout)
}
sanitized := SanitizeStderr(serr)
if !strings.Contains(sanitized, `Ephemeral value used in non-ephemeral context with data.simple_resource.test_data1`) {
t.Errorf("unexpected stderr: %s;\nstderr:\n%s\nstdout:\n%s", err, serr, sout)
t.Logf("sanitized serr: %s", sanitized)
}
})
with("regular_output_given_ephemeral_value.tf", func() {
tofuInit()
sout, serr, err := tf.Run("apply")
if err == nil || !strings.Contains(err.Error(), "exit status 1") {
t.Errorf("unexpected err: %s;\nstderr:\n%s\nstdout:\n%s", err, serr, sout)
}
sanitized := SanitizeStderr(serr)
if !strings.Contains(sanitized, `Output does not allow ephemeral value on __mod-with-regular-output-got-ephemeral-value/main.tf`) ||
!strings.Contains(sanitized, `The value that was generated for the output is ephemeral, but it is not configured to allow one`) {
t.Errorf("unexpected stderr: %s;\nstderr:\n%s\nstdout:\n%s", err, serr, sout)
t.Logf("sanitized serr: %s", sanitized)
}
})
with("ephemeral_output_with_precondition.tf", func() {
tofuInit()
sout, serr, err := tf.Run("apply", "-var", "in=notdefaultvalue")
if err == nil || !strings.Contains(err.Error(), "exit status 1") {
t.Errorf("unexpected err: %s;\nstderr:\n%s\nstdout:\n%s", err, serr, sout)
}
sanitizedSerr := SanitizeStderr(serr)
sanitizedSout := SanitizeStderr(sout)
if !strings.Contains(sanitizedSerr, `Module output value precondition failed on __mod-ephemeral-output-with-precondition/main.tf`) ||
!strings.Contains(sanitizedSerr, `This check failed, but has an invalid error message as described in the other accompanying messages`) ||
strings.Contains(sanitizedSerr, `"notdefaultvalue" -> "default value"`) {
t.Errorf("unexpected stderr: %s;\nstderr:\n%s\nstdout:\n%s", err, serr, sout)
t.Logf("sanitized serr: %s", sanitizedSerr)
}
if !strings.Contains(sanitizedSout, `Warning: Error message refers to ephemeral values on __mod-ephemeral-output-with-precondition/main.tf`) ||
!strings.Contains(sanitizedSout, `The error expression used to explain this condition refers to ephemeral values, so OpenTofu will not display the resulting message. You can correct this by removing references to ephemeral values`) {
t.Errorf("unexpected stdout: %s;\nstdout:\n%s\nstdout:\n%s", err, serr, sout)
t.Logf("sanitized sout: %s", sanitizedSerr)
}
})
}

View File

@@ -17,6 +17,7 @@ import (
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/google/go-cmp/cmp"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/e2e"
"github.com/opentofu/opentofu/internal/getproviders"
@@ -274,7 +275,6 @@ func TestEphemeralWorkflowAndOutput(t *testing.T) {
t.Fatalf("unexpected plan error: %s\nstderr:\n%s", err, stderr)
}
// TODO ephemeral - this "value_wo" should be shown something like (write-only attribute). This will be handled during the work on the write-only attributes.
// TODO ephemeral - "out_ephemeral" should fail later when the marking of the outputs is implemented fully, so that should not be visible in the output
expectedChangesOutput := `OpenTofu used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+ create
@@ -302,8 +302,7 @@ OpenTofu will perform the following actions:
Plan: 2 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ final_output = "just a simple resource to ensure that the second provider it's working fine"
+ out_ephemeral = "rawvalue"`
+ final_output = "just a simple resource to ensure that the second provider it's working fine"`
entriesChecker := &outputEntriesChecker{phase: "plan"}
entriesChecker.addChecks(outputEntry{[]string{"data.simple_resource.test_data1: Reading..."}, true},
@@ -321,6 +320,7 @@ Changes to Outputs:
if !strings.Contains(out, expectedChangesOutput) {
t.Errorf("wrong plan output:\nstdout:%s\nstderr:%s", stdout, stderr)
t.Log(cmp.Diff(out, expectedChangesOutput))
}
entriesChecker.check(t, out)
@@ -443,6 +443,7 @@ Changes to Outputs:
if !strings.Contains(out, expectedChangesOutput) {
t.Errorf("wrong apply output:\nstdout:%s\nstderr%s", stdout, stderr)
t.Log(cmp.Diff(out, expectedChangesOutput))
}
entriesChecker.check(t, out)
}

View File

@@ -0,0 +1,18 @@
variable "variable_for_output_precondition" {
type = string
default = "default value"
ephemeral = true
}
// NOTE: This is meant to test the precondition warnings and errors when ephemeral values are used.
output "output_with_precondition" {
value = var.variable_for_output_precondition
ephemeral = true
precondition {
// NOTE: When this condition will fail, for ephemeral references in the condition, the diff between the left
// side and right side will be skipped. This is printed only for sensitive values.
condition = var.variable_for_output_precondition == "default value"
// NOTE: This error_message is going to generate a warning because ephemeral values are used in the expression.
error_message = "Variable `variable_for_output_precondition` does not have the required value: ${var.variable_for_output_precondition}"
}
}

View File

@@ -0,0 +1,27 @@
variable "ephemeral_var" {
type = string
ephemeral = true
}
variable "regular_var" {
type = string
}
output "ephemeral_out_from_ephemeral_in" {
value = var.ephemeral_var
ephemeral = true
}
// NOTE: This output is used to test that it can receive a non-ephemeral value but it results in an
// ephemeral value when used.
output "ephemeral_out_from_regular_var" {
value = var.regular_var
ephemeral = true
}
// NOTE: This output is used to test that a hardcoded raw value will be marked as ephemeral when
// referenced in an expression.
output "ephemeral_out_hardcoded_with_non_ephemeral_value" {
value = "raw value"
ephemeral = true
}

View File

@@ -0,0 +1,10 @@
variable "test_var" {
type = string
default = "test_var value"
ephemeral = true
}
// NOTE: An output that wants to use ephemeral values needs to be configured with "ephemeral = true"
output "test" {
value = var.test_var
}

View File

@@ -0,0 +1,24 @@
// Running this configuration is expected to have an error since data sources are not allowed
// to use ephemeral values at all.
terraform {
required_providers {
simple = {
source = "registry.opentofu.org/hashicorp/simple"
}
}
}
provider "simple" {
alias = "s1"
}
module "test" {
source = "./__mod-with-ephemeral-output"
ephemeral_var = "value in ephemeral_var"
regular_var = "value in regular_var"
}
data "simple_resource" "test_data1" {
provider = simple.s1
value = module.test.ephemeral_out_from_regular_var
}

View File

@@ -0,0 +1,24 @@
// Running this configuration is expected to have an error since `value` is not a write-only argument.
// Regular resources arguments are not allowed to use ephemeral variables and values
terraform {
required_providers {
simple = {
source = "registry.opentofu.org/hashicorp/simple"
}
}
}
provider "simple" {
alias = "s1"
}
module "test" {
source = "./__mod-with-ephemeral-output"
ephemeral_var = "value in ephemeral_var"
regular_var = "value in regular_var"
}
resource "simple_resource" "test_res" {
provider = simple.s1
value = module.test.ephemeral_out_hardcoded_with_non_ephemeral_value
}

View File

@@ -0,0 +1,5 @@
// NOTE: Ephemeral outputs are not allowed in the root module
output "test" {
value = "test"
ephemeral = true
}

View File

@@ -0,0 +1,9 @@
variable "in" {
ephemeral = true
default = "default value"
type = string
}
module "test" {
source = "./__mod-ephemeral-output-with-precondition"
variable_for_output_precondition = var.in
}

View File

@@ -0,0 +1,3 @@
module "test" {
source = "./__mod-with-regular-output-got-ephemeral-value"
}

View File

@@ -99,11 +99,6 @@ module "call" {
// NOTE: because variable "in" is marked as ephemeral, this should work as expected.
}
output "out_ephemeral" {
value = module.call.out2
// TODO: Because the output ephemeral marking is not done yet entirely, this is working now but remove this output once the marking of outputs are done completely.
}
output "final_output" {
value = simple_resource.test_res_second_provider.value
}

View File

@@ -8,8 +8,3 @@ output "out1" {
value = var.in
ephemeral = true // NOTE: because
}
output "out2" {
value = "rawvalue" // TODO ephemeral - this is returning a raw value and since incomplete work, the evaluated value is not marked as ephemeral. Once this will be fixed, the test should fail
ephemeral = true
}

View File

@@ -115,6 +115,7 @@ type resource struct {
type output struct {
Sensitive bool `json:"sensitive,omitempty"`
Ephemeral bool `json:"ephemeral,omitempty"`
Deprecated string `json:"deprecated,omitempty"`
Expression *expression `json:"expression,omitempty"`
DependsOn []string `json:"depends_on,omitempty"`
@@ -355,6 +356,7 @@ func marshalModule(c *configs.Config, schemas *tofu.Schemas, addr string) (modul
for _, v := range c.Module.Outputs {
o := output{
Sensitive: v.Sensitive,
Ephemeral: v.Ephemeral,
Deprecated: v.Deprecated,
}
if !inSingleModuleMode(schemas) {

View File

@@ -10,6 +10,7 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/configs"
@@ -351,6 +352,62 @@ func TestMarshalModule(t *testing.T) {
},
},
},
"output, minimal": {
Input: &configs.Config{
Module: &configs.Module{
Outputs: map[string]*configs.Output{
"example": {
Name: "example",
},
},
},
},
Schemas: emptySchemas,
Want: module{
Outputs: map[string]output{
"example": {
Expression: ptrTo(marshalExpression(nil)),
},
},
ModuleCalls: map[string]moduleCall{},
},
},
"output, elaborate": {
Input: &configs.Config{
Module: &configs.Module{
Outputs: map[string]*configs.Output{
"example": {
Name: "example",
Description: "description",
Expr: &hclsyntax.LiteralValueExpr{Val: cty.StringVal("test")},
DependsOn: []hcl.Traversal{},
Sensitive: true,
Deprecated: "deprecation message",
Ephemeral: true,
Preconditions: []*configs.CheckRule{
{
Condition: &hclsyntax.ConditionalExpr{},
},
},
IsOverridden: false,
},
},
},
},
Schemas: emptySchemas,
Want: module{
Outputs: map[string]output{
"example": {
Sensitive: true,
Ephemeral: true,
Deprecated: "deprecation message",
Expression: ptrTo(marshalExpression(&hclsyntax.LiteralValueExpr{Val: cty.StringVal("test")})),
Description: "description",
},
},
ModuleCalls: map[string]moduleCall{},
},
},
// TODO: More test cases covering things other than input variables.
// (For now the other details are mainly tested in package command,
// as part of the tests for "tofu show".)

View File

@@ -365,6 +365,15 @@ func newDiagnosticDifference(diag tfdiags.Diagnostic) *jsonplan.Change {
lhs, _ := binExpr.LHS.Value(ctx)
rhs, _ := binExpr.RHS.Value(ctx)
// Because a jsonplan.Change is not meant to hold any ephemeral information,
// 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.
if marks.Contains(lhs, marks.Ephemeral) || marks.Contains(rhs, marks.Ephemeral) {
return nil
}
change, err := jsonplan.GenerateChange(lhs, rhs)
if err != nil {
return nil

View File

@@ -17,8 +17,6 @@ import (
"github.com/opentofu/opentofu/internal/tfdiags"
)
// TODO ephemeral - check this when working on the ephemeral outputs - and the unit tests for it
// We might need to returne diags in case of ephemeral - let's check this at that point
func OutputsFromMap(outputValues map[string]*states.OutputValue) (jsonentities.Outputs, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics

View File

@@ -450,9 +450,6 @@ func changeFromTfplan(rawChange *planproto.Change) (*plans.ChangeSrc, error) {
}
ret.GeneratedConfig = rawChange.GeneratedConfig
// TODO ephemeral - investiaate if ephemeral mark needs to be handled here.
// It looks that it's not needed, but let's be sure.
// But check the unit tests
sensitive := cty.NewValueMarks(marks.Sensitive)
beforeValMarks, err := pathValueMarksFromTfplan(rawChange.BeforeSensitivePaths, sensitive)
if err != nil {

View File

@@ -264,9 +264,6 @@ func evalCheckErrorMessage(expr hcl.Expression, hclCtx *hcl.EvalContext) (string
return "", diags
}
// TODO ephemeral - ephemeral mark needs to be handled too here.
// See the validation for variables since there we already handled the
// situation where error_message contains a reference to the ephemeral values
val, valMarks := val.Unmark()
if _, sensitive := valMarks[marks.Sensitive]; sensitive {
diags = diags.Append(&hcl.Diagnostic{
@@ -281,6 +278,19 @@ You can correct this by removing references to sensitive values, or by carefully
})
return "", diags
}
if _, ephemeral := valMarks[marks.Ephemeral]; ephemeral {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: "Error message refers to ephemeral values",
Detail: `The error expression used to explain this condition refers to ephemeral values, so OpenTofu will not display the resulting message.
You can correct this by removing references to ephemeral values.`, // TODO ephemeral - update the message to include ephemeralasnull option too
Subject: expr.Range().Ptr(),
Expression: expr,
EvalContext: hclCtx,
})
return "", diags
}
// NOTE: We've discarded any other marks the string might have been carrying,
// aside from the sensitive mark.

View File

@@ -395,11 +395,19 @@ func (d *evaluationStateData) GetModule(_ context.Context, addr addrs.ModuleCall
stateMap := map[addrs.InstanceKey]map[string]cty.Value{}
for _, output := range d.Evaluator.State.ModuleOutputs(d.ModulePath, addr) {
val := output.Value
// TODO ephemeral - outputs need to get the ephemeral attribute too and then it can be used here
if output.Sensitive {
val = val.Mark(marks.Sensitive)
}
// Since states.OutputValue is for managing the state content, we don't want to
// store in there the ephemeral attribute of the output.
// Therefore, to be able to mark a child module output as ephemeral, we need
// to check its configuration instead.
outputCfg := outputConfigs[output.Addr.OutputValue.Name]
if outputCfg != nil {
if outputCfg.Ephemeral {
val = val.Mark(marks.Ephemeral)
}
}
if output.Deprecated != "" {
val = marks.DeprecatedOutput(val, output.Addr, output.Deprecated, parentCfg.IsModuleCallFromRemoteModule(addr.Name))
}
@@ -442,8 +450,8 @@ func (d *evaluationStateData) GetModule(_ context.Context, addr addrs.ModuleCall
unknownMap[cfg.Name] = cty.DynamicPseudoType
// get all instance output for this path from the state
for key, states := range stateMap {
outputState, ok := states[cfg.Name]
for key, outputStates := range stateMap {
outputState, ok := outputStates[cfg.Name]
if !ok {
continue
}
@@ -484,6 +492,12 @@ func (d *evaluationStateData) GetModule(_ context.Context, addr addrs.ModuleCall
if change.Sensitive {
instance[cfg.Name] = change.After.Mark(marks.Sensitive)
}
// This is necessary for cases where the change of the output is not generated by evaluation
// an expression referencing ephemeral values, but the output block is configured as ephemeral.
// Any other case where a change is generated by ephemeral values is not affected by the double marking.
if cfg.Ephemeral {
instance[cfg.Name] = change.After.Mark(marks.Ephemeral)
}
if cfg.Deprecated != "" {
instance[cfg.Name] = marks.DeprecatedOutput(change.After, change.Addr, cfg.Deprecated, parentCfg.IsModuleCallFromRemoteModule(addr.Name))
@@ -1049,8 +1063,12 @@ func (d *evaluationStateData) GetOutput(_ context.Context, addr addrs.OutputValu
if output.Sensitive {
val = val.Mark(marks.Sensitive)
}
// TODO ephemeral - ensure that output is getting ephemeral marks correctly
// TODO ephemeral - 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 {
// val = val.Mark(marks.Ephemeral)
// }
if config.Deprecated != "" {
isRemote := false
if p := moduleConfig.Path; p != nil && !p.IsRoot() {

View File

@@ -111,6 +111,10 @@ func TestEvaluatorGetOutputValue(t *testing.T) {
Name: "some_output",
Sensitive: true,
},
"ephemeral_output": {
Name: "ephemeral_output",
Ephemeral: true,
},
"some_other_output": {
Name: "some_other_output",
},
@@ -130,6 +134,12 @@ func TestEvaluatorGetOutputValue(t *testing.T) {
Name: "some_other_output",
},
}, cty.StringVal("second"), false, "")
state.SetOutputValue(addrs.AbsOutputValue{
Module: addrs.RootModuleInstance,
OutputValue: addrs.OutputValue{
Name: "ephemeral_output",
},
}, cty.StringVal("third"), false, "")
}).SyncWrapper(),
}
@@ -161,6 +171,20 @@ func TestEvaluatorGetOutputValue(t *testing.T) {
if !got.RawEquals(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
// want = cty.StringVal("third").Mark(marks.Ephemeral)
want = cty.StringVal("third")
got, diags = scope.Data.GetOutput(t.Context(), addrs.OutputValue{
Name: "ephemeral_output",
}, tfdiags.SourceRange{})
if len(diags) != 0 {
t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
}
if !got.RawEquals(want) {
t.Errorf("wrong result %#v; want %#v", got, want)
}
}
// This particularly tests that a sensitive attribute in config
@@ -785,13 +809,22 @@ func TestEvaluatorGetModule(t *testing.T) {
true,
"",
)
ss.SetOutputValue(
addrs.OutputValue{Name: "out2"}.Absolute(addrs.ModuleInstance{addrs.ModuleInstanceStep{Name: "mod"}}),
cty.StringVal("baz"),
false,
"",
)
}).SyncWrapper()
evaluator := evaluatorForModule(stateSync, plans.NewChanges().SyncWrapper())
data := &evaluationStateData{
Evaluator: evaluator,
}
scope := evaluator.Scope(data, nil, nil, nil)
want := cty.ObjectVal(map[string]cty.Value{"out": cty.StringVal("bar").Mark(marks.Sensitive)})
want := cty.ObjectVal(map[string]cty.Value{
"out": cty.StringVal("bar").Mark(marks.Sensitive),
"out2": cty.StringVal("baz").Mark(marks.Ephemeral),
})
got, diags := scope.Data.GetModule(t.Context(), addrs.ModuleCall{
Name: "mod",
}, tfdiags.SourceRange{})
@@ -800,7 +833,7 @@ func TestEvaluatorGetModule(t *testing.T) {
t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
}
if !got.RawEquals(want) {
t.Errorf("wrong result %#v; want %#v", got, want)
t.Errorf("wrong result %#v\nwant %#v", got, want)
}
// Changes should override the state value
@@ -814,12 +847,23 @@ func TestEvaluatorGetModule(t *testing.T) {
}
cs, _ := change.Encode()
changesSync.AppendOutputChange(cs)
change2 := &plans.OutputChange{
Addr: addrs.OutputValue{Name: "out2"}.Absolute(addrs.ModuleInstance{addrs.ModuleInstanceStep{Name: "mod"}}),
Change: plans.Change{
After: cty.StringVal("bazz"),
},
}
cs2, _ := change2.Encode()
changesSync.AppendOutputChange(cs2)
evaluator = evaluatorForModule(stateSync, changesSync)
data = &evaluationStateData{
Evaluator: evaluator,
}
scope = evaluator.Scope(data, nil, nil, nil)
want = cty.ObjectVal(map[string]cty.Value{"out": cty.StringVal("baz").Mark(marks.Sensitive)})
want = cty.ObjectVal(map[string]cty.Value{
"out": cty.StringVal("baz").Mark(marks.Sensitive),
"out2": cty.StringVal("bazz").Mark(marks.Ephemeral),
})
got, diags = scope.Data.GetModule(t.Context(), addrs.ModuleCall{
Name: "mod",
}, tfdiags.SourceRange{})
@@ -837,7 +881,10 @@ func TestEvaluatorGetModule(t *testing.T) {
Evaluator: evaluator,
}
scope = evaluator.Scope(data, nil, nil, nil)
want = cty.ObjectVal(map[string]cty.Value{"out": cty.StringVal("baz").Mark(marks.Sensitive)})
want = cty.ObjectVal(map[string]cty.Value{
"out": cty.StringVal("baz").Mark(marks.Sensitive),
"out2": cty.StringVal("bazz").Mark(marks.Ephemeral),
})
got, diags = scope.Data.GetModule(t.Context(), addrs.ModuleCall{
Name: "mod",
}, tfdiags.SourceRange{})
@@ -872,6 +919,10 @@ func evaluatorForModule(stateSync *states.SyncState, changesSync *plans.ChangesS
Name: "out",
Sensitive: true,
},
"out2": {
Name: "out2",
Ephemeral: true,
},
},
},
},

View File

@@ -392,14 +392,6 @@ If you do intend to export this data, annotate the output value as sensitive by
Subject: n.Config.DeclRange.Ptr(),
})
}
if n.Config.Ephemeral {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid output configuration",
Detail: "Root modules are not allowed to have outputs defined as ephemeral",
Subject: n.Config.DeclRange.Ptr(),
})
}
}
}
@@ -448,6 +440,15 @@ If you do intend to export this data, annotate the output value as sensitive by
}
func (n *NodeApplyableOutput) validateEphemerality(val cty.Value) (diags tfdiags.Diagnostics) {
if n.Config.Ephemeral && n.Addr.Module.IsRoot() {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid output configuration",
Detail: "Root modules are not allowed to have outputs defined as ephemeral",
Subject: n.Config.DeclRange.Ptr(),
})
}
// We don't want to check when the value is unknown and is not marked.
// If the value is unknown due to the referenced values and inherited the marks from those,
// we do want to validate though.
@@ -464,6 +465,16 @@ func (n *NodeApplyableOutput) validateEphemerality(val cty.Value) (diags tfdiags
Subject: n.Config.UsageRange().Ptr(),
})
}
// There would be a 3rd validation that could be added: when the value generated for a
// root module output is marked as ephemeral.
// But considering the other 2 validations above, that validation could not be reached:
// - 1st validation does not allow for a root module output to be configured
// - 2nd validation does not allow for an ephemeral value to be stored
// into an output without the "ephemeral = true" config
// Therefore, since we do not allow ephemeral marked values to be stored into an output
// without "ephemeral = true" AND we don't allow "ephemeral = true" on root modules, then
// the 3rd validation can be skipped.
return diags
}