Skip to content

Commit 5b752fa

Browse files
committed
api: add Priority field to EndpointSettings
This new field is used by libnetwork to determine which endpoint provides the default gateway for a container. Signed-off-by: Albin Kerouanton <[email protected]>
1 parent 229dc66 commit 5b752fa

7 files changed

Lines changed: 208 additions & 1 deletion

File tree

api/server/router/container/container_routes.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,14 @@ func (c *containerRouter) postContainersCreate(ctx context.Context, w http.Respo
643643
}
644644
}
645645

646+
if versions.LessThan(version, "1.48") {
647+
for _, epConfig := range networkingConfig.EndpointsConfig {
648+
// Before 1.48, all endpoints had the same priority, so
649+
// reinitialize this field.
650+
epConfig.GwPriority = 0
651+
}
652+
}
653+
646654
var warnings []string
647655
if warn := handleVolumeDriverBC(version, hostConfig); warn != "" {
648656
warnings = append(warnings, warn)

api/swagger.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2927,6 +2927,16 @@ definitions:
29272927
example:
29282928
com.example.some-label: "some-value"
29292929
com.example.some-other-label: "some-other-value"
2930+
GwPriority:
2931+
description: |
2932+
This property determines which endpoint will provide the default
2933+
gateway for a container. The endpoint with the highest priority will
2934+
be used. If multiple endpoints have the same priority, endpoints are
2935+
lexicographically sorted based on their network name, and the one
2936+
that sorts first is picked.
2937+
type: "number"
2938+
example:
2939+
- 10
29302940

29312941
# Operational data
29322942
NetworkID:
@@ -10910,6 +10920,7 @@ paths:
1091010920
IPv4Address: "172.24.56.89"
1091110921
IPv6Address: "2001:db8::5689"
1091210922
MacAddress: "02:42:ac:12:05:02"
10923+
Priority: 100
1091310924
tags: ["Network"]
1091410925

1091510926
/networks/{id}/disconnect:

api/types/network/endpoint.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type EndpointSettings struct {
1919
// generated address).
2020
MacAddress string
2121
DriverOpts map[string]string
22+
GwPriority int
2223
// Operational data
2324
NetworkID string
2425
EndpointID string

container/view.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,7 @@ func (v *View) transform(ctr *Container) *Snapshot {
377377
GlobalIPv6PrefixLen: netw.GlobalIPv6PrefixLen,
378378
MacAddress: netw.MacAddress,
379379
NetworkID: netw.NetworkID,
380+
GwPriority: netw.GwPriority,
380381
}
381382
if netw.IPAMConfig != nil {
382383
networks[name].IPAMConfig = &network.EndpointIPAMConfig{

daemon/network.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1120,7 +1120,10 @@ func buildJoinOptions(settings *network.Settings, n interface{ Name() string })
11201120
return []libnetwork.EndpointOption{}, nil
11211121
}
11221122

1123-
var joinOptions []libnetwork.EndpointOption
1123+
joinOptions := []libnetwork.EndpointOption{
1124+
libnetwork.JoinOptionPriority(epConfig.GwPriority),
1125+
}
1126+
11241127
for _, str := range epConfig.Links {
11251128
name, alias, err := opts.ParseLink(str)
11261129
if err != nil {

docs/api/version-history.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ keywords: "API, Docker, rcli, REST, documentation"
5050
daemon has experimental features enabled.
5151
* `GET /networks/{id}` now returns an `EnableIPv4` field showing whether the
5252
network has IPv4 IPAM enabled.
53+
* `POST /networks/{id}/connect` and `POST /containers/create` now accept a
54+
`GwPriority` field in `EndpointsConfig`. This value is used to determine which
55+
network endpoint provides the default gateway for the container. The endpoint
56+
with the highest priority is selected. If multiple endpoints have the same
57+
priority, endpoints are sorted lexicographically by their network name, and
58+
the one that sorts first is picked.
59+
* `GET /containers/json` now returns a `GwPriority` field in `NetworkSettings`
60+
for each network endpoint.
5361

5462
## v1.47 API changes
5563

integration/network/network_linux_test.go

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,19 @@ package network // import "github.com/docker/docker/integration/network"
22

33
import (
44
"bytes"
5+
"context"
56
"fmt"
67
"os/exec"
8+
"slices"
79
"strings"
10+
"syscall"
811
"testing"
12+
"time"
913

1014
containertypes "github.com/docker/docker/api/types/container"
1115
networktypes "github.com/docker/docker/api/types/network"
16+
"github.com/docker/docker/api/types/versions"
17+
"github.com/docker/docker/client"
1218
"github.com/docker/docker/integration/internal/container"
1319
"github.com/docker/docker/integration/internal/network"
1420
"github.com/docker/docker/internal/testutils/networking"
@@ -218,3 +224,172 @@ func TestHostGatewayFromDocker0(t *testing.T) {
218224
assert.Check(t, is.Contains(res.Stdout.String(), "192.168.50.1\thg"))
219225
assert.Check(t, is.Contains(res.Stdout.String(), "fddd:6ff4:6e08::1\thg"))
220226
}
227+
228+
func TestCreateWithPriority(t *testing.T) {
229+
// This feature should work on Windows, but the test is skipped because:
230+
// 1. Linux-specific tools are used here; 2. 'windows' IPAM driver doesn't
231+
// support static allocations.
232+
skip.If(t, testEnv.DaemonInfo.OSType == "windows")
233+
skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.48"), "requires API v1.48")
234+
235+
ctx := setupTest(t)
236+
apiClient := testEnv.APIClient()
237+
238+
network.CreateNoError(ctx, t, apiClient, "testnet1",
239+
network.WithIPv6(),
240+
network.WithIPAM("10.100.20.0/24", "10.100.20.1"),
241+
network.WithIPAM("fd54:7a1b:8269::/64", "fd54:7a1b:8269::1"))
242+
defer network.RemoveNoError(ctx, t, apiClient, "testnet1")
243+
244+
network.CreateNoError(ctx, t, apiClient, "testnet2",
245+
network.WithIPv6(),
246+
network.WithIPAM("10.100.30.0/24", "10.100.30.1"),
247+
network.WithIPAM("fdff:6dfe:37d2::/64", "fdff:6dfe:37d2::1"))
248+
defer network.RemoveNoError(ctx, t, apiClient, "testnet2")
249+
250+
ctrID := container.Run(ctx, t, apiClient,
251+
container.WithCmd("sleep", "infinity"),
252+
container.WithNetworkMode("testnet1"),
253+
container.WithEndpointSettings("testnet1", &networktypes.EndpointSettings{GwPriority: 10}),
254+
container.WithEndpointSettings("testnet2", &networktypes.EndpointSettings{GwPriority: 100}))
255+
defer container.Remove(ctx, t, apiClient, ctrID, containertypes.RemoveOptions{Force: true})
256+
257+
checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET, 3, "default via 10.100.30.1 dev")
258+
// IPv6 routing table will contain for each interface, one route for the LL
259+
// address, one for the ULA, and one multicast.
260+
checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET6, 7, "default via fdff:6dfe:37d2::1 dev")
261+
}
262+
263+
func TestConnectWithPriority(t *testing.T) {
264+
// This feature should work on Windows, but the test is skipped because:
265+
// 1. Linux-specific tools are used here; 2. 'windows' IPAM driver doesn't
266+
// support static allocations.
267+
skip.If(t, testEnv.DaemonInfo.OSType == "windows")
268+
skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.48"), "requires API v1.48")
269+
270+
ctx := setupTest(t)
271+
apiClient := testEnv.APIClient()
272+
273+
network.CreateNoError(ctx, t, apiClient, "testnet1",
274+
network.WithIPv6(),
275+
network.WithIPAM("10.100.10.0/24", "10.100.10.1"),
276+
network.WithIPAM("fddd:4901:f594::/64", "fddd:4901:f594::1"))
277+
defer network.RemoveNoError(ctx, t, apiClient, "testnet1")
278+
279+
network.CreateNoError(ctx, t, apiClient, "testnet2",
280+
network.WithIPv6(),
281+
network.WithIPAM("10.100.20.0/24", "10.100.20.1"),
282+
network.WithIPAM("fd83:7683:7008::/64", "fd83:7683:7008::1"))
283+
defer network.RemoveNoError(ctx, t, apiClient, "testnet2")
284+
285+
network.CreateNoError(ctx, t, apiClient, "testnet3",
286+
network.WithDriver("bridge"),
287+
network.WithIPv6(),
288+
network.WithIPAM("10.100.30.0/24", "10.100.30.1"),
289+
network.WithIPAM("fd72:de0:adad::/64", "fd72:de0:adad::1"))
290+
defer network.RemoveNoError(ctx, t, apiClient, "testnet3")
291+
292+
network.CreateNoError(ctx, t, apiClient, "testnet4",
293+
network.WithIPv6(),
294+
network.WithIPAM("10.100.40.0/24", "10.100.40.1"),
295+
network.WithIPAM("fd4c:c927:7d90::/64", "fd4c:c927:7d90::1"))
296+
defer network.RemoveNoError(ctx, t, apiClient, "testnet4")
297+
298+
network.CreateNoError(ctx, t, apiClient, "testnet5",
299+
network.WithIPv6(),
300+
network.WithIPAM("10.100.50.0/24", "10.100.50.1"),
301+
network.WithIPAM("fd4c:364b:1110::/64", "fd4c:364b:1110::1"))
302+
defer network.RemoveNoError(ctx, t, apiClient, "testnet5")
303+
304+
ctrID := container.Run(ctx, t, apiClient,
305+
container.WithCmd("sleep", "infinity"),
306+
container.WithNetworkMode("testnet1"),
307+
container.WithEndpointSettings("testnet1", &networktypes.EndpointSettings{}))
308+
defer container.Remove(ctx, t, apiClient, ctrID, containertypes.RemoveOptions{Force: true})
309+
310+
checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET, 2, "default via 10.100.10.1 dev eth0")
311+
checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET6, 4, "default via fddd:4901:f594::1 dev eth0")
312+
313+
// testnet5 has a negative priority -- the default gateway should not change.
314+
err := apiClient.NetworkConnect(ctx, "testnet5", ctrID, &networktypes.EndpointSettings{GwPriority: -100})
315+
assert.NilError(t, err)
316+
checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET, 3, "default via 10.100.10.1 dev eth0")
317+
checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET6, 7, "default via fddd:4901:f594::1 dev eth0")
318+
319+
// testnet2 has a higher priority. It should now provide the default gateway.
320+
err = apiClient.NetworkConnect(ctx, "testnet2", ctrID, &networktypes.EndpointSettings{GwPriority: 100})
321+
assert.NilError(t, err)
322+
checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET, 4, "default via 10.100.20.1 dev eth2")
323+
checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET6, 10, "default via fd83:7683:7008::1 dev eth2")
324+
325+
// testnet3 has a lower priority, so testnet2 should still provide the default gateway.
326+
err = apiClient.NetworkConnect(ctx, "testnet3", ctrID, &networktypes.EndpointSettings{GwPriority: 10})
327+
assert.NilError(t, err)
328+
checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET, 5, "default via 10.100.20.1 dev eth2")
329+
checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET6, 13, "default via fd83:7683:7008::1 dev eth2")
330+
331+
// testnet4 has the same priority as testnet3, but it sorts after in
332+
// lexicographic order. For now, testnet2 stays the default gateway.
333+
err = apiClient.NetworkConnect(ctx, "testnet4", ctrID, &networktypes.EndpointSettings{GwPriority: 10})
334+
assert.NilError(t, err)
335+
checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET, 6, "default via 10.100.20.1 dev eth2")
336+
checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET6, 16, "default via fd83:7683:7008::1 dev eth2")
337+
338+
inspect := container.Inspect(ctx, t, apiClient, ctrID)
339+
assert.Equal(t, inspect.NetworkSettings.Networks["testnet1"].GwPriority, 0)
340+
assert.Equal(t, inspect.NetworkSettings.Networks["testnet2"].GwPriority, 100)
341+
assert.Equal(t, inspect.NetworkSettings.Networks["testnet3"].GwPriority, 10)
342+
assert.Equal(t, inspect.NetworkSettings.Networks["testnet4"].GwPriority, 10)
343+
assert.Equal(t, inspect.NetworkSettings.Networks["testnet5"].GwPriority, -100)
344+
345+
// Disconnect testnet2, so testnet3 should now provide the default gateway.
346+
// When two endpoints have the same priority (eg. testnet3 vs testnet4),
347+
// the one that sorts first in lexicographic order is picked.
348+
err = apiClient.NetworkDisconnect(ctx, "testnet2", ctrID, true)
349+
assert.NilError(t, err)
350+
checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET, 5, "default via 10.100.30.1 dev eth3")
351+
checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET6, 13, "default via fd72:de0:adad::1 dev eth3")
352+
353+
// Disconnect testnet3, so testnet4 should now provide the default gateway.
354+
err = apiClient.NetworkDisconnect(ctx, "testnet3", ctrID, true)
355+
assert.NilError(t, err)
356+
checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET, 4, "default via 10.100.40.1 dev eth4")
357+
checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET6, 10, "default via fd4c:c927:7d90::1 dev eth4")
358+
359+
// Disconnect testnet4, so testnet1 should now provide the default gateway.
360+
err = apiClient.NetworkDisconnect(ctx, "testnet4", ctrID, true)
361+
assert.NilError(t, err)
362+
checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET, 3, "default via 10.100.10.1 dev eth0")
363+
checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET6, 7, "default via fddd:4901:f594::1 dev eth0")
364+
}
365+
366+
// checkCtrRoutes execute 'ip route show' in a container, and check that the
367+
// number of routes matches expRoutes. It also checks that the default route
368+
// matches expDefRoute. A substring match is used to avoid issues with
369+
// non-stable interface names.
370+
func checkCtrRoutes(t *testing.T, ctx context.Context, apiClient client.APIClient, ctrID string, af, expRoutes int, expDefRoute string) {
371+
t.Helper()
372+
373+
fam := "-4"
374+
if af == syscall.AF_INET6 {
375+
fam = "-6"
376+
}
377+
378+
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
379+
defer cancel()
380+
res, err := container.Exec(ctx, apiClient, ctrID, []string{"ip", "-o", fam, "route", "show"})
381+
assert.NilError(t, err)
382+
383+
assert.Equal(t, res.ExitCode, 0)
384+
assert.Equal(t, res.Stderr(), "")
385+
386+
routes := slices.DeleteFunc(strings.Split(res.Stdout(), "\n"), func(s string) bool {
387+
return s == ""
388+
})
389+
390+
assert.Equal(t, len(routes), expRoutes, "expected %d routes, got %d:\n%s", expRoutes, len(routes), strings.Join(routes, "\n"))
391+
defFound := slices.ContainsFunc(routes, func(s string) bool {
392+
return strings.Contains(s, expDefRoute)
393+
})
394+
assert.Assert(t, defFound, "default route %q not found:\n%s", expDefRoute, strings.Join(routes, "\n"))
395+
}

0 commit comments

Comments
 (0)