Skip to content

fix: ensure deterministic variable interpolation#860

Open
yunus25jmi1 wants to merge 3 commits intocompose-spec:mainfrom
yunus25jmi1:main
Open

fix: ensure deterministic variable interpolation#860
yunus25jmi1 wants to merge 3 commits intocompose-spec:mainfrom
yunus25jmi1:main

Conversation

@yunus25jmi1
Copy link
Copy Markdown

Description

This fixes the non-deterministic behavior when multiple required variables (e.g., ${TIMEZONE:?}, ${TRAEFIK_ACME_PATH:?}) are missing in docker-compose.yaml.

Root Cause

Go's map iteration order is randomized. When iterating over service environment variables or volumes during interpolation, different keys can be processed in different orders on each run. This causes the error message for missing required variables to vary between runs.

Fix

Added sort.Strings(keys) before map iteration in two places:

  1. In Interpolate() function - sorts top-level config keys
  2. In recursiveInterpolate() function - sorts nested map keys

This ensures deterministic variable interpolation order, so the same missing variable is always reported first.

Testing

  • go build: passes
  • go test ./interpolation/: all 7 tests pass
  • go vet: passes

Related

@yunus25jmi1 yunus25jmi1 requested a review from ndeloof as a code owner April 4, 2026 20:00
yunus25jmi1 added a commit to yunus25jmi1/compose that referenced this pull request Apr 4, 2026
TODO: Remove replace once compose-go#860 is merged
See: compose-spec/compose-go#860

Signed-off-by: Md Yunus <[email protected]>
out := map[string]interface{}{}

for key, value := range config {
keys := make([]string, 0, len(config))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps the slices and maps packages could be used here; something like;

keys := maps.Keys(m)
slices.Sort(keys)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh! That won't work because maps.Keys returns an iterator; this probably works though;

keys := slices.Sorted(maps.Keys(config))

@yunus25jmi1
Copy link
Copy Markdown
Author

Thanks for the suggestion @thaJeztah! I've updated the code to use slices.Sorted(maps.Keys(config)) as you recommended - this is much more idiomatic Go.

Changes:

  • Replaced manual slice creation + sort.Strings with slices.Sorted(maps.Keys())
  • Added "slices" and "maps" imports
  • Applied to both locations (Interpolate and recursiveInterpolate functions)

All tests pass.

@thaJeztah
Copy link
Copy Markdown
Member

Ah, nice, thanks!

There was nothing wrong with your original implementation (and in some cases open-coding these things could still be used for performance critical paths) but the slices and maps packages can make the code cleaner, easier to understand and maintain, which is a big plus :-)

@yunus25jmi1
Copy link
Copy Markdown
Author

@ndeloof review the PR.

@thaJeztah
Copy link
Copy Markdown
Member

I think Nicolas is off for a few days, but a maintainer will probably review this change after the weekend / after Easter.

FWIW; (I haven't dug deep into the code) I think this change would make failures more deterministic, but I think there may still be situations where it could fail even though technically it's valid; this PR sorts the env-vars in a deterministic way, but expansion may fail if an expansion depends on another env-var that's sorted "later"?

Haven't tested it yet, but considering something like this;

- ${AAAA:-${BBBB:-${ZZZZ}}}
- ${BBBB:-${ZZZZ}}
- ZZZZ=default

Those are sorted, but if those are interpolated in alphabetic order, they would likely still fail.

Basically;

  • AAAA depends on BBBB
  • BBBB depends on ZZZZ
  • ZZZZ has a default value

So the correct order would be to expand them in topological order;

  • ZZZZ first
  • BBBB second
  • AAAA last

This fixes the non-deterministic behavior when multiple required
variables (e.g., ${TIMEZONE:?}, ${TRAEFIK_ACME_PATH:?}) are missing.
Go's map iteration order is randomized, causing different error messages
to be reported on each run.

By sorting keys before iteration, we ensure consistent error reporting.

Fixes: docker/compose#13712
Signed-off-by: Md Yunus <[email protected]>
Refactored to use slices.Sorted(maps.Keys()) as suggested by reviewer.
This is more idiomatic Go than manual slice creation and sorting.

Fixes: docker/compose#13712

Signed-off-by: Md Yunus <[email protected]>
@yunus25jmi1
Copy link
Copy Markdown
Author

Thanks @thaJeztah for the suggestion! I've updated the code to use slices.Sorted(maps.Keys(config)) as you recommended.

I also investigated your comment about nested variable dependencies (e.g., ${AAAA:-}). The template package already handles this correctly through recursive substitution - when a default value contains another variable, it recursively resolves the inner variables first before using them as defaults for outer ones. This provides the correct topological ordering automatically.

I've added test cases to verify this behavior. Let me know if there are any other changes or suggestions needed before the PR can be merged!

@yunus25jmi1
Copy link
Copy Markdown
Author

Hi @ndeloof @thaJeztah and maintainers,

This PR is ready for review. All feedback from @thaJeztah has been addressed:

  • Updated to use idiomatic Go (slices.Sorted(maps.Keys()))
  • Added test cases for nested variable expansion

This fix is aligned with docker/compose PR #13713 (docker/compose#13713) which addresses the same non-deterministic variable interpolation issue. Once this PR is merged, we can update docker/compose to use the new compose-go version with the fix.

Please let me know if any additional changes are needed!

Signed-off-by: Md Yunus [email protected]

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] docker compose config --quiet produces non-deterministic result for required variables

2 participants