Skip to content

Commit

Permalink
stackeval: Ephemeral output values
Browse files Browse the repository at this point in the history
Now we'll mark the result of an ephemeral output value as ephemeral, and
reject attempts to assign any ephemeral values to a non-ephemeral output
value.
  • Loading branch information
apparentlymart committed Jun 17, 2024
1 parent 71749d9 commit 79f78b2
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 6 deletions.
42 changes: 38 additions & 4 deletions internal/stacks/stackruntime/internal/stackeval/output_value.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/zclconf/go-cty/cty/convert"

"github.com/hashicorp/terraform/internal/collections"
"github.com/hashicorp/terraform/internal/lang/marks"
"github.com/hashicorp/terraform/internal/promising"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/stacks/stackconfig"
Expand Down Expand Up @@ -116,19 +117,20 @@ func (v *OutputValue) CheckResultValue(ctx context.Context, phase EvalPhase) (ct
func(ctx context.Context) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics

cfg := v.Config(ctx)
ty, defs := v.ResultType(ctx)

stack := v.Stack(ctx, phase)
if stack == nil {
// If we're in a stack whose expansion isn't known yet then
// we'll return an unknown value placeholder so that
// downstreams can at least do type-checking.
return cty.UnknownVal(ty), diags
return cfg.markResultValue(cty.UnknownVal(ty)), diags
}
result, moreDiags := EvalExprAndEvalContext(ctx, v.Declaration(ctx).Value, phase, stack)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
return cty.UnknownVal(ty), diags
return cfg.markResultValue(cty.UnknownVal(ty)), diags
}

var err error
Expand All @@ -142,10 +144,42 @@ func (v *OutputValue) CheckResultValue(ctx context.Context, phase EvalPhase) (ct
"Invalid output value result",
fmt.Sprintf("Unsuitable value for output %q: %s.", v.Addr().Item.Name, tfdiags.FormatError(err)),
))
return cty.UnknownVal(ty), diags
return cfg.markResultValue(cty.UnknownVal(ty)), diags
}

return result.Value, diags
if !cfg.Declaration(ctx).Ephemeral {
_, markses := result.Value.UnmarkDeepWithPaths()
problemPaths, _ := marks.PathsWithMark(markses, marks.Ephemeral)
var moreDiags tfdiags.Diagnostics
for _, path := range problemPaths {
if len(path) == 0 {
moreDiags = moreDiags.Append(result.Diagnostic(
tfdiags.Error,
"Ephemeral value not allowed",
fmt.Sprintf("The output value %q does not accept ephemeral values.", v.Addr().Item.Name),
))
} else {
moreDiags = moreDiags.Append(result.Diagnostic(
tfdiags.Error,
"Ephemeral value not allowed",
fmt.Sprintf(
"The output value %q does not accept ephemeral values, so the value of %s is not compatible.",
v.Addr().Item.Name,
tfdiags.FormatCtyPath(path),
),
))
}
}
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
// We return an unknown value placeholder here to avoid
// the risk of a recipient of this value using it in a
// way that would be inappropriate for an ephemeral value.
result.Value = cty.UnknownVal(ty)
}
}

return cfg.markResultValue(result.Value), diags
},
))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"

"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/lang/marks"
"github.com/hashicorp/terraform/internal/promising"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/stacks/stackconfig"
Expand Down Expand Up @@ -101,7 +102,7 @@ func (ov *OutputValueConfig) validateValueInner(ctx context.Context) (cty.Value,
v := result.Value
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
v = cty.UnknownVal(ov.ValueTypeConstraint(ctx))
v = ov.markResultValue(cty.UnknownVal(ov.ValueTypeConstraint(ctx)))
}

var err error
Expand All @@ -121,7 +122,18 @@ func (ov *OutputValueConfig) validateValueInner(ctx context.Context) (cty.Value,
})
}

return v, diags
return ov.markResultValue(v), diags
}

func (ov *OutputValueConfig) markResultValue(v cty.Value) cty.Value {
decl := ov.config
if decl.Sensitive {
v = v.Mark(marks.Sensitive)
}
if decl.Ephemeral {
v = v.Mark(marks.Ephemeral)
}
return v
}

