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"
|
"testing"
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/opentofu/opentofu/internal/addrs"
|
"github.com/opentofu/opentofu/internal/addrs"
|
||||||
"github.com/opentofu/opentofu/internal/e2e"
|
"github.com/opentofu/opentofu/internal/e2e"
|
||||||
"github.com/opentofu/opentofu/internal/getproviders"
|
"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)
|
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 - 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
|
expectedChangesOutput := `OpenTofu used the selected providers to generate the following execution
|
||||||
plan. Resource actions are indicated with the following symbols:
|
plan. Resource actions are indicated with the following symbols:
|
||||||
+ create
|
+ create
|
||||||
@@ -302,8 +302,7 @@ OpenTofu will perform the following actions:
|
|||||||
Plan: 2 to add, 0 to change, 0 to destroy.
|
Plan: 2 to add, 0 to change, 0 to destroy.
|
||||||
|
|
||||||
Changes to Outputs:
|
Changes to Outputs:
|
||||||
+ final_output = "just a simple resource to ensure that the second provider it's working fine"
|
+ final_output = "just a simple resource to ensure that the second provider it's working fine"`
|
||||||
+ out_ephemeral = "rawvalue"`
|
|
||||||
|
|
||||||
entriesChecker := &outputEntriesChecker{phase: "plan"}
|
entriesChecker := &outputEntriesChecker{phase: "plan"}
|
||||||
entriesChecker.addChecks(outputEntry{[]string{"data.simple_resource.test_data1: Reading..."}, true},
|
entriesChecker.addChecks(outputEntry{[]string{"data.simple_resource.test_data1: Reading..."}, true},
|
||||||
@@ -321,6 +320,7 @@ Changes to Outputs:
|
|||||||
|
|
||||||
if !strings.Contains(out, expectedChangesOutput) {
|
if !strings.Contains(out, expectedChangesOutput) {
|
||||||
t.Errorf("wrong plan output:\nstdout:%s\nstderr:%s", stdout, stderr)
|
t.Errorf("wrong plan output:\nstdout:%s\nstderr:%s", stdout, stderr)
|
||||||
|
t.Log(cmp.Diff(out, expectedChangesOutput))
|
||||||
}
|
}
|
||||||
entriesChecker.check(t, out)
|
entriesChecker.check(t, out)
|
||||||
|
|
||||||
@@ -443,6 +443,7 @@ Changes to Outputs:
|
|||||||
|
|
||||||
if !strings.Contains(out, expectedChangesOutput) {
|
if !strings.Contains(out, expectedChangesOutput) {
|
||||||
t.Errorf("wrong apply output:\nstdout:%s\nstderr%s", stdout, stderr)
|
t.Errorf("wrong apply output:\nstdout:%s\nstderr%s", stdout, stderr)
|
||||||
|
t.Log(cmp.Diff(out, expectedChangesOutput))
|
||||||
}
|
}
|
||||||
entriesChecker.check(t, out)
|
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.
|
// 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" {
|
output "final_output" {
|
||||||
value = simple_resource.test_res_second_provider.value
|
value = simple_resource.test_res_second_provider.value
|
||||||
}
|
}
|
||||||
@@ -8,8 +8,3 @@ output "out1" {
|
|||||||
value = var.in
|
value = var.in
|
||||||
ephemeral = true // NOTE: because
|
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 {
|
type output struct {
|
||||||
Sensitive bool `json:"sensitive,omitempty"`
|
Sensitive bool `json:"sensitive,omitempty"`
|
||||||
|
Ephemeral bool `json:"ephemeral,omitempty"`
|
||||||
Deprecated string `json:"deprecated,omitempty"`
|
Deprecated string `json:"deprecated,omitempty"`
|
||||||
Expression *expression `json:"expression,omitempty"`
|
Expression *expression `json:"expression,omitempty"`
|
||||||
DependsOn []string `json:"depends_on,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 {
|
for _, v := range c.Module.Outputs {
|
||||||
o := output{
|
o := output{
|
||||||
Sensitive: v.Sensitive,
|
Sensitive: v.Sensitive,
|
||||||
|
Ephemeral: v.Ephemeral,
|
||||||
Deprecated: v.Deprecated,
|
Deprecated: v.Deprecated,
|
||||||
}
|
}
|
||||||
if !inSingleModuleMode(schemas) {
|
if !inSingleModuleMode(schemas) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/hashicorp/hcl/v2"
|
||||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||||
"github.com/opentofu/opentofu/internal/addrs"
|
"github.com/opentofu/opentofu/internal/addrs"
|
||||||
"github.com/opentofu/opentofu/internal/configs"
|
"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.
|
// TODO: More test cases covering things other than input variables.
|
||||||
// (For now the other details are mainly tested in package command,
|
// (For now the other details are mainly tested in package command,
|
||||||
// as part of the tests for "tofu show".)
|
// as part of the tests for "tofu show".)
|
||||||
|
|||||||
@@ -365,6 +365,15 @@ func newDiagnosticDifference(diag tfdiags.Diagnostic) *jsonplan.Change {
|
|||||||
|
|
||||||
lhs, _ := binExpr.LHS.Value(ctx)
|
lhs, _ := binExpr.LHS.Value(ctx)
|
||||||
rhs, _ := binExpr.RHS.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)
|
change, err := jsonplan.GenerateChange(lhs, rhs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ import (
|
|||||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
"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) {
|
func OutputsFromMap(outputValues map[string]*states.OutputValue) (jsonentities.Outputs, tfdiags.Diagnostics) {
|
||||||
var diags tfdiags.Diagnostics
|
var diags tfdiags.Diagnostics
|
||||||
|
|
||||||
|
|||||||
@@ -450,9 +450,6 @@ func changeFromTfplan(rawChange *planproto.Change) (*plans.ChangeSrc, error) {
|
|||||||
}
|
}
|
||||||
ret.GeneratedConfig = rawChange.GeneratedConfig
|
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)
|
sensitive := cty.NewValueMarks(marks.Sensitive)
|
||||||
beforeValMarks, err := pathValueMarksFromTfplan(rawChange.BeforeSensitivePaths, sensitive)
|
beforeValMarks, err := pathValueMarksFromTfplan(rawChange.BeforeSensitivePaths, sensitive)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -264,9 +264,6 @@ func evalCheckErrorMessage(expr hcl.Expression, hclCtx *hcl.EvalContext) (string
|
|||||||
return "", diags
|
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()
|
val, valMarks := val.Unmark()
|
||||||
if _, sensitive := valMarks[marks.Sensitive]; sensitive {
|
if _, sensitive := valMarks[marks.Sensitive]; sensitive {
|
||||||
diags = diags.Append(&hcl.Diagnostic{
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
@@ -281,6 +278,19 @@ You can correct this by removing references to sensitive values, or by carefully
|
|||||||
})
|
})
|
||||||
return "", diags
|
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,
|
// NOTE: We've discarded any other marks the string might have been carrying,
|
||||||
// aside from the sensitive mark.
|
// 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{}
|
stateMap := map[addrs.InstanceKey]map[string]cty.Value{}
|
||||||
for _, output := range d.Evaluator.State.ModuleOutputs(d.ModulePath, addr) {
|
for _, output := range d.Evaluator.State.ModuleOutputs(d.ModulePath, addr) {
|
||||||
val := output.Value
|
val := output.Value
|
||||||
// TODO ephemeral - outputs need to get the ephemeral attribute too and then it can be used here
|
|
||||||
if output.Sensitive {
|
if output.Sensitive {
|
||||||
val = val.Mark(marks.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 != "" {
|
if output.Deprecated != "" {
|
||||||
val = marks.DeprecatedOutput(val, output.Addr, output.Deprecated, parentCfg.IsModuleCallFromRemoteModule(addr.Name))
|
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
|
unknownMap[cfg.Name] = cty.DynamicPseudoType
|
||||||
|
|
||||||
// get all instance output for this path from the state
|
// get all instance output for this path from the state
|
||||||
for key, states := range stateMap {
|
for key, outputStates := range stateMap {
|
||||||
outputState, ok := states[cfg.Name]
|
outputState, ok := outputStates[cfg.Name]
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -484,6 +492,12 @@ func (d *evaluationStateData) GetModule(_ context.Context, addr addrs.ModuleCall
|
|||||||
if change.Sensitive {
|
if change.Sensitive {
|
||||||
instance[cfg.Name] = change.After.Mark(marks.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 != "" {
|
if cfg.Deprecated != "" {
|
||||||
instance[cfg.Name] = marks.DeprecatedOutput(change.After, change.Addr, cfg.Deprecated, parentCfg.IsModuleCallFromRemoteModule(addr.Name))
|
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 {
|
if output.Sensitive {
|
||||||
val = val.Mark(marks.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 != "" {
|
if config.Deprecated != "" {
|
||||||
isRemote := false
|
isRemote := false
|
||||||
if p := moduleConfig.Path; p != nil && !p.IsRoot() {
|
if p := moduleConfig.Path; p != nil && !p.IsRoot() {
|
||||||
|
|||||||
@@ -111,6 +111,10 @@ func TestEvaluatorGetOutputValue(t *testing.T) {
|
|||||||
Name: "some_output",
|
Name: "some_output",
|
||||||
Sensitive: true,
|
Sensitive: true,
|
||||||
},
|
},
|
||||||
|
"ephemeral_output": {
|
||||||
|
Name: "ephemeral_output",
|
||||||
|
Ephemeral: true,
|
||||||
|
},
|
||||||
"some_other_output": {
|
"some_other_output": {
|
||||||
Name: "some_other_output",
|
Name: "some_other_output",
|
||||||
},
|
},
|
||||||
@@ -130,6 +134,12 @@ func TestEvaluatorGetOutputValue(t *testing.T) {
|
|||||||
Name: "some_other_output",
|
Name: "some_other_output",
|
||||||
},
|
},
|
||||||
}, cty.StringVal("second"), false, "")
|
}, cty.StringVal("second"), false, "")
|
||||||
|
state.SetOutputValue(addrs.AbsOutputValue{
|
||||||
|
Module: addrs.RootModuleInstance,
|
||||||
|
OutputValue: addrs.OutputValue{
|
||||||
|
Name: "ephemeral_output",
|
||||||
|
},
|
||||||
|
}, cty.StringVal("third"), false, "")
|
||||||
}).SyncWrapper(),
|
}).SyncWrapper(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,6 +171,20 @@ func TestEvaluatorGetOutputValue(t *testing.T) {
|
|||||||
if !got.RawEquals(want) {
|
if !got.RawEquals(want) {
|
||||||
t.Errorf("wrong result %#v; want %#v", got, want)
|
t.Errorf("wrong result %#v; want %#v", got, want)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO ephemeral - uncomment the line with the ephemeral mark once the testing support implementation is done
|
||||||
|
// 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
|
// This particularly tests that a sensitive attribute in config
|
||||||
@@ -785,13 +809,22 @@ func TestEvaluatorGetModule(t *testing.T) {
|
|||||||
true,
|
true,
|
||||||
"",
|
"",
|
||||||
)
|
)
|
||||||
|
ss.SetOutputValue(
|
||||||
|
addrs.OutputValue{Name: "out2"}.Absolute(addrs.ModuleInstance{addrs.ModuleInstanceStep{Name: "mod"}}),
|
||||||
|
cty.StringVal("baz"),
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
)
|
||||||
}).SyncWrapper()
|
}).SyncWrapper()
|
||||||
evaluator := evaluatorForModule(stateSync, plans.NewChanges().SyncWrapper())
|
evaluator := evaluatorForModule(stateSync, plans.NewChanges().SyncWrapper())
|
||||||
data := &evaluationStateData{
|
data := &evaluationStateData{
|
||||||
Evaluator: evaluator,
|
Evaluator: evaluator,
|
||||||
}
|
}
|
||||||
scope := evaluator.Scope(data, nil, nil, nil)
|
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{
|
got, diags := scope.Data.GetModule(t.Context(), addrs.ModuleCall{
|
||||||
Name: "mod",
|
Name: "mod",
|
||||||
}, tfdiags.SourceRange{})
|
}, tfdiags.SourceRange{})
|
||||||
@@ -800,7 +833,7 @@ func TestEvaluatorGetModule(t *testing.T) {
|
|||||||
t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
|
t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
|
||||||
}
|
}
|
||||||
if !got.RawEquals(want) {
|
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
|
// Changes should override the state value
|
||||||
@@ -814,12 +847,23 @@ func TestEvaluatorGetModule(t *testing.T) {
|
|||||||
}
|
}
|
||||||
cs, _ := change.Encode()
|
cs, _ := change.Encode()
|
||||||
changesSync.AppendOutputChange(cs)
|
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)
|
evaluator = evaluatorForModule(stateSync, changesSync)
|
||||||
data = &evaluationStateData{
|
data = &evaluationStateData{
|
||||||
Evaluator: evaluator,
|
Evaluator: evaluator,
|
||||||
}
|
}
|
||||||
scope = evaluator.Scope(data, nil, nil, nil)
|
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{
|
got, diags = scope.Data.GetModule(t.Context(), addrs.ModuleCall{
|
||||||
Name: "mod",
|
Name: "mod",
|
||||||
}, tfdiags.SourceRange{})
|
}, tfdiags.SourceRange{})
|
||||||
@@ -837,7 +881,10 @@ func TestEvaluatorGetModule(t *testing.T) {
|
|||||||
Evaluator: evaluator,
|
Evaluator: evaluator,
|
||||||
}
|
}
|
||||||
scope = evaluator.Scope(data, nil, nil, nil)
|
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{
|
got, diags = scope.Data.GetModule(t.Context(), addrs.ModuleCall{
|
||||||
Name: "mod",
|
Name: "mod",
|
||||||
}, tfdiags.SourceRange{})
|
}, tfdiags.SourceRange{})
|
||||||
@@ -872,6 +919,10 @@ func evaluatorForModule(stateSync *states.SyncState, changesSync *plans.ChangesS
|
|||||||
Name: "out",
|
Name: "out",
|
||||||
Sensitive: true,
|
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(),
|
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) {
|
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.
|
// 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,
|
// If the value is unknown due to the referenced values and inherited the marks from those,
|
||||||
// we do want to validate though.
|
// we do want to validate though.
|
||||||
@@ -464,6 +465,16 @@ func (n *NodeApplyableOutput) validateEphemerality(val cty.Value) (diags tfdiags
|
|||||||
Subject: n.Config.UsageRange().Ptr(),
|
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
|
return diags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user