diff --git a/internal/runtime/internal/controller/loader.go b/internal/runtime/internal/controller/loader.go index b12044545f..dc4fc68291 100644 --- a/internal/runtime/internal/controller/loader.go +++ b/internal/runtime/internal/controller/loader.go @@ -512,7 +512,7 @@ func (l *Loader) populateConfigBlockNodes(args map[string]any, g *dag.Graph, con node = exist.(BlockNode) node.UpdateBlock(block) } else { - node, newConfigNodeDiags = NewConfigNode(block, l.globals) + node, newConfigNodeDiags = NewConfigNode(block, l.globals, l.componentNodeManager.customComponentReg) diags = append(diags, newConfigNodeDiags...) if diags.HasErrors() { continue @@ -625,6 +625,8 @@ func (l *Loader) wireGraphEdges(g *dag.Graph) diag.Diagnostics { continue case *CustomComponentNode: l.wireCustomComponentNode(g, n) + case *ForeachConfigNode: + l.wireForEachNode(g, n) } // Finally, wire component references. @@ -656,6 +658,14 @@ func (l *Loader) wireCustomComponentNode(g *dag.Graph, cc *CustomComponentNode) } } +func (l *Loader) wireForEachNode(g *dag.Graph, fn *ForeachConfigNode) { + refs := l.findCustomComponentReferences(fn.Block()) + for ref := range refs { + // add edges between the foreach node and declare/import nodes. + g.AddEdge(dag.Edge{From: fn, To: ref}) + } +} + // Variables returns the Variables the Loader exposes for other components to // reference. func (l *Loader) Variables() map[string]interface{} { @@ -915,10 +925,10 @@ func (l *Loader) isRootController() bool { return l.globals.ControllerID == "" } -// findCustomComponentReferences returns references to import/declare nodes in a declare block. -func (l *Loader) findCustomComponentReferences(declare *ast.BlockStmt) map[BlockNode]struct{} { +// findCustomComponentReferences returns references to import/declare nodes in a block. +func (l *Loader) findCustomComponentReferences(block *ast.BlockStmt) map[BlockNode]struct{} { uniqueReferences := make(map[BlockNode]struct{}) - l.collectCustomComponentReferences(declare.Body, uniqueReferences) + l.collectCustomComponentReferences(block.Body, uniqueReferences) return uniqueReferences } @@ -938,7 +948,7 @@ func (l *Loader) collectCustomComponentReferences(stmts ast.Body, uniqueReferenc ) switch { - case componentName == declareType: + case componentName == declareType || componentName == templateType: l.collectCustomComponentReferences(blockStmt.Body, uniqueReferences) case foundDeclare: uniqueReferences[declareNode] = struct{}{} diff --git a/internal/runtime/internal/controller/node_config.go b/internal/runtime/internal/controller/node_config.go index ff8c6647db..c0eb346244 100644 --- a/internal/runtime/internal/controller/node_config.go +++ b/internal/runtime/internal/controller/node_config.go @@ -18,7 +18,7 @@ const ( // NewConfigNode creates a new ConfigNode from an initial ast.BlockStmt. // The underlying config isn't applied until Evaluate is called. -func NewConfigNode(block *ast.BlockStmt, globals ComponentGlobals) (BlockNode, diag.Diagnostics) { +func NewConfigNode(block *ast.BlockStmt, globals ComponentGlobals, customReg *CustomComponentRegistry) (BlockNode, diag.Diagnostics) { switch block.GetBlockName() { case argumentBlockID: return NewArgumentConfigNode(block, globals), nil @@ -31,7 +31,7 @@ func NewConfigNode(block *ast.BlockStmt, globals ComponentGlobals) (BlockNode, d case importsource.BlockImportFile, importsource.BlockImportString, importsource.BlockImportHTTP, importsource.BlockImportGit: return NewImportConfigNode(block, globals, importsource.GetSourceType(block.GetBlockName())), nil case foreachID: - return NewForeachConfigNode(block, globals), nil + return NewForeachConfigNode(block, globals, customReg), nil default: var diags diag.Diagnostics diags.Add(diag.Diagnostic{ diff --git a/internal/runtime/internal/controller/node_config_foreach.go b/internal/runtime/internal/controller/node_config_foreach.go index ed52dc0d82..8e87df2041 100644 --- a/internal/runtime/internal/controller/node_config_foreach.go +++ b/internal/runtime/internal/controller/node_config_foreach.go @@ -14,11 +14,17 @@ import ( "github.com/grafana/alloy/syntax/vm" ) +const templateType = "template" + type ForeachConfigNode struct { nodeID string label string moduleController ModuleController + // customReg is the customComponentRegistry of the current loader. + // We pass it so that the foreach children have access to modules. + customReg *CustomComponentRegistry + customComponents map[string]CustomComponent customComponentHashCounts map[string]int @@ -39,7 +45,7 @@ type ForeachArguments struct { Collection []string `alloy:"collection,attr"` } -func NewForeachConfigNode(block *ast.BlockStmt, globals ComponentGlobals) *ForeachConfigNode { +func NewForeachConfigNode(block *ast.BlockStmt, globals ComponentGlobals, customReg *CustomComponentRegistry) *ForeachConfigNode { nodeID := BlockComponentID(block).String() globalID := nodeID if globals.ControllerID != "" { @@ -51,6 +57,7 @@ func NewForeachConfigNode(block *ast.BlockStmt, globals ComponentGlobals) *Forea label: block.Label, block: block, moduleController: globals.NewModuleController(globalID), + customReg: customReg, forEachChildrenUpdateChan: make(chan struct{}, 1), customComponents: make(map[string]CustomComponent, 0), customComponentHashCounts: make(map[string]int, 0), @@ -61,7 +68,11 @@ func (fn *ForeachConfigNode) Label() string { return fn.label } func (fn *ForeachConfigNode) NodeID() string { return fn.nodeID } -func (fn *ForeachConfigNode) Block() *ast.BlockStmt { return fn.block } +func (fn *ForeachConfigNode) Block() *ast.BlockStmt { + fn.mut.RLock() + defer fn.mut.RUnlock() + return fn.block +} type Arguments struct { Collection []any `alloy:"collection,attr"` @@ -75,7 +86,7 @@ func (fn *ForeachConfigNode) Evaluate(scope *vm.Scope) error { var argsBody ast.Body var template *ast.BlockStmt for _, stmt := range fn.block.Body { - if blockStmt, ok := stmt.(*ast.BlockStmt); ok && blockStmt.GetBlockName() == "template" { + if blockStmt, ok := stmt.(*ast.BlockStmt); ok && blockStmt.GetBlockName() == templateType { template = blockStmt continue // we don't add the template to the argsBody } @@ -115,8 +126,7 @@ func (fn *ForeachConfigNode) Evaluate(scope *vm.Scope) error { vars := deepCopyMap(scope.Variables) vars[args.Var] = args.Collection[i] - // TODO: use the registry from the loader to access the modules - customComponentRegistry := NewCustomComponentRegistry(nil, vm.NewScope(vars)) + customComponentRegistry := NewCustomComponentRegistry(fn.customReg, vm.NewScope(vars)) if err := cc.LoadBody(template.Body, map[string]any{}, customComponentRegistry); err != nil { return fmt.Errorf("updating custom component in foreach: %w", err) } diff --git a/internal/runtime/internal/controller/node_config_foreach_test.go b/internal/runtime/internal/controller/node_config_foreach_test.go index 1e21effed6..ff8b10a6bc 100644 --- a/internal/runtime/internal/controller/node_config_foreach_test.go +++ b/internal/runtime/internal/controller/node_config_foreach_test.go @@ -26,7 +26,7 @@ func TestCreateCustomComponents(t *testing.T) { template { } }` - foreachConfigNode := NewForeachConfigNode(getBlockFromConfig(t, config), getComponentGlobals(t)) + foreachConfigNode := NewForeachConfigNode(getBlockFromConfig(t, config), getComponentGlobals(t), nil) require.NoError(t, foreachConfigNode.Evaluate(vm.NewScope(make(map[string]interface{})))) customComponentIds := foreachConfigNode.moduleController.(*ModuleControllerMock).CustomComponents require.ElementsMatch(t, customComponentIds, []string{"foreach_1_1", "foreach_2_1", "foreach_3_1"}) @@ -44,7 +44,7 @@ func TestCreateCustomComponentsDuplicatedIds(t *testing.T) { template { } }` - foreachConfigNode := NewForeachConfigNode(getBlockFromConfig(t, config), getComponentGlobals(t)) + foreachConfigNode := NewForeachConfigNode(getBlockFromConfig(t, config), getComponentGlobals(t), nil) require.NoError(t, foreachConfigNode.Evaluate(vm.NewScope(make(map[string]interface{})))) customComponentIds := foreachConfigNode.moduleController.(*ModuleControllerMock).CustomComponents require.ElementsMatch(t, customComponentIds, []string{"foreach_1_1", "foreach_2_1", "foreach_1_2"}) @@ -62,7 +62,7 @@ func TestCreateCustomComponentsWithUpdate(t *testing.T) { template { } }` - foreachConfigNode := NewForeachConfigNode(getBlockFromConfig(t, config), getComponentGlobals(t)) + foreachConfigNode := NewForeachConfigNode(getBlockFromConfig(t, config), getComponentGlobals(t), nil) require.NoError(t, foreachConfigNode.Evaluate(vm.NewScope(make(map[string]interface{})))) customComponentIds := foreachConfigNode.moduleController.(*ModuleControllerMock).CustomComponents require.ElementsMatch(t, customComponentIds, []string{"foreach_1_1", "foreach_2_1", "foreach_3_1"}) @@ -101,7 +101,7 @@ func TestRunCustomComponents(t *testing.T) { template { } }` - foreachConfigNode := NewForeachConfigNode(getBlockFromConfig(t, config), getComponentGlobals(t)) + foreachConfigNode := NewForeachConfigNode(getBlockFromConfig(t, config), getComponentGlobals(t), nil) require.NoError(t, foreachConfigNode.Evaluate(vm.NewScope(make(map[string]interface{})))) ctx, cancel := context.WithCancel(context.Background()) go foreachConfigNode.Run(ctx) @@ -129,7 +129,7 @@ func TestRunCustomComponentsAfterUpdate(t *testing.T) { template { } }` - foreachConfigNode := NewForeachConfigNode(getBlockFromConfig(t, config), getComponentGlobals(t)) + foreachConfigNode := NewForeachConfigNode(getBlockFromConfig(t, config), getComponentGlobals(t), nil) require.NoError(t, foreachConfigNode.Evaluate(vm.NewScope(make(map[string]interface{})))) ctx, cancel := context.WithCancel(context.Background()) go foreachConfigNode.Run(ctx) @@ -176,7 +176,7 @@ func TestCreateCustomComponentsCollectionObjectsWithUpdate(t *testing.T) { template { } }` - foreachConfigNode := NewForeachConfigNode(getBlockFromConfig(t, config), getComponentGlobals(t)) + foreachConfigNode := NewForeachConfigNode(getBlockFromConfig(t, config), getComponentGlobals(t), nil) vars := map[string]interface{}{ "obj1": map[string]string{ "label1": "a", diff --git a/internal/runtime/testdata/foreach/foreach_7.txtar b/internal/runtime/testdata/foreach/foreach_7.txtar new file mode 100644 index 0000000000..4994d52549 --- /dev/null +++ b/internal/runtime/testdata/foreach/foreach_7.txtar @@ -0,0 +1,32 @@ +Module used inside of a foreach. + +-- main.alloy -- +import.file "testImport" { + filename = "module.alloy" +} + +foreach "testForeach" { + collection = [5, 5] + var = "num" + + template { + testImport.a "cc" { + max = num + receiver = testcomponents.summation_receiver.sum.receiver + } + } +} + +testcomponents.summation_receiver "sum" { +} + +-- module.alloy -- +declare "a" { + argument "max" {} + argument "receiver" {} + testcomponents.pulse "pt" { + max = argument.max.value + frequency = "10ms" + forward_to = [argument.receiver.value] + } +} \ No newline at end of file