Merge pull request #46211 from thaJeztah/daemon_error_cleanup

daemon: WithNamespaces(): improve error-handling, and assorted cleanups
This commit is contained in:
Sebastiaan van Stijn 2023-08-24 19:23:18 +02:00 committed by GitHub
commit cfd431d4d5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 140 additions and 94 deletions

View file

@ -68,16 +68,6 @@ func (daemon *Daemon) GetContainer(prefixOrName string) (*container.Container, e
return ctr, nil
}
// checkContainer make sure the specified container validates the specified conditions
func (daemon *Daemon) checkContainer(container *container.Container, conditions ...func(*container.Container) error) error {
for _, condition := range conditions {
if err := condition(container); err != nil {
return err
}
}
return nil
}
// Exists returns a true if a container of the specified ID or name exists,
// false otherwise.
func (daemon *Daemon) Exists(id string) bool {

View file

@ -59,60 +59,54 @@ func (daemon *Daemon) setupLinkedContainers(container *container.Container) ([]s
return env, nil
}
func (daemon *Daemon) getIpcContainer(id string) (*container.Container, error) {
errMsg := "can't join IPC of container " + id
// Check the container exists
func (daemon *Daemon) getIPCContainer(id string) (*container.Container, error) {
// Check if the container exists, is running, and not restarting
ctr, err := daemon.GetContainer(id)
if err != nil {
return nil, errors.Wrap(err, errMsg)
return nil, errdefs.InvalidParameter(err)
}
// Check the container is running and not restarting
if err := daemon.checkContainer(ctr, containerIsRunning, containerIsNotRestarting); err != nil {
return nil, errors.Wrap(err, errMsg)
if !ctr.IsRunning() {
return nil, errNotRunning(id)
}
if ctr.IsRestarting() {
return nil, errContainerIsRestarting(id)
}
// Check the container ipc is shareable
if st, err := os.Stat(ctr.ShmPath); err != nil || !st.IsDir() {
if err == nil || os.IsNotExist(err) {
return nil, errors.New(errMsg + ": non-shareable IPC (hint: use IpcMode:shareable for the donor container)")
return nil, errdefs.InvalidParameter(errors.New("container " + id + ": non-shareable IPC (hint: use IpcMode:shareable for the donor container)"))
}
// stat() failed?
return nil, errors.Wrap(err, errMsg+": unexpected error from stat "+ctr.ShmPath)
return nil, errdefs.System(errors.Wrap(err, "container "+id))
}
return ctr, nil
}
func (daemon *Daemon) getPidContainer(ctr *container.Container) (*container.Container, error) {
containerID := ctr.HostConfig.PidMode.Container()
ctr, err := daemon.GetContainer(containerID)
func (daemon *Daemon) getPIDContainer(id string) (*container.Container, error) {
ctr, err := daemon.GetContainer(id)
if err != nil {
return nil, errors.Wrapf(err, "cannot join PID of a non running container: %s", containerID)
return nil, errdefs.InvalidParameter(err)
}
return ctr, daemon.checkContainer(ctr, containerIsRunning, containerIsNotRestarting)
if !ctr.IsRunning() {
return nil, errNotRunning(id)
}
if ctr.IsRestarting() {
return nil, errContainerIsRestarting(id)
}
return ctr, nil
}
func containerIsRunning(c *container.Container) error {
if !c.IsRunning() {
return errdefs.Conflict(errors.Errorf("container %s is not running", c.ID))
}
return nil
}
func containerIsNotRestarting(c *container.Container) error {
if c.IsRestarting() {
return errContainerIsRestarting(c.ID)
}
return nil
}
func (daemon *Daemon) setupIpcDirs(c *container.Container) error {
func (daemon *Daemon) setupIPCDirs(c *container.Container) error {
ipcMode := c.HostConfig.IpcMode
switch {
case ipcMode.IsContainer():
ic, err := daemon.getIpcContainer(ipcMode.Container())
ic, err := daemon.getIPCContainer(ipcMode.Container())
if err != nil {
return err
return errors.Wrapf(err, "failed to join IPC namespace")
}
c.ShmPath = ic.ShmPath

View file

@ -244,34 +244,47 @@ func WithNamespaces(daemon *Daemon, c *container.Container) coci.SpecOpts {
userNS := false
// user
if c.HostConfig.UsernsMode.IsPrivate() {
uidMap := daemon.idMapping.UIDMaps
if uidMap != nil {
if uidMap := daemon.idMapping.UIDMaps; uidMap != nil {
userNS = true
ns := specs.LinuxNamespace{Type: "user"}
setNamespace(s, ns)
setNamespace(s, specs.LinuxNamespace{
Type: specs.UserNamespace,
})
s.Linux.UIDMappings = specMapping(uidMap)
s.Linux.GIDMappings = specMapping(daemon.idMapping.GIDMaps)
}
}
// network
if !c.Config.NetworkDisabled {
ns := specs.LinuxNamespace{Type: "network"}
if c.HostConfig.NetworkMode.IsContainer() {
nc, err := daemon.getNetworkedContainer(c.ID, c.HostConfig.NetworkMode.ConnectedContainer())
networkMode := c.HostConfig.NetworkMode
switch {
case networkMode.IsContainer():
nc, err := daemon.getNetworkedContainer(c.ID, networkMode.ConnectedContainer())
if err != nil {
return err
}
ns.Path = fmt.Sprintf("/proc/%d/ns/net", nc.State.GetPID())
setNamespace(s, specs.LinuxNamespace{
Type: specs.NetworkNamespace,
Path: fmt.Sprintf("/proc/%d/ns/net", nc.State.GetPID()),
})
if userNS {
// to share a net namespace, they must also share a user namespace
nsUser := specs.LinuxNamespace{Type: "user"}
nsUser.Path = fmt.Sprintf("/proc/%d/ns/user", nc.State.GetPID())
setNamespace(s, nsUser)
// to share a net namespace, the containers must also share a user namespace.
//
// FIXME(thaJeztah): this will silently overwrite an earlier user namespace when joining multiple containers: https://github.com/moby/moby/issues/46210
setNamespace(s, specs.LinuxNamespace{
Type: specs.UserNamespace,
Path: fmt.Sprintf("/proc/%d/ns/user", nc.State.GetPID()),
})
}
} else if c.HostConfig.NetworkMode.IsHost() {
ns.Path = c.NetworkSettings.SandboxKey
case networkMode.IsHost():
setNamespace(s, specs.LinuxNamespace{
Type: specs.NetworkNamespace,
Path: c.NetworkSettings.SandboxKey,
})
default:
setNamespace(s, specs.LinuxNamespace{
Type: specs.NetworkNamespace,
})
}
setNamespace(s, ns)
}
// ipc
@ -281,64 +294,73 @@ func WithNamespaces(daemon *Daemon, c *container.Container) coci.SpecOpts {
}
switch {
case ipcMode.IsContainer():
ns := specs.LinuxNamespace{Type: "ipc"}
ic, err := daemon.getIpcContainer(ipcMode.Container())
ic, err := daemon.getIPCContainer(ipcMode.Container())
if err != nil {
return errdefs.InvalidParameter(errors.Wrapf(err, "invalid IPC mode: %v", ipcMode))
return errors.Wrap(err, "failed to join IPC namespace")
}
ns.Path = fmt.Sprintf("/proc/%d/ns/ipc", ic.State.GetPID())
setNamespace(s, ns)
setNamespace(s, specs.LinuxNamespace{
Type: specs.IPCNamespace,
Path: fmt.Sprintf("/proc/%d/ns/ipc", ic.State.GetPID()),
})
if userNS {
// to share an IPC namespace, they must also share a user namespace
nsUser := specs.LinuxNamespace{Type: "user"}
nsUser.Path = fmt.Sprintf("/proc/%d/ns/user", ic.State.GetPID())
setNamespace(s, nsUser)
// to share a IPC namespace, the containers must also share a user namespace.
//
// FIXME(thaJeztah): this will silently overwrite an earlier user namespace when joining multiple containers: https://github.com/moby/moby/issues/46210
setNamespace(s, specs.LinuxNamespace{
Type: specs.UserNamespace,
Path: fmt.Sprintf("/proc/%d/ns/user", ic.State.GetPID()),
})
}
case ipcMode.IsHost():
oci.RemoveNamespace(s, "ipc")
oci.RemoveNamespace(s, specs.IPCNamespace)
case ipcMode.IsEmpty():
// A container was created by an older version of the daemon.
// The default behavior used to be what is now called "shareable".
fallthrough
case ipcMode.IsPrivate(), ipcMode.IsShareable(), ipcMode.IsNone():
ns := specs.LinuxNamespace{Type: "ipc"}
setNamespace(s, ns)
setNamespace(s, specs.LinuxNamespace{
Type: specs.IPCNamespace,
})
}
// pid
if !c.HostConfig.PidMode.Valid() {
return errdefs.InvalidParameter(errors.Errorf("invalid PID mode: %v", c.HostConfig.PidMode))
pidMode := c.HostConfig.PidMode
if !pidMode.Valid() {
return errdefs.InvalidParameter(errors.Errorf("invalid PID mode: %v", pidMode))
}
if c.HostConfig.PidMode.IsContainer() {
pc, err := daemon.getPidContainer(c)
switch {
case pidMode.IsContainer():
pc, err := daemon.getPIDContainer(pidMode.Container())
if err != nil {
return err
return errors.Wrap(err, "failed to join PID namespace")
}
ns := specs.LinuxNamespace{
Type: "pid",
setNamespace(s, specs.LinuxNamespace{
Type: specs.PIDNamespace,
Path: fmt.Sprintf("/proc/%d/ns/pid", pc.State.GetPID()),
}
setNamespace(s, ns)
})
if userNS {
// to share a PID namespace, they must also share a user namespace
nsUser := specs.LinuxNamespace{
Type: "user",
// to share a PID namespace, the containers must also share a user namespace.
//
// FIXME(thaJeztah): this will silently overwrite an earlier user namespace when joining multiple containers: https://github.com/moby/moby/issues/46210
setNamespace(s, specs.LinuxNamespace{
Type: specs.UserNamespace,
Path: fmt.Sprintf("/proc/%d/ns/user", pc.State.GetPID()),
}
setNamespace(s, nsUser)
})
}
} else if c.HostConfig.PidMode.IsHost() {
oci.RemoveNamespace(s, "pid")
} else {
ns := specs.LinuxNamespace{Type: "pid"}
setNamespace(s, ns)
case pidMode.IsHost():
oci.RemoveNamespace(s, specs.PIDNamespace)
default:
setNamespace(s, specs.LinuxNamespace{
Type: specs.PIDNamespace,
})
}
// uts
if !c.HostConfig.UTSMode.Valid() {
return errdefs.InvalidParameter(errors.Errorf("invalid UTS mode: %v", c.HostConfig.UTSMode))
}
if c.HostConfig.UTSMode.IsHost() {
oci.RemoveNamespace(s, "uts")
oci.RemoveNamespace(s, specs.UTSNamespace)
s.Hostname = ""
}
@ -346,11 +368,10 @@ func WithNamespaces(daemon *Daemon, c *container.Container) coci.SpecOpts {
if !c.HostConfig.CgroupnsMode.Valid() {
return errdefs.InvalidParameter(errors.Errorf("invalid cgroup namespace mode: %v", c.HostConfig.CgroupnsMode))
}
if !c.HostConfig.CgroupnsMode.IsEmpty() {
if c.HostConfig.CgroupnsMode.IsPrivate() {
nsCgroup := specs.LinuxNamespace{Type: "cgroup"}
setNamespace(s, nsCgroup)
}
if c.HostConfig.CgroupnsMode.IsPrivate() {
setNamespace(s, specs.LinuxNamespace{
Type: specs.CgroupNamespace,
})
}
return nil
@ -511,7 +532,7 @@ func withMounts(daemon *Daemon, daemonCfg *configStore, c *container.Container)
return err
}
if err := daemon.setupIpcDirs(c); err != nil {
if err := daemon.setupIPCDirs(c); err != nil {
return err
}

View file

@ -6,8 +6,11 @@ import (
"testing"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/integration/internal/container"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/poll"
"gotest.tools/v3/skip"
)
@ -33,3 +36,41 @@ func TestPIDModeHost(t *testing.T) {
cPid = container.GetContainerNS(ctx, t, apiClient, cID, "pid")
assert.Assert(t, hostPid != cPid)
}
func TestPIDModeContainer(t *testing.T) {
skip.If(t, testEnv.DaemonInfo.OSType != "linux")
defer setupTest(t)()
apiClient := testEnv.APIClient()
ctx := context.Background()
t.Run("non-existing container", func(t *testing.T) {
_, err := container.CreateFromConfig(ctx, apiClient, container.NewTestConfig(container.WithPIDMode("container:nosuchcontainer")))
assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
assert.Check(t, is.ErrorContains(err, "No such container: nosuchcontainer"))
})
t.Run("non-running container", func(t *testing.T) {
const pidCtrName = "stopped-pid-namespace-container"
cPIDContainerID := container.Create(ctx, t, apiClient, container.WithName(pidCtrName))
ctr, err := container.CreateFromConfig(ctx, apiClient, container.NewTestConfig(container.WithPIDMode("container:"+pidCtrName)))
assert.NilError(t, err, "should not produce an error when creating, only when starting")
err = apiClient.ContainerStart(ctx, ctr.ID, types.ContainerStartOptions{})
assert.Check(t, is.ErrorType(err, errdefs.IsSystem), "should produce a System error when starting an existing container from an invalid state")
assert.Check(t, is.ErrorContains(err, "failed to join PID namespace"))
assert.Check(t, is.ErrorContains(err, cPIDContainerID+" is not running"))
})
t.Run("running container", func(t *testing.T) {
const pidCtrName = "running-pid-namespace-container"
container.Run(ctx, t, apiClient, container.WithName(pidCtrName))
ctr, err := container.CreateFromConfig(ctx, apiClient, container.NewTestConfig(container.WithPIDMode("container:"+pidCtrName)))
assert.NilError(t, err)
err = apiClient.ContainerStart(ctx, ctr.ID, types.ContainerStartOptions{})
assert.Check(t, err)
})
}