Skip to content

settings.depends_on: explicit stack ignored by describe dependents unless context fields also match #2910

Description

@thejrose1984

Describe the Bug

A settings.depends_on entry that names an explicit stack is honored by the dependency DAG but silently ignored by atmos describe dependents unless the two components' context fields (namespace / tenant / environment / stage) also happen to match.

Because describe dependents backs affected-based workflows, a cross-stage dependency can order correctly under atmos terraform --all and still be invisible to CI — a missing deployment gate rather than a visible error.

Expected Behavior

When stack is set on a depends_on entry, it should identify the target stack on its own. The context fields exist to derive a stack when one is not given; once it is given they should not be able to veto the match.

Steps to Reproduce

Two stages, dev and prod, sharing namespace/tenant/environment:

# stacks/deploy/dev.yaml
vars: { namespace: acme, tenant: core, environment: ue2, stage: dev }
components:
  terraform:
    upstream:
      metadata: { component: mock }

# stacks/deploy/prod.yaml
vars: { namespace: acme, tenant: core, environment: ue2, stage: prod }
components:
  terraform:
    downstream-by-stack:            # `stack` alone
      metadata: { component: mock }
      settings:
        depends_on:
          1: { component: upstream, stack: dev }

    downstream-stack-and-stage:     # `stack` plus a redundant `stage`
      metadata: { component: mock }
      settings:
        depends_on:
          1: { component: upstream, stack: dev, stage: dev }
$ atmos describe dependents upstream -s dev --format json
[
  { "component": "downstream-stack-and-stage", ... }
]

downstream-by-stack is missing. Adding a stage: dev that the stack: dev should already imply is what makes it appear.

The DAG path disagrees — it resolves the same edge correctly:

$ atmos terraform plan --all --dry-run
✓ Would plan upstream in dev (dry run)
✓ Would plan downstream-by-stack in prod (dry run)     # correctly ordered after upstream

…and emits no "Dependency target not found" warning for downstream-by-stack.

Cause

internal/exec/describe_dependents.go:

func isDependencyMatch(p *dependencyMatchParams) bool {
	...
	return matchLegacyStack(p.dependsOn, p.args.Stack, p.stackName) &&
		matchLegacyContextFields(p.dependsOn, p.providedComponentVars, p.stackComponentVars)
}

matchLegacyStack short-circuits correctly on dependsOn.Stack, but it is ANDed with matchLegacyContextFields, which still runs. For a field left unset, matchContextField falls through to requiring the depending component's value to equal the target's:

func matchContextField(depValue, providedValue, stackValue string) bool {
	if depValue != "" {
		return providedValue == depValue
	}
	return providedValue == stackValue   // dev != prod  -> whole match fails
}

So stage is compared even when stack was explicit, and a cross-stage dependency fails on exactly the field the explicit stack was meant to settle.

The two other consumers behave differently:

  • parseDependencyMapEntry (internal/exec/dependency_parser.go) reads only component and stack — correct.
  • matchNewFormatStack, used for the replacement dependencies.components format, returns on Stack with no context matching — also correct.

Only the legacy settings.depends_on path ANDs them.

Possible Fix

Skip the context-field check when stack is explicit, mirroring matchNewFormatStack:

if p.dependsOn.Stack != "" {
    return matchLegacyStack(p.dependsOn, p.args.Stack, p.stackName)
}
return matchLegacyStack(...) && matchLegacyContextFields(...)

Worth confirming against existing fixtures first — some may rely on the current AND for entries that set both.

Note settings.depends_on is deprecated in favor of dependencies.components, which is unaffected. If the decision is to leave legacy behavior frozen, the alternative is to document the requirement explicitly, since the shape most users would reach for is the one that silently fails.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions