mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-19 17:59:05 -05:00
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:
committed by
Christian Mesh
parent
db39f00583
commit
b5d414331f
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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}"
|
||||
}
|
||||
}
|
||||
27
internal/command/e2etest/testdata/ephemeral-errors/outputs/__mod-with-ephemeral-output/main.tf
vendored
Normal file
27
internal/command/e2etest/testdata/ephemeral-errors/outputs/__mod-with-ephemeral-output/main.tf
vendored
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// NOTE: Ephemeral outputs are not allowed in the root module
|
||||
output "test" {
|
||||
value = "test"
|
||||
ephemeral = true
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module "test" {
|
||||
source = "./__mod-with-regular-output-got-ephemeral-value"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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".)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user