Skip to content
Merged
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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,20 @@ For example, this is a JSON version of an emitted RuntimeContainer struct:
}
```

#### Ordering of `Addresses` and `Networks`

The `Addresses` and `Networks` slices are emitted in a deterministic order, so generated output is stable across regenerations and does not trigger spurious reloads/restarts when nothing meaningful changed:

- `Addresses` are sorted by port (compared numerically), then by protocol, host port, host IP and IP.
- `Networks` are sorted alphabetically by `Name`.

As a consequence, `index .Networks 0` returns the alphabetically-first network (for example `bridge`) rather than an arbitrary one. To select a specific network by name, filter with `where`:

```
{{ $net := index (where $value.Networks "Name" "my-network") 0 }}
server {{ $net.IP }}:{{ (index $value.Addresses 0).Port }};
```

#### Functions

- [Functions from Go](https://pkg.go.dev/text/template#hdr-Functions)
Expand Down
27 changes: 27 additions & 0 deletions internal/context/address.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package context

import (
"sort"
"strconv"

docker "github.com/fsouza/go-dockerclient"
)

Expand Down Expand Up @@ -46,5 +49,29 @@ func GetContainerAddresses(container *docker.Container) []Address {
}
}

sortAddresses(addresses)

return addresses
}

// sortAddresses sorts addresses in place by port (numeric), then proto, host port, host IP and IP.
func sortAddresses(addresses []Address) {
sort.Slice(addresses, func(i, j int) bool {
a, b := addresses[i], addresses[j]
pa, _ := strconv.Atoi(a.Port)
pb, _ := strconv.Atoi(b.Port)
if pa != pb {
return pa < pb
}
if a.Proto != b.Proto {
return a.Proto < b.Proto
}
if a.HostPort != b.HostPort {
return a.HostPort < b.HostPort
}
if a.HostIP != b.HostIP {
return a.HostIP < b.HostIP
}
return a.IP < b.IP
})
}
47 changes: 47 additions & 0 deletions internal/context/address_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,50 @@ func TestGenerateContainerAddressesWithNoPorts(t *testing.T) {
HostPort: "",
})
}

func TestSortAddresses(t *testing.T) {
addresses := []Address{
{IP: "10.0.0.10", Port: "8080", Proto: "tcp"},
{IP: "10.0.0.10", Port: "80", Proto: "tcp"},
{IP: "10.0.0.10", Port: "443", Proto: "tcp"},
{IP: "10.0.0.10", Port: "53", Proto: "udp"},
{IP: "10.0.0.10", Port: "53", Proto: "tcp"},
}

// Port sorts numerically (not lexically: "443" must not sort before "80"),
// with Proto as the tie-breaker for equal ports (53/tcp before 53/udp).
want := []Address{
{IP: "10.0.0.10", Port: "53", Proto: "tcp"},
{IP: "10.0.0.10", Port: "53", Proto: "udp"},
{IP: "10.0.0.10", Port: "80", Proto: "tcp"},
{IP: "10.0.0.10", Port: "443", Proto: "tcp"},
{IP: "10.0.0.10", Port: "8080", Proto: "tcp"},
}

sortAddresses(addresses)
assert.Equal(t, want, addresses)
}

func TestGetContainerAddressesSorted(t *testing.T) {
testContainer := &docker.Container{
Config: &docker.Config{
ExposedPorts: map[docker.Port]struct{}{},
},
NetworkSettings: &docker.NetworkSettings{
IPAddress: "10.0.0.10",
Ports: map[docker.Port][]docker.PortBinding{},
},
}
// Insert ports out of numeric order; the map iteration order is random but
// GetContainerAddresses must return them deterministically sorted by port.
testContainer.NetworkSettings.Ports[httpTestPort] = []docker.PortBinding{} // 8080
testContainer.NetworkSettings.Ports[httpPort] = []docker.PortBinding{} // 80
testContainer.NetworkSettings.Ports[httpsPort] = []docker.PortBinding{} // 443

addresses := GetContainerAddresses(testContainer)
ports := make([]string, len(addresses))
for i, a := range addresses {
ports[i] = a.Port
}
assert.Equal(t, []string{"80", "443", "8080"}, ports)
}
10 changes: 10 additions & 0 deletions internal/generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"
"os/exec"
"os/signal"
"sort"
"strings"
"sync"
"syscall"
Expand Down Expand Up @@ -388,6 +389,13 @@ func (g *generator) sendSignalToFilteredContainers(config config.Config) {
}
}

// sortNetworks sorts networks in place by Name (ascending).
func sortNetworks(networks []context.Network) {
sort.Slice(networks, func(i, j int) bool {
return networks[i].Name < networks[j].Name
})
}

func (g *generator) getContainers(config config.Config) ([]*context.RuntimeContainer, error) {
apiInfo, err := g.Client.Info()
if err != nil {
Expand Down Expand Up @@ -474,6 +482,8 @@ func (g *generator) getContainers(config config.Config) ([]*context.RuntimeConta
network)
}

sortNetworks(runtimeContainer.Networks)

for k, v := range container.Volumes {
runtimeContainer.Volumes[k] = context.Volume{
Path: k,
Expand Down
30 changes: 30 additions & 0 deletions internal/generator/generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/nginx-proxy/docker-gen/internal/config"
"github.com/nginx-proxy/docker-gen/internal/context"
"github.com/nginx-proxy/docker-gen/internal/dockerclient"
"github.com/stretchr/testify/assert"
)

func TestGenerateFromEvents(t *testing.T) {
Expand Down Expand Up @@ -216,3 +217,32 @@ func TestGenerateFromEvents(t *testing.T) {
}
}
}

func TestSortNetworks(t *testing.T) {
for _, tc := range []struct {
desc string
in []context.Network
want []context.Network
}{
{
desc: "multiple unsorted",
in: []context.Network{{Name: "frontend"}, {Name: "bridge"}, {Name: "app_net"}},
want: []context.Network{{Name: "app_net"}, {Name: "bridge"}, {Name: "frontend"}},
},
{
desc: "single element",
in: []context.Network{{Name: "bridge"}},
want: []context.Network{{Name: "bridge"}},
},
{
desc: "empty",
in: []context.Network{},
want: []context.Network{},
},
} {
t.Run(tc.desc, func(t *testing.T) {
sortNetworks(tc.in)
assert.Equal(t, tc.want, tc.in)
})
}
}