Skip to content

fix: emit Networks and Addresses in deterministic order#744

Merged
buchdag merged 1 commit into
nginx-proxy:mainfrom
JamBalaya56562:fix/431-169-deterministic-network-order
Jun 20, 2026
Merged

fix: emit Networks and Addresses in deterministic order#744
buchdag merged 1 commit into
nginx-proxy:mainfrom
JamBalaya56562:fix/431-169-deterministic-network-order

Conversation

@JamBalaya56562

Copy link
Copy Markdown
Contributor

Summary

The per-container Networks and Addresses slices exposed to templates are built by ranging over Go maps (NetworkSettings.Networks, NetworkSettings.Ports and Config.ExposedPorts), whose iteration order is random. As a result:

This PR sorts both slices into a deterministic, total order so the rendered output is stable across regenerations. Routing is unaffected — only the ordering becomes deterministic.

Changes

  • internal/context/address.go: sort addresses in GetContainerAddresses via a new sortAddresses helper — by port (compared numerically, so 80 < 443 < 8080), then proto, host port, host IP, IP.
  • internal/generator/generator.go: sort runtimeContainer.Networks by Name (the unique network-name map key) via a new sortNetworks helper, right after the network map is collected.
  • Tests: add order-sensitive TestSortAddresses, TestGetContainerAddressesSorted and TestSortNetworks. The existing order-insensitive address tests are unchanged and still pass.
  • README.md: document the deterministic ordering and how to select a network by name.

All new symbols are unexported and the change is purely additive.

Behavior note

index .Networks 0 now returns the alphabetically-first network (e.g. bridge) instead of an arbitrary one — this is the intended, deterministic fix (as suggested in #431). A specific network can still be selected by name:

{{ $net := index (where $value.Networks "Name" "my-network") 0 }}

Resolves


Note: an unrelated, pre-existing flake in TestGenerateFromEvents (timing-dependent; see #238) can fail on the Windows runner — it reproduces on main without this change (verified locally on Windows / Go 1.26). It is independent of this PR.

@JamBalaya56562 JamBalaya56562 force-pushed the fix/431-169-deterministic-network-order branch from c20b526 to f28de76 Compare June 18, 2026 14:05
@buchdag buchdag requested a review from Copilot June 19, 2026 12:45

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR makes the Networks and Addresses slices exposed to templates deterministic by sorting them, eliminating output churn caused by Go map iteration randomness and making index .Networks 0 stable across regenerations (fixing the instability described in #431 and #169).

Changes:

  • Sort container Addresses deterministically in GetContainerAddresses (numeric port order, then proto/host-port/host-ip/ip).
  • Sort container Networks deterministically by Name immediately after collecting them from Docker’s network map.
  • Add targeted, order-sensitive unit tests and document the new ordering behavior (including how to select a network by name).

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
README.md Documents deterministic ordering for Addresses/Networks and shows how to select a network by name.
internal/generator/generator.go Adds sortNetworks and applies it to stabilize runtimeContainer.Networks ordering.
internal/generator/generator_test.go Adds TestSortNetworks to validate deterministic network ordering.
internal/context/address.go Adds sortAddresses and calls it from GetContainerAddresses to stabilize address ordering.
internal/context/address_test.go Adds order-sensitive tests for sortAddresses and deterministic GetContainerAddresses output.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@buchdag buchdag changed the title fix: emit Networks and Addresses in deterministic order (#431, #169) fix: emit Networks and Addresses in deterministic order Jun 20, 2026
@buchdag

buchdag commented Jun 20, 2026

Copy link
Copy Markdown
Member

@JamBalaya56562 please do not include issue or PR numbers in either PR titles or commit message.

The per-container Networks and Addresses slices passed to templates were
built by ranging over Go maps (NetworkSettings.Networks, NetworkSettings.Ports
and Config.ExposedPorts), whose iteration order is random. This caused
generated files to churn on every regeneration and triggered spurious reloads
/ container restarts even when nothing meaningful changed (nginx-proxy#431), and made
`index .Networks 0` pick an arbitrary network (nginx-proxy#169).

- internal/context/address.go: sort addresses in GetContainerAddresses via a
  new sortAddresses helper (Port numeric -> Proto -> HostPort -> HostIP -> IP).
- internal/generator/generator.go: sort runtimeContainer.Networks by Name via
  a new sortNetworks helper, right after the network map is collected.
- Tests: add order-sensitive TestSortAddresses / TestGetContainerAddressesSorted
  and TestSortNetworks. Existing order-insensitive address tests still pass.
- README.md: document the deterministic ordering, the behavior change to
  `index .Networks 0`, and how to select a network by name with `where`.

`index (where $value.Networks "Name" $name) 0` once the order is stable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@buchdag buchdag force-pushed the fix/431-169-deterministic-network-order branch from f28de76 to 28b2b35 Compare June 20, 2026 12:19

@buchdag buchdag left a comment

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.

sortAddresses sorts addresses in place by port (numeric), then proto, host port, host IP and IP.

I'm not 100% certain about the rationale of sorting by IP last, could you elaborate ? 🤔

Not that I'm against it but I'm not quite sure what sorting strategy would make the most sense for a slice of Address.

@JamBalaya56562

Copy link
Copy Markdown
Contributor Author

Good question. The key thing is that the only fields which actually vary between the addresses of a single container are Port and ProtoIP, IP6LinkLocal and IP6Global all come straight from the one container.NetworkSettings.* in renderAddress, so they're identical across every Address we emit for that container and can't discriminate between them at all. That's really the answer to "why IP last": it's the weakest possible key here, since it never changes within the slice.

Port goes first because it's the field templates actually reason about and index on — the documented common access is (index .Addresses 0).Port — and sorting it numerically (via strconv.Atoi) gives the intuitive 80 < 443 < 8080 rather than the lexical "443" < "80". Proto is the natural next key for the real case where the same port appears as both tcp and udp (e.g. 53/tcp vs 53/udp).

HostPort, HostIP and IP are then just there to make the comparator a total order. The whole point of the PR is deterministic output, and if two entries ever compared equal on the meaningful keys, sort.Slice would leave their relative order unspecified and we'd be back to depending on Ports/ExposedPorts map iteration order — exactly the nondeterminism we're removing. IP lands last because it's the most arbitrary (Docker-assigned, not user-chosen) and, being container-wide, almost never decides anything — it's a final backstop for totality rather than a key we expect to matter. Totally happy to reorder the tie-breakers, or promote a different primary key, if another precedence reads better to you; the determinism holds regardless of how we rank them.

@buchdag buchdag merged commit 6612732 into nginx-proxy:main Jun 20, 2026
6 checks passed
@JamBalaya56562 JamBalaya56562 deleted the fix/431-169-deterministic-network-order branch June 20, 2026 13:54
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.

order of networks in nginx template is random Support for multiple docker networks (via libnetwork)

3 participants