in internal/core/component.go [323:435]
func WalkComponentTree(startingPath string, environments []string, iterator componentIteration, rootInit rootComponentInit) <-chan WalkResult {
queue := make(chan Component) // components enqueued to be 'visited' (ie; walked over)
results := make(chan WalkResult) // To pass WalkResults to
walking := sync.WaitGroup{} // Keep track of all nodes being worked on
// Prepares `component` by loading/de-serializing the component.yaml/json and configs
// Note: this is only needed for non-inlined components
prepareComponent := func(c Component) Component {
logger.Debug(fmt.Sprintf("Preparing component '%s'", c.Name))
// 1. Parse the component at that path into a Component
c, err := c.LoadComponent()
if err != nil {
results <- WalkResult{Error: err}
}
// 2. Load the config for this Component
if err = c.LoadConfig(environments); err != nil {
results <- WalkResult{Error: err}
}
return c
}
// Enqueue the given component
enqueue := func(c Component) {
// Increment working counter; MUST happen BEFORE sending to queue or race condition can occur
walking.Add(1)
logger.Debug(fmt.Sprintf("Adding subcomponent '%s' to queue with physical path '%s' and logical path '%s'\n", c.Name, c.PhysicalPath, c.LogicalPath))
queue <- c
}
// Mark a component as visited and report it back as a result; decrements the walking counter
markAsVisited := func(c *Component) {
results <- WalkResult{Component: c}
walking.Done()
}
// Main worker thread to enqueue root node, wait, and close the channel once all nodes visited
go func() {
// Manually enqueue the first root component
rootComponent := prepareComponent(Component{
PhysicalPath: startingPath,
LogicalPath: "./",
Config: NewComponentConfig(startingPath),
})
// Init rootComponent
rootComponent, err := rootInit(startingPath, environments, rootComponent)
if err != nil {
results <- WalkResult{Error: err}
} else {
enqueue(rootComponent)
}
// Close results channel once all nodes visited
walking.Wait()
close(results)
}()
// Worker thread to pull from queue and call the iterator
go func() {
for queuedComponent := range queue {
go func(c Component) {
// Decrement working counter; Must happen AFTER the subcomponents are enqueued
defer markAsVisited(&c)
// Call the iterator
err := iterator(c.PhysicalPath, &c)
if err != nil {
results <- WalkResult{Error: err}
}
// Range over subcomponents; preparing and enqueuing
for _, subcomponent := range c.Subcomponents {
// Prep component config
subcomponent.Config = c.Config.Subcomponents[subcomponent.Name]
if err = subcomponent.applyDefaultsAndMigrations(); err != nil {
results <- WalkResult{Error: err}
}
// Do not add to the queue if component or subcomponent is Disabled.
if subcomponent.Config.Disabled {
logger.Info(emoji.Sprintf(":prohibited: Subcomponent '%s' is disabled", subcomponent.Name))
continue
}
// Depending if the subcomponent is inlined or not; prepare the component to either load
// config/path info from filesystem (non-inlined) or inherit from parent (inlined)
if subcomponent.ComponentType == "component" || subcomponent.ComponentType == "" {
// This subcomponent is not inlined, so set the paths to their relative positions and prepare the configs
subcomponent.PhysicalPath = path.Join(subcomponent.RelativePathTo(), subcomponent.Path)
if !filepath.IsAbs(subcomponent.RelativePathTo()) {
subcomponent.PhysicalPath = path.Join(c.PhysicalPath, subcomponent.PhysicalPath)
}
subcomponent.LogicalPath = path.Join(c.LogicalPath, subcomponent.Name)
subcomponent = prepareComponent(subcomponent)
} else {
// This subcomponent is inlined, so it inherits paths from parent and no need to prepareComponent().
subcomponent.PhysicalPath = c.PhysicalPath
subcomponent.LogicalPath = c.LogicalPath
}
logger.Debug(fmt.Sprintf("Adding subcomponent '%s' to queue with physical path '%s' and logical path '%s'\n", subcomponent.Name, subcomponent.PhysicalPath, subcomponent.LogicalPath))
enqueue(subcomponent)
}
}(queuedComponent)
}
}()
return results
}