libnetwork: implement Controller.GetSandbox(containerID)

Various parts of the code were using "walkers" to iterate over the
controller's sandboxes, and the only condition for all of them was
to find the sandbox for a given container-ID. Iterating over all
sandboxes was also sub-optimal, because on Windows, the ContainerID
is used as Sandbox-ID, which can be used to lookup the sandbox from
the "sandboxes" map on the controller.

This patch implements a GetSandbox method on the controller that
looks up the sandbox for a given container-ID, using the most optimal
approach (depending on the platform).

The new method can return errors for invalid (empty) container-IDs, and
a "not found" error to allow consumers to detect non-existing sandboxes,
or potentially invalid IDs.

This new method replaces the (non-exported) Daemon.getNetworkSandbox(),
which was only used internally, in favor of directly accessing the
controller's method.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn 2023-08-12 14:38:43 +02:00
parent 6c4153f348
commit 6dba98cf38
No known key found for this signature in database
GPG key ID: 76698F39D527CE8C
6 changed files with 89 additions and 33 deletions

View file

@ -532,7 +532,11 @@ func (daemon *Daemon) allocateNetwork(cfg *config.Config, container *container.C
// If the container is not to be connected to any network, // If the container is not to be connected to any network,
// create its network sandbox now if not present // create its network sandbox now if not present
if len(networks) == 0 { if len(networks) == 0 {
if nil == daemon.getNetworkSandbox(container) { if _, err := daemon.netController.GetSandbox(container.ID); err != nil {
if !errdefs.IsNotFound(err) {
return err
}
sbOptions, err := daemon.buildSandboxOptions(cfg, container) sbOptions, err := daemon.buildSandboxOptions(cfg, container)
if err != nil { if err != nil {
return err return err
@ -557,18 +561,6 @@ func (daemon *Daemon) allocateNetwork(cfg *config.Config, container *container.C
return nil return nil
} }
func (daemon *Daemon) getNetworkSandbox(container *container.Container) *libnetwork.Sandbox {
var sb *libnetwork.Sandbox
daemon.netController.WalkSandboxes(func(s *libnetwork.Sandbox) bool {
if s.ContainerID() == container.ID {
sb = s
return true
}
return false
})
return sb
}
// hasUserDefinedIPAddress returns whether the passed IPAM configuration contains IP address configuration // hasUserDefinedIPAddress returns whether the passed IPAM configuration contains IP address configuration
func hasUserDefinedIPAddress(ipamConfig *networktypes.EndpointIPAMConfig) bool { func hasUserDefinedIPAddress(ipamConfig *networktypes.EndpointIPAMConfig) bool {
return ipamConfig != nil && (len(ipamConfig.IPv4Address) > 0 || len(ipamConfig.IPv6Address) > 0) return ipamConfig != nil && (len(ipamConfig.IPv4Address) > 0 || len(ipamConfig.IPv6Address) > 0)
@ -715,7 +707,8 @@ func (daemon *Daemon) connectToNetwork(cfg *config.Config, container *container.
return err return err
} }
sb := daemon.getNetworkSandbox(container) // TODO(thaJeztah): should this fail early if no sandbox was found?
sb, _ := daemon.netController.GetSandbox(container.ID)
createOptions, err := buildCreateEndpointOptions(container, n, endpointConfig, sb, cfg.DNS) createOptions, err := buildCreateEndpointOptions(container, n, endpointConfig, sb, cfg.DNS)
if err != nil { if err != nil {
return err return err
@ -1072,9 +1065,9 @@ func (daemon *Daemon) ActivateContainerServiceBinding(containerName string) erro
if err != nil { if err != nil {
return err return err
} }
sb := daemon.getNetworkSandbox(ctr) sb, err := daemon.netController.GetSandbox(ctr.ID)
if sb == nil { if err != nil {
return fmt.Errorf("network sandbox does not exist for container %s", containerName) return fmt.Errorf("failed to activate service binding for container %s: %w", containerName, err)
} }
return sb.EnableService() return sb.EnableService()
} }
@ -1085,10 +1078,10 @@ func (daemon *Daemon) DeactivateContainerServiceBinding(containerName string) er
if err != nil { if err != nil {
return err return err
} }
sb := daemon.getNetworkSandbox(ctr) sb, err := daemon.netController.GetSandbox(ctr.ID)
if sb == nil { if err != nil {
// If the network sandbox is not found, then there is nothing to deactivate // If the network sandbox is not found, then there is nothing to deactivate
log.G(context.TODO()).Debugf("Could not find network sandbox for container %s on service binding deactivation request", containerName) log.G(context.TODO()).WithError(err).Debugf("Could not find network sandbox for container %s on service binding deactivation request", containerName)
return nil return nil
} }
return sb.DisableService() return sb.DisableService()

View file

@ -997,6 +997,32 @@ func (c *Controller) WalkSandboxes(walker SandboxWalker) {
} }
} }
// GetSandbox returns the Sandbox which has the passed id.
//
// It returns an [ErrInvalidID] when passing an invalid ID, or an
// [types.NotFoundError] if no Sandbox was found for the container.
func (c *Controller) GetSandbox(containerID string) (*Sandbox, error) {
if containerID == "" {
return nil, ErrInvalidID("id is empty")
}
c.mu.Lock()
defer c.mu.Unlock()
if runtime.GOOS == "windows" {
// fast-path for Windows, which uses the container ID as sandbox ID.
if sb := c.sandboxes[containerID]; sb != nil && !sb.isStub {
return sb, nil
}
} else {
for _, sb := range c.sandboxes {
if sb.containerID == containerID && !sb.isStub {
return sb, nil
}
}
}
return nil, types.NotFoundErrorf("network sandbox for container %s not found", containerID)
}
// SandboxByID returns the Sandbox which has the passed id. // SandboxByID returns the Sandbox which has the passed id.
// If not found, a [types.NotFoundError] is returned. // If not found, a [types.NotFoundError] is returned.
func (c *Controller) SandboxByID(id string) (*Sandbox, error) { func (c *Controller) SandboxByID(id string) (*Sandbox, error) {

View file

@ -2109,11 +2109,10 @@ func (pt parallelTester) Do(t *testing.T, thrNumber int) error {
return errors.New("got nil ep with no error") return errors.New("got nil ep with no error")
} }
var sb *libnetwork.Sandbox
cid := fmt.Sprintf("%drace", thrNumber) cid := fmt.Sprintf("%drace", thrNumber)
pt.controller.WalkSandboxes(libnetwork.SandboxContainerWalker(&sb, cid)) sb, err := pt.controller.GetSandbox(cid)
if sb == nil { if err != nil {
return errors.Errorf("got nil sandbox for container: %s", cid) return err
} }
for i := 0; i < pt.iterCnt; i++ { for i := 0; i < pt.iterCnt; i++ {

View file

@ -147,7 +147,10 @@ func (sb *Sandbox) updateParentHosts() error {
var pSb *Sandbox var pSb *Sandbox
for _, update := range sb.config.parentUpdates { for _, update := range sb.config.parentUpdates {
sb.controller.WalkSandboxes(SandboxContainerWalker(&pSb, update.cid)) // TODO(thaJeztah): was it intentional for this loop to re-use prior results of pSB? If not, we should make pSb local and always replace here.
if s, _ := sb.controller.GetSandbox(update.cid); s != nil {
pSb = s
}
if pSb == nil { if pSb == nil {
continue continue
} }

View file

@ -164,15 +164,11 @@ func (c *Controller) processExternalKey(conn net.Conn) error {
if err = json.Unmarshal(buf[0:nr], &s); err != nil { if err = json.Unmarshal(buf[0:nr], &s); err != nil {
return err return err
} }
sb, err := c.GetSandbox(s.ContainerID)
var sandbox *Sandbox if err != nil {
search := SandboxContainerWalker(&sandbox, s.ContainerID) return types.InvalidParameterErrorf("failed to get sandbox for %s", s.ContainerID)
c.WalkSandboxes(search)
if sandbox == nil {
return types.InvalidParameterErrorf("no sandbox present for %s", s.ContainerID)
} }
return sb.SetKey(s.Key)
return sandbox.SetKey(s.Key)
} }
func (c *Controller) stopExternalKeyListener() { func (c *Controller) stopExternalKeyListener() {

View file

@ -6,12 +6,15 @@ import (
"strconv" "strconv"
"testing" "testing"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/internal/testutils/netnsutils" "github.com/docker/docker/internal/testutils/netnsutils"
"github.com/docker/docker/libnetwork/config" "github.com/docker/docker/libnetwork/config"
"github.com/docker/docker/libnetwork/ipamapi" "github.com/docker/docker/libnetwork/ipamapi"
"github.com/docker/docker/libnetwork/netlabel" "github.com/docker/docker/libnetwork/netlabel"
"github.com/docker/docker/libnetwork/options" "github.com/docker/docker/libnetwork/options"
"github.com/docker/docker/libnetwork/osl" "github.com/docker/docker/libnetwork/osl"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
) )
func getTestEnv(t *testing.T, opts ...[]NetworkOption) (*Controller, []*Network) { func getTestEnv(t *testing.T, opts ...[]NetworkOption) (*Controller, []*Network) {
@ -51,6 +54,42 @@ func getTestEnv(t *testing.T, opts ...[]NetworkOption) (*Controller, []*Network)
return c, nwList return c, nwList
} }
func TestControllerGetSandbox(t *testing.T) {
ctrlr, _ := getTestEnv(t)
t.Run("invalid id", func(t *testing.T) {
const cID = ""
sb, err := ctrlr.GetSandbox(cID)
_, ok := err.(ErrInvalidID)
assert.Check(t, ok, "expected ErrInvalidID, got %[1]v (%[1]T)", err)
assert.Check(t, is.Nil(sb))
})
t.Run("not found", func(t *testing.T) {
const cID = "container-id-with-no-sandbox"
sb, err := ctrlr.GetSandbox(cID)
assert.Check(t, errdefs.IsNotFound(err), "expected a ErrNotFound, got %[1]v (%[1]T)", err)
assert.Check(t, is.Nil(sb))
})
t.Run("existing sandbox", func(t *testing.T) {
const cID = "test-container-id"
expected, err := ctrlr.NewSandbox(cID)
assert.Check(t, err)
sb, err := ctrlr.GetSandbox(cID)
assert.Check(t, err)
assert.Check(t, is.Equal(sb.ContainerID(), cID))
assert.Check(t, is.Equal(sb.ID(), expected.ID()))
assert.Check(t, is.Equal(sb.Key(), expected.Key()))
assert.Check(t, is.Equal(sb.ContainerID(), expected.ContainerID()))
err = sb.Delete()
assert.Check(t, err)
sb, err = ctrlr.GetSandbox(cID)
assert.Check(t, errdefs.IsNotFound(err), "expected a ErrNotFound, got %[1]v (%[1]T)", err)
assert.Check(t, is.Nil(sb))
})
}
func TestSandboxAddEmpty(t *testing.T) { func TestSandboxAddEmpty(t *testing.T) {
ctrlr, _ := getTestEnv(t) ctrlr, _ := getTestEnv(t)