Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion cmd/describe_dependents.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ var describeDependentsCmd = &cobra.Command{
Use: "dependents",
Aliases: []string{"dependants"},
Short: "List Atmos components that depend on a given component",
Long: "This command generates a list of Atmos components within stacks that depend on the specified Atmos component.",
Long: "This command generates a list of Atmos components within stacks that depend on the specified Atmos component. Optional dependencies declared with required: false are included when available and ignored when their targets are missing or disabled. See https://atmos.tools/stacks/dependencies/components.",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false},
Args: cobra.ExactArgs(1),
ValidArgsFunction: ComponentsArgCompletion,
Expand Down
4 changes: 3 additions & 1 deletion cmd/list/dependencies.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ var dependenciesCmd = &cobra.Command{

By default the output is a tree showing both dependency directions. Use
--direction to show one side. Use --format=levels to list dependency distance
from selected roots.`,
from selected roots. Optional component dependencies declared with
required: false are included when their targets are available and skipped when
targets are missing or disabled. See https://atmos.tools/stacks/dependencies/components.`,
Aliases: []string{"deps"},
FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false},
Args: cobra.MaximumNArgs(1),
Expand Down
8 changes: 5 additions & 3 deletions errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ var (
ErrInvalidTemplateSettings = errors.New("invalid template settings")
ErrTemplateEvaluation = errors.New("template evaluation failed")
ErrCommandEnvDecodeFailed = schemaPkg.ErrCommandEnvDecodeFailed
ErrComponentDependencyMissingComponent = schemaPkg.ErrComponentDependencyMissingComponent
ErrCastStepRequiresSteps = errors.New("cast step requires nested steps")
ErrCastSessionRequiresActions = errors.New("cast session step requires session actions")
ErrInvalidCastMode = errors.New("cast step has invalid mode")
Expand Down Expand Up @@ -872,9 +873,10 @@ var (
ErrProcessStack = errors.New("error processing stack")

// Dependency errors.
ErrUnsupportedDependencyType = errors.New("unsupported dependency type")
ErrMissingDependencyField = errors.New("dependency missing required field")
ErrDependencyTargetNotFound = errors.New("dependency target not found")
ErrUnsupportedDependencyType = errors.New("unsupported dependency type")
ErrMissingDependencyField = errors.New("dependency missing required field")
ErrDependencyTargetNotFound = errors.New("dependency target not found")
ErrDependencyTargetUnavailable = errors.New("dependency target unavailable")
// ErrCustomCommandDependencyNotRegistered is returned when a dependencies.commands entry
// names a command that isn't registered under the custom-command cobra tree.
ErrCustomCommandDependencyNotRegistered = errors.New("dependency command is not registered")
Expand Down
91 changes: 84 additions & 7 deletions internal/exec/dependency_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
cfg "github.com/cloudposse/atmos/pkg/config"
"github.com/cloudposse/atmos/pkg/dependency"
"github.com/cloudposse/atmos/pkg/perf"
"github.com/cloudposse/atmos/pkg/schema"
"github.com/cloudposse/atmos/pkg/tags"
)

const (
Expand All @@ -24,20 +26,35 @@ const (

// DependencyParser handles parsing of component dependencies from configuration.
type DependencyParser struct {
builder *dependency.GraphBuilder
nodeMap map[string]string
builder *dependency.GraphBuilder
nodeMap map[string]string
targetStates map[string]string
leftDelim string
}

// NewDependencyParser creates a new dependency parser.
func NewDependencyParser(builder *dependency.GraphBuilder, nodeMap map[string]string) *DependencyParser {
func NewDependencyParser(builder *dependency.GraphBuilder, nodeMap map[string]string, targetStates ...map[string]string) *DependencyParser {
defer perf.Track(nil, "exec.NewDependencyParser")()

states := map[string]string(nil)
if len(targetStates) > 0 {
states = targetStates[0]
}
return &DependencyParser{
builder: builder,
nodeMap: nodeMap,
builder: builder,
nodeMap: nodeMap,
targetStates: states,
}
}

// NewDependencyParserWithDelimiter creates a dependency parser with a configured template delimiter.
func NewDependencyParserWithDelimiter(builder *dependency.GraphBuilder, nodeMap, targetStates map[string]string, leftDelim string) *DependencyParser {
defer perf.Track(nil, "exec.NewDependencyParserWithDelimiter")()
parser := NewDependencyParser(builder, nodeMap, targetStates)
parser.leftDelim = leftDelim
return parser
}

// ParseComponentDependencies parses all dependencies from a component's settings.
func (p *DependencyParser) ParseComponentDependencies(
stackName string,
Expand All @@ -46,13 +63,39 @@ func (p *DependencyParser) ParseComponentDependencies(
) error {
defer perf.Track(nil, "exec.DependencyParser.ParseComponentDependencies")()

// Skip abstract components.
// Skip abstract and disabled source components.
if p.shouldSkipComponent(componentSection) {
return nil
}

fromID := fmt.Sprintf(nodeIDFormat, componentName, stackName)

//nolint:nestif // Modern and legacy dependency surfaces require distinct fallback semantics.
if dependenciesSection, ok := componentSection[cfg.DependenciesSectionName]; ok {
depsMap, ok := dependenciesSection.(map[string]any)
if !ok {
return fmt.Errorf("%w: dependencies must be a map", errUtils.ErrUnsupportedDependencyType)
}
if _, modern := depsMap["components"]; modern {
dependencies, err := schema.ParseComponentDependencies(depsMap, cfg.TerraformComponentType, stackName)
if err != nil {
return fmt.Errorf("%w: parse dependencies: %w", errUtils.ErrDependencyResolution, err)
}
for i := range dependencies {
dep := &dependencies[i]
if dep.Kind != "" && dep.Kind != cfg.TerraformComponentType {
continue
}
if tags.SelectorUnresolved(dep.Component, p.leftDelim) || tags.SelectorUnresolved(dep.Stack, p.leftDelim) {
return fmt.Errorf("%w: from=%s component=%s stack=%s", errUtils.ErrDependencyResolution, fromID, dep.Component, dep.Stack)
}
if err := p.addModernDependency(fromID, stackName, dep); err != nil {
return err
}
}
return nil
}
}

// Check for dependencies in settings.depends_on, then the historical
// component-level location accepted for backward compatibility.
settingsSection, ok := componentSection[cfg.SettingsSectionName].(map[string]any)
Expand Down Expand Up @@ -158,6 +201,40 @@ func (p *DependencyParser) parseDependencyMapEntry(fromID, defaultStack string,
return p.addDependencyIfExists(fromID, toID)
}

func (p *DependencyParser) addModernDependency(fromID, defaultStack string, dep *schema.ComponentDependency) error {
stack := dep.Stack
if stack == "" {
stack = defaultStack
}
toID := fmt.Sprintf(nodeIDFormat, dep.Component, stack)
reason, unavailable := p.targetStates[toID]
if !unavailable {
_, exists := p.nodeMap[toID]
unavailable = !exists
reason = "target_missing"
}
if unavailable {
if dep.IsRequired() {
targetErr := errUtils.ErrDependencyTargetNotFound
if reason == "target_disabled" {
targetErr = errUtils.ErrDependencyTargetUnavailable
}
return fmt.Errorf("%w: from=%s to=%s reason=%s", targetErr, fromID, toID, reason)
}
log.Info("optional dependency skipped", "event", "optional_dependency_skipped", "from", fromID, "to", toID,
"reason", reason, "kind", dep.Kind)
return nil
}
if err := p.builder.AddDependencyWithOptional(fromID, toID, !dep.IsRequired()); err != nil {
return err
}
if !dep.IsRequired() {
log.Debug("optional dependency included", "event", "optional_dependency_included", "from", fromID, "to", toID,
"kind", dep.Kind)
}
return nil
}

// parseDependencyMapAnyEntry parses a map[any]any dependency entry.
func (p *DependencyParser) parseDependencyMapAnyEntry(fromID, defaultStack string, depMap map[any]any) error {
component, ok := depMap["component"].(string)
Expand Down
5 changes: 4 additions & 1 deletion internal/exec/describe_affected_utils_2.go
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,10 @@ func addDependentsToAffected(
}

// Build the reverse dependency index once from the cached stacks.
depIdx := buildDependencyIndex(stacks)
depIdx, err := buildDependencyIndexWithError(stacks)
if err != nil {
return err
}

for i := 0; i < len(*affected); i++ {
a := &(*affected)[i]
Expand Down
Loading
Loading