Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/v1.14/BUG FIXES-20251024-164434.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: BUG FIXES
body: 'test: allow ephemeral outputs in root modules'
time: 2025-10-24T16:44:34.197847+02:00
custom:
Issue: "37813"
5 changes: 5 additions & 0 deletions .changes/v1.14/BUG FIXES-20251024-164448.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: BUG FIXES
body: 'stacks: allow ephemeral outputs in root modules'
time: 2025-10-24T16:44:48.264142+02:00
custom:
Issue: "37813"
3 changes: 3 additions & 0 deletions internal/command/test_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,9 @@ func TestTest_Runs(t *testing.T) {
expectedOut: []string{"3 passed, 0 failed."},
code: 0,
},
"ephemeral_output": {
code: 0,
},
"no-tests": {
code: 0,
},
Expand Down
8 changes: 8 additions & 0 deletions internal/command/testdata/test/ephemeral_output/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
variable "foo" {
ephemeral = true
type = string
}
output "value" {
value = var.foo
ephemeral = true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
run "validate_ephemeral_input" {
variables {
foo = "whaaat"
}
}
3 changes: 3 additions & 0 deletions internal/configs/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -1039,6 +1039,9 @@ func (c *Config) EffectiveRequiredProviderConfigs() addrs.Map[addrs.RootProvider
for _, rc := range c.Module.Actions {
maybePutLocal(rc.ProviderConfigAddr(), false)
}
for _, rc := range c.Module.EphemeralResources {
maybePutLocal(rc.ProviderConfigAddr(), false)
}
for _, ic := range c.Module.Import {
if ic.ProviderConfigRef != nil {
maybePutLocal(addrs.LocalProviderConfig{
Expand Down
30 changes: 16 additions & 14 deletions internal/moduletest/graph/node_state_cleanup.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,14 @@ func (n *NodeStateCleanup) restore(ctx *EvalContext, file *configs.TestFile, run
setVariables, _, _ := FilterVariablesToModule(module, variables)

planOpts := &terraform.PlanOpts{
Mode: plans.NormalMode,
SetVariables: setVariables,
Overrides: mocking.PackageOverrides(run, file, mocks),
ExternalProviders: providers,
SkipRefresh: true,
OverridePreventDestroy: true,
DeferralAllowed: ctx.deferralAllowed,
Mode: plans.NormalMode,
SetVariables: setVariables,
Overrides: mocking.PackageOverrides(run, file, mocks),
ExternalProviders: providers,
SkipRefresh: true,
OverridePreventDestroy: true,
DeferralAllowed: ctx.deferralAllowed,
AllowRootEphemeralOutputs: true,
}

tfCtx, _ := terraform.NewContext(n.opts.ContextOpts)
Expand Down Expand Up @@ -177,13 +178,14 @@ func (n *NodeStateCleanup) destroy(ctx *EvalContext, file *configs.TestFile, run
setVariables, _, _ := FilterVariablesToModule(module, variables)

planOpts := &terraform.PlanOpts{
Mode: plans.DestroyMode,
SetVariables: setVariables,
Overrides: mocking.PackageOverrides(run, file, mocks),
ExternalProviders: providers,
SkipRefresh: true,
OverridePreventDestroy: true,
DeferralAllowed: ctx.deferralAllowed,
Mode: plans.DestroyMode,
SetVariables: setVariables,
Overrides: mocking.PackageOverrides(run, file, mocks),
ExternalProviders: providers,
SkipRefresh: true,
OverridePreventDestroy: true,
DeferralAllowed: ctx.deferralAllowed,
AllowRootEphemeralOutputs: true,
}

tfCtx, _ := terraform.NewContext(n.opts.ContextOpts)
Expand Down
3 changes: 2 additions & 1 deletion internal/moduletest/graph/node_test_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,8 @@ func (n *NodeTestRun) testValidate(providers map[addrs.RootProviderConfig]provid
}
waiter.update(tfCtx, moduletest.Running, nil)
validateDiags := tfCtx.Validate(config, &terraform.ValidateOpts{
ExternalProviders: providers,
ExternalProviders: providers,
AllowRootEphemeralOutputs: true,
})
run.Diagnostics = run.Diagnostics.Append(validateDiags)
if validateDiags.HasErrors() {
Expand Down
17 changes: 9 additions & 8 deletions internal/moduletest/graph/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,15 @@ func plan(ctx *EvalContext, tfCtx *terraform.Context, file *configs.TestFile, ru
return plans.NormalMode
}
}(),
Targets: targets,
ForceReplace: replaces,
SkipRefresh: !run.Options.Refresh,
SetVariables: variables,
ExternalReferences: references,
ExternalProviders: providers,
Overrides: mocking.PackageOverrides(run, file, mocks),
DeferralAllowed: ctx.deferralAllowed,
Targets: targets,
ForceReplace: replaces,
SkipRefresh: !run.Options.Refresh,
SetVariables: variables,
ExternalReferences: references,
ExternalProviders: providers,
Overrides: mocking.PackageOverrides(run, file, mocks),
DeferralAllowed: ctx.deferralAllowed,
AllowRootEphemeralOutputs: true,
}

waiter.update(tfCtx, moduletest.Running, nil)
Expand Down
63 changes: 63 additions & 0 deletions internal/stacks/stackruntime/apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2229,6 +2229,69 @@ After applying this plan, Terraform will no longer manage these objects. You wil
},
},
},
"ephemeral-module-outputs": {
path: "ephemeral-module-output",
cycles: []TestCycle{
{
wantPlannedChanges: []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("component.ephemeral_in"),
PlanApplyable: false,
PlanComplete: true,
Action: plans.Create,
RequiredComponents: collections.NewSet(mustAbsComponent("component.ephemeral_out")),
PlannedInputValues: make(map[string]plans.DynamicValue),
PlannedOutputValues: make(map[string]cty.Value),
PlannedCheckResults: new(states.CheckResults),
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("component.ephemeral_out"),
PlanApplyable: false,
PlanComplete: true,
Action: plans.Create,
PlannedInputValues: make(map[string]plans.DynamicValue),
PlannedOutputValues: map[string]cty.Value{
"value": cty.DynamicVal, // ephemeral
},
PlannedCheckResults: new(states.CheckResults),
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
},
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.ephemeral_in"),
ComponentInstanceAddr: mustAbsComponentInstance("component.ephemeral_in"),
Dependencies: collections.NewSet[stackaddrs.AbsComponent](
mustAbsComponent("component.ephemeral_out"),
),
OutputValues: make(map[addrs.OutputValue]cty.Value),
InputVariables: map[addrs.InputVariable]cty.Value{
mustInputVariable("input"): cty.UnknownVal(cty.String), // ephemeral
},
},
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.ephemeral_out"),
ComponentInstanceAddr: mustAbsComponentInstance("component.ephemeral_out"),
Dependents: collections.NewSet[stackaddrs.AbsComponent](
mustAbsComponent("component.ephemeral_in"),
),
OutputValues: make(map[addrs.OutputValue]cty.Value),
InputVariables: make(map[addrs.InputVariable]cty.Value),
},
},
},
},
},
}