func (ov *OutputValueConfig) checkValid(ctx context.Context, phase EvalPhase) tfdiags.Diagnostics {
Expand Down
100 changes: 100 additions & 0 deletions internal/stacks/stackruntime/internal/stackeval/output_value_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ import (
"context"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/lang/marks"
"github.com/hashicorp/terraform/internal/promising"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/zclconf/go-cty-debug/ctydebug"
"github.com/zclconf/go-cty/cty"
)

Expand Down Expand Up @@ -71,6 +74,16 @@ func TestOutputValueResultValue(t *testing.T) {
WantRootVal: cty.UnknownVal(cty.String),
WantChildVal: cty.UnknownVal(cty.String),
},
"ephemeral value when not allowed": {
RootVal: cty.StringVal("root value").Mark(marks.Ephemeral),
ChildVal: cty.StringVal("child value").Mark(marks.Ephemeral),

WantRootVal: cty.UnknownVal(cty.String),
WantChildVal: cty.UnknownVal(cty.String),

WantRootErr: `The output value "root" does not accept ephemeral values.`,
WantChildErr: `The output value "foo" does not accept ephemeral values.`,
},
}

for name, test := range tests {
Expand Down Expand Up @@ -178,3 +191,90 @@ func TestOutputValueResultValue(t *testing.T) {
})
}
}

func TestOutputValueEphemeral(t *testing.T) {
ctx := context.Background()

tests := map[string]struct {
fixtureName string
givenVal cty.Value
allowed bool
wantVal cty.Value
}{
"ephemeral and allowed": {
fixtureName: "ephemeral_yes",
givenVal: cty.StringVal("beep").Mark(marks.Ephemeral),
allowed: true,
wantVal: cty.StringVal("beep").Mark(marks.Ephemeral),
},
"ephemeral and not allowed": {
fixtureName: "ephemeral_no",
givenVal: cty.StringVal("beep").Mark(marks.Ephemeral),
allowed: false,
wantVal: cty.UnknownVal(cty.String),
},
"non-ephemeral and allowed": {
fixtureName: "ephemeral_yes",
givenVal: cty.StringVal("beep"),
allowed: true,
wantVal: cty.StringVal("beep").Mark(marks.Ephemeral),
},
"non-ephemeral and not allowed": {
fixtureName: "ephemeral_no",
givenVal: cty.StringVal("beep"),
allowed: true,
wantVal: cty.StringVal("beep"),
},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
cfg := testStackConfig(t, "output_value", test.fixtureName)
outputAddr := stackaddrs.OutputValue{Name: "result"}

main := testEvaluator(t, testEvaluatorOpts{
Config: cfg,
TestOnlyGlobals: map[string]cty.Value{
"result": test.givenVal,
},
})

promising.MainTask(ctx, func(ctx context.Context) (struct{}, error) {
stack := main.MainStack(ctx)
output := stack.OutputValues(ctx)[outputAddr]
if output == nil {
t.Fatalf("missing %s", outputAddr)
}
want := test.wantVal
got, diags := output.CheckResultValue(ctx, InspectPhase)
if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" {
t.Errorf("wrong value for %s\n%s", outputAddr, diff)
}

if test.allowed {
if diags.HasErrors() {
t.Errorf("unexpected errors\n%s", diags.Err().Error())
}
} else {
if !diags.HasErrors() {
t.Fatalf("no errors; should have failed")
}
found := 0
for _, diag := range diags {
summary := diag.Description().Summary
if summary == "Ephemeral value not allowed" {
found++
}
}
if found == 0 {
t.Errorf("no diagnostics about disallowed ephemeral values\n%s", diags.Err().Error())
} else if found > 1 {
t.Errorf("found %d errors about disallowed ephemeral values, but wanted only one\n%s", found, diags.Err().Error())
}
}
return struct{}{}, nil
})
})
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
output "result" {
type = string
value = _test_only_global.result
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
output "result" {
type = string
value = _test_only_global.result
ephemeral = true
}

0 comments on commit 79f78b2

Please sign in to comment.