Reset default bridge addresses after integration tests

CI runs a docker daemon, and some tests run their own. That tramples
the CI daemon's default bridge.

If a test leaves the bridge with only a link-local IPv6 address (for
example), subsequent tests are likely to fail - even if they also
start their own daemon because, by default the default bridge
network's config is based on an existing bridge device.

So, remember the bridge's addresses at the start of each test, and
restore them afterwards.

Signed-off-by: Rob Murray <rob.murray@docker.com>
This commit is contained in:
Rob Murray
2025-04-16 18:45:22 +01:00
parent c2b7abacf8
commit 6083fad7df
4 changed files with 130 additions and 5 deletions

View File

@@ -39,6 +39,7 @@ func (e *Execution) Clean(ctx context.Context, t testing.TB) {
deleteAllNetworks(ctx, t, apiClient, platform, e.protectedElements.networks)
if platform == "linux" {
deleteAllPlugins(ctx, t, apiClient, e.protectedElements.plugins)
restoreDefaultBridge(t, e.protectedElements.defaultBridgeInfo)
}
}

View File

@@ -25,11 +25,12 @@ var frozenImages = []string{
}
type protectedElements struct {
containers map[string]struct{}
images map[string]struct{}
networks map[string]struct{}
plugins map[string]struct{}
volumes map[string]struct{}
containers map[string]struct{}
defaultBridgeInfo *defaultBridgeInfo
images map[string]struct{}
networks map[string]struct{}
plugins map[string]struct{}
volumes map[string]struct{}
}
func newProtectedElements() protectedElements {
@@ -57,6 +58,7 @@ func ProtectAll(ctx context.Context, t testing.TB, testEnv *Execution) {
ProtectNetworks(ctx, t, testEnv)
ProtectVolumes(ctx, t, testEnv)
if testEnv.DaemonInfo.OSType == "linux" {
ProtectDefaultBridge(ctx, t, testEnv)
ProtectPlugins(ctx, t, testEnv)
}
}

View File

@@ -0,0 +1,106 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.22
package environment
import (
"context"
"errors"
"maps"
"net"
"testing"
"github.com/docker/docker/internal/nlwrap"
"github.com/docker/docker/libnetwork/drivers/bridge"
"github.com/vishvananda/netlink"
"gotest.tools/v3/assert"
)
type defaultBridgeInfo struct {
bridge netlink.Link
addrs map[string]*netlink.Addr
}
var _, llSubnet, _ = net.ParseCIDR("fe80::/64")
// ProtectDefaultBridge remembers default bridge settings so that, when a test
// runs its own daemon and tramples settings of the bridge belonging to the
// CI-started bridge, the bridge is restored to its old state before the next
// test.
//
// For example, a test may enable IPv6 with a link-local fixed-cidr-v6. That's
// likely to break later tests, even if they also start their own daemon
// (because, in the absence of any specific settings, the daemon learns default
// bridge config from addresses on an existing bridge device).
func ProtectDefaultBridge(_ context.Context, t testing.TB, testEnv *Execution) {
t.Helper()
// Find the bridge - there should always be one, belonging to the daemon started by CI.
br, err := nlwrap.LinkByName(bridge.DefaultBridgeName)
if err != nil {
var lnf netlink.LinkNotFoundError
if !errors.As(err, &lnf) {
t.Fatal("Getting default bridge before test:", err)
}
return
}
testEnv.ProtectDefaultBridge(t, &defaultBridgeInfo{
bridge: br,
addrs: getAddrs(t, br),
})
}
func getAddrs(t testing.TB, br netlink.Link) map[string]*netlink.Addr {
t.Helper()
addrs, err := nlwrap.AddrList(br, netlink.FAMILY_ALL)
assert.NilError(t, err, "Getting default bridge addresses before test")
addrMap := map[string]*netlink.Addr{}
for _, addr := range addrs {
addrMap[addr.IPNet.String()] = &addr
}
return addrMap
}
// ProtectDefaultBridge stores default bridge info, to be restored on clean.
func (e *Execution) ProtectDefaultBridge(t testing.TB, info *defaultBridgeInfo) {
e.protectedElements.defaultBridgeInfo = info
}
func restoreDefaultBridge(t testing.TB, info *defaultBridgeInfo) {
t.Helper()
if info == nil {
return
}
// Re-create the bridge if the test was antisocial enough to delete it.
// Yes, I'm looking at you TestDockerDaemonSuite/TestBuildOnDisabledBridgeNetworkDaemon.
br, err := nlwrap.LinkByName(bridge.DefaultBridgeName)
if err != nil {
var lnf netlink.LinkNotFoundError
if !errors.As(err, &lnf) {
t.Fatal("Failed to find default bridge after test:", err)
}
err := netlink.LinkAdd(info.bridge)
assert.NilError(t, err, "Failed to re-create default bridge after test")
br, err = nlwrap.LinkByName(bridge.DefaultBridgeName)
assert.NilError(t, err, "Failed to find re-created default bridge after test")
}
addrs, err := nlwrap.AddrList(br, netlink.FAMILY_ALL)
assert.NilError(t, err, "Failed get default bridge addresses after test")
// Delete addresses the bridge didn't have before the test, apart from IPv6 LL
// addresses - because the bridge doesn't get a kernel-assigned LL address until
// the first veth is hooked up and, once that address is deleted, it's not
// re-added.
wantAddrs := maps.Clone(info.addrs)
for _, addr := range addrs {
if _, ok := wantAddrs[addr.IPNet.String()]; ok {
delete(wantAddrs, addr.IPNet.String())
} else if !llSubnet.Contains(addr.IP) {
err := netlink.AddrDel(br, &netlink.Addr{IPNet: addr.IPNet})
assert.NilError(t, err, "Failed to remove default bridge address '%s' after test", addr.IPNet.String())
}
}
// Add missing addresses.
for _, wantAddr := range wantAddrs {
err = netlink.AddrAdd(br, wantAddr)
assert.NilError(t, err, "Failed to add default bridge address '%s' after test", wantAddr.IPNet.String())
}
}

View File

@@ -0,0 +1,16 @@
//go:build !linux
package environment
import (
"context"
"testing"
)
type defaultBridgeInfo struct{}
func ProtectDefaultBridge(context.Context, testing.TB, *Execution) {
return
}
func restoreDefaultBridge(testing.TB, *defaultBridgeInfo) {}