for name, tc := range tcs {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,8 @@ func (c *ComponentConfig) checkValid(ctx context.Context, phase EvalPhase) tfdia
}()

diags = diags.Append(tfCtx.Validate(moduleTree, &terraform.ValidateOpts{
ExternalProviders: providerClients,
ExternalProviders: providerClients,
AllowRootEphemeralOutputs: true,
}))
return diags, nil
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ func (c *ComponentInstance) PlanOpts(ctx context.Context, mode plans.Mode, skipR
ExternalProviders: providerClients,
ExternalDependencyDeferred: c.deferred,
DeferralAllowed: true,
AllowRootEphemeralOutputs: true,

// We want the same plantimestamp between all components and the stacks language
ForcePlanTimestamp: &plantimestamp,
}, nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,8 @@ func (r *RemovedComponentConfig) CheckValid(ctx context.Context, phase EvalPhase
}()

diags = diags.Append(tfCtx.Validate(moduleTree, &terraform.ValidateOpts{
ExternalProviders: providerClients,
ExternalProviders: providerClients,
AllowRootEphemeralOutputs: true,
}))
return diags, nil
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ func (r *RemovedComponentInstance) ModuleTreePlan(ctx context.Context) (*plans.P
DeferralAllowed: true,
ExternalDependencyDeferred: deferred,
Forget: forget,
AllowRootEphemeralOutputs: true,

// We want the same plantimestamp between all components and the stacks language
ForcePlanTimestamp: &plantimestamp,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

variable "input" {
type = string
ephemeral = true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
required_providers {
testing = {
source = "hashicorp/testing"
version = "0.1.0"
}
}

provider "testing" "main" {}

component "ephemeral_out" {
source = "./ephemeral-output"

providers = {
testing = provider.testing.main
}
}

component "ephemeral_in" {
source = "./ephemeral-input"

providers = {
testing = provider.testing.main
}

inputs = {
input = component.ephemeral_out.value
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

ephemeral "testing_resource" "resource" {}

output "value" {
value = ephemeral.testing_resource.resource.value
ephemeral = true
}
23 changes: 23 additions & 0 deletions internal/stacks/stackruntime/testing/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ var (
},
}

TestingEphemeralResourceSchema = providers.Schema{
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"value": {
Type: cty.String,
Computed: true,
},
},
},
}

DeferredResourceSchema = providers.Schema{
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
Expand Down Expand Up @@ -199,6 +210,11 @@ func NewProviderWithData(t *testing.T, store *ResourceStore) *MockProvider {
Body: WriteOnlyDataSourceSchema.Body,
},
},
EphemeralResourceTypes: map[string]providers.Schema{
"testing_resource": {
Body: TestingEphemeralResourceSchema.Body,
},
},
Functions: map[string]providers.FunctionDecl{
"echo": {
Parameters: []providers.FunctionParam{
Expand Down Expand Up @@ -299,6 +315,13 @@ func NewProviderWithData(t *testing.T, store *ResourceStore) *MockProvider {
Result: request.Arguments[0],
}
},
OpenEphemeralResourceFn: func(request providers.OpenEphemeralResourceRequest) providers.OpenEphemeralResourceResponse {
return providers.OpenEphemeralResourceResponse{
Result: cty.ObjectVal(map[string]cty.Value{
"value": cty.StringVal("secret"),
}),
}
},
},
ResourceStore: store,
}
Expand Down
7 changes: 7 additions & 0 deletions internal/terraform/context_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ type ApplyOpts struct {
// values that were declared as ephemeral, because all other input
// values must retain the values that were specified during planning.
SetVariables InputValues

// AllowRootEphemeralOutputs overrides a specific check made within the
// output nodes that they cannot be ephemeral at within root modules. This
// should be set to true for plans executing from within either the stacks
// or test runtimes, where the root modules as Terraform sees them aren't
// the actual root modules.
AllowRootEphemeralOutputs bool
}

// ApplyOpts creates an [ApplyOpts] with copies of all of the elements that
Expand Down
Loading
Loading