Module expander for enabled field

Signed-off-by: Diogenes Fernandes <diofeher@gmail.com>
This commit is contained in:
Diogenes Fernandes
2025-07-21 15:52:57 -03:00
committed by Diógenes Fernandes
parent ca53b2521d
commit 732623f604
6 changed files with 109 additions and 7 deletions

View File

@@ -38,6 +38,7 @@ type ModuleCall struct {
Count hcl.Expression
ForEach hcl.Expression
Enabled hcl.Expression
Providers []PassedProviderConfig

View File

@@ -59,6 +59,12 @@ func (e *Expander) SetModuleSingle(parentAddr addrs.ModuleInstance, callAddr add
e.setModuleExpansion(parentAddr, callAddr, expansionSingleVal)
}
// SetModuleEnabled records that the given module call inside the given parent
// module uses the "enabled" lifecycle repetition argument, with the given value.
func (e *Expander) SetModuleEnabled(parentAddr addrs.ModuleInstance, callAddr addrs.ModuleCall, enabled bool) {
e.setModuleExpansion(parentAddr, callAddr, expansionEnabled(enabled))
}
// SetModuleCount records that the given module call inside the given parent
// module instance uses the "count" repetition argument, with the given value.
func (e *Expander) SetModuleCount(parentAddr addrs.ModuleInstance, callAddr addrs.ModuleCall, count int) {

View File

@@ -22,6 +22,7 @@ func TestExpander(t *testing.T) {
count2ModuleAddr := addrs.ModuleCall{Name: "count2"}
count0ModuleAddr := addrs.ModuleCall{Name: "count0"}
forEachModuleAddr := addrs.ModuleCall{Name: "for_each"}
enabledModuleAddr := addrs.ModuleCall{Name: "enabled"}
singleResourceAddr := addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test",
@@ -42,6 +43,11 @@ func TestExpander(t *testing.T) {
Type: "test",
Name: "for_each",
}
enabledResourceAddr := addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test",
Name: "enabled",
}
eachMap := map[string]cty.Value{
"a": cty.NumberIntVal(1),
"b": cty.NumberIntVal(2),
@@ -88,6 +94,7 @@ func TestExpander(t *testing.T) {
ex.SetResourceCount(addrs.RootModuleInstance, count2ResourceAddr, 2)
ex.SetResourceCount(addrs.RootModuleInstance, count0ResourceAddr, 0)
ex.SetResourceForEach(addrs.RootModuleInstance, forEachResourceAddr, eachMap)
ex.SetResourceEnabled(addrs.RootModuleInstance, enabledResourceAddr, true)
ex.SetModuleSingle(addrs.RootModuleInstance, singleModuleAddr)
{
@@ -97,6 +104,12 @@ func TestExpander(t *testing.T) {
ex.SetResourceCount(moduleInstanceAddr, count2ResourceAddr, 2)
}
ex.SetModuleEnabled(addrs.RootModuleInstance, enabledModuleAddr, true)
{
moduleInstanceAddr := addrs.RootModuleInstance.Child("enabled", addrs.NoKey)
ex.SetResourceSingle(moduleInstanceAddr, singleResourceAddr)
}
ex.SetModuleCount(addrs.RootModuleInstance, count2ModuleAddr, 2)
for i1 := 0; i1 < 2; i1++ {
moduleInstanceAddr := addrs.RootModuleInstance.Child("count2", addrs.IntKey(i1))
@@ -146,6 +159,19 @@ func TestExpander(t *testing.T) {
t.Errorf("wrong result\n%s", diff)
}
})
t.Run("resource enabled", func(t *testing.T) {
got := ex.ExpandModuleResource(
addrs.RootModule,
enabledResourceAddr,
)
want := []addrs.AbsResourceInstance{
mustAbsResourceInstanceAddr(`test.enabled`),
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("wrong result\n%s", diff)
}
})
t.Run("resource count2", func(t *testing.T) {
got := ex.ExpandModuleResource(
addrs.RootModule,
@@ -169,6 +195,18 @@ func TestExpander(t *testing.T) {
t.Errorf("wrong result\n%s", diff)
}
})
t.Run("resource enabled", func(t *testing.T) {
got := ex.ExpandModuleResource(
addrs.RootModule,
enabledResourceAddr,
)
want := []addrs.AbsResourceInstance{
mustAbsResourceInstanceAddr(`test.enabled`),
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("wrong result\n%s", diff)
}
})
t.Run("resource for_each", func(t *testing.T) {
got := ex.ExpandModuleResource(
addrs.RootModule,
@@ -191,6 +229,16 @@ func TestExpander(t *testing.T) {
t.Errorf("wrong result\n%s", diff)
}
})
t.Run("module enabled", func(t *testing.T) {
got := ex.ExpandModule(addrs.RootModule.Child("enabled"))
want := []addrs.ModuleInstance{
mustModuleInstanceAddr(`module.enabled`),
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("wrong result\n%s", diff)
}
})
t.Run("module single resource single", func(t *testing.T) {
got := ex.ExpandModuleResource(
mustModuleAddr("single"),

View File

@@ -565,16 +565,23 @@ func (d *evaluationStateData) GetModule(_ context.Context, addr addrs.ModuleCall
}
default:
val, ok := moduleInstances[addrs.NoKey]
vals, ok := moduleInstances[addrs.NoKey]
if !ok {
// create the object if there wasn't one known
val = map[string]cty.Value{}
for k := range outputConfigs {
val[k] = cty.DynamicVal
if callConfig.Enabled == nil {
// create the object if there wasn't one known
vals = map[string]cty.Value{}
for k := range outputConfigs {
vals[k] = cty.DynamicVal
}
ret = cty.ObjectVal(vals)
} else {
// when we're using enabled it's okay to have no
// instance, and the entire object is null.
ret = cty.NullVal(cty.DynamicPseudoType)
}
} else {
ret = cty.ObjectVal(vals)
}
ret = cty.ObjectVal(val)
}
// The module won't be expanded during validation, so we need to return an

View File

@@ -77,6 +77,10 @@ func (n *nodeExpandModule) References() []*addrs.Reference {
forEachRefs, _ := lang.ReferencesInExpr(addrs.ParseRef, n.ModuleCall.ForEach)
refs = append(refs, forEachRefs...)
}
if n.ModuleCall.Enabled != nil {
enabledRefs, _ := lang.ReferencesInExpr(addrs.ParseRef, n.ModuleCall.Enabled)
refs = append(refs, enabledRefs...)
}
for _, passed := range n.ModuleCall.Providers {
if passed.InParent.KeyExpression != nil {
@@ -142,6 +146,23 @@ func (n *nodeExpandModule) Execute(ctx context.Context, evalCtx EvalContext, op
}
expander.SetModuleForEach(module, call, forEach)
case n.ModuleCall.Enabled != nil:
// For enabled expressions, we need to evaluate in the parent module context
// since the expression may reference variables defined in the parent module. e.g.
// variable "on" { type = bool }
// module "mod1" {
// source = "./mod1"
// lifecycle {
// enabled = var.on
// }
// }
parentEvalCtx := evalCtx.WithPath(module.Parent())
enabled, enDiags := evaluateEnabledExpression(ctx, n.ModuleCall.Enabled, parentEvalCtx)
diags = diags.Append(enDiags)
if diags.HasErrors() {
return diags
}
expander.SetModuleEnabled(module, call, enabled)
default:
expander.SetModuleSingle(module, call)
}

View File

@@ -0,0 +1,19 @@
variable "on" {
type = bool
}
module "mod1" {
source = "./mod1"
lifecycle {
enabled = var.on
}
}
output "result" {
// This is in a 1-tuple just because OpenTofu treats a fully-null
// root module output value as if it wasn't declared at all,
// but we want to make sure we're actually testing the result
// of this resource directly.
value = [try(module.mod1.result, "")]
}