2018-02-05 21:05:59 +00:00
|
|
|
package daemon // import "github.com/docker/docker/daemon"
|
2015-08-03 22:05:34 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
2023-06-23 00:33:17 +00:00
|
|
|
"context"
|
2015-08-26 12:00:01 +00:00
|
|
|
"fmt"
|
|
|
|
"io"
|
2023-01-16 22:25:13 +00:00
|
|
|
"net"
|
2015-08-03 22:05:34 +00:00
|
|
|
"os"
|
2016-03-29 22:27:04 +00:00
|
|
|
"regexp"
|
2015-08-03 22:05:34 +00:00
|
|
|
"strings"
|
2023-04-05 11:32:03 +00:00
|
|
|
"sync"
|
2015-08-03 22:05:34 +00:00
|
|
|
|
2023-09-13 15:41:45 +00:00
|
|
|
"github.com/containerd/log"
|
2018-07-17 19:11:38 +00:00
|
|
|
"github.com/docker/docker/daemon/config"
|
2023-01-16 22:25:13 +00:00
|
|
|
"github.com/docker/docker/libnetwork/ns"
|
2021-04-06 00:24:47 +00:00
|
|
|
"github.com/docker/docker/libnetwork/resolvconf"
|
2020-03-13 23:38:24 +00:00
|
|
|
"github.com/moby/sys/mount"
|
|
|
|
"github.com/moby/sys/mountinfo"
|
2018-01-24 23:10:01 +00:00
|
|
|
"github.com/pkg/errors"
|
2023-01-16 22:25:13 +00:00
|
|
|
"github.com/vishvananda/netlink"
|
2023-04-05 11:32:03 +00:00
|
|
|
"golang.org/x/sys/unix"
|
2015-08-03 22:05:34 +00:00
|
|
|
)
|
|
|
|
|
2017-02-02 19:22:12 +00:00
|
|
|
// On Linux, plugins use a static path for storing execution state,
|
|
|
|
// instead of deriving path from daemon's exec-root. This is because
|
|
|
|
// plugin socket files are created here and they cannot exceed max
|
|
|
|
// path length of 108 bytes.
|
2022-10-17 11:47:56 +00:00
|
|
|
func getPluginExecRoot(_ *config.Config) string {
|
2017-02-02 19:22:12 +00:00
|
|
|
return "/run/docker/plugins"
|
|
|
|
}
|
|
|
|
|
2016-03-18 18:50:19 +00:00
|
|
|
func (daemon *Daemon) cleanupMountsByID(id string) error {
|
2023-06-23 00:33:17 +00:00
|
|
|
log.G(context.TODO()).Debugf("Cleaning up old mountid %s: start.", id)
|
2016-03-18 18:50:19 +00:00
|
|
|
f, err := os.Open("/proc/self/mountinfo")
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
return daemon.cleanupMountsFromReaderByID(f, id, mount.Unmount)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (daemon *Daemon) cleanupMountsFromReaderByID(reader io.Reader, id string, unmount func(target string) error) error {
|
|
|
|
if daemon.root == "" {
|
|
|
|
return nil
|
|
|
|
}
|
2019-08-09 12:10:07 +00:00
|
|
|
var errs []string
|
2016-03-29 22:27:04 +00:00
|
|
|
|
|
|
|
regexps := getCleanPatterns(id)
|
2016-03-18 18:50:19 +00:00
|
|
|
sc := bufio.NewScanner(reader)
|
|
|
|
for sc.Scan() {
|
2020-10-19 04:27:12 +00:00
|
|
|
if fields := strings.Fields(sc.Text()); len(fields) > 4 {
|
2016-03-29 22:27:04 +00:00
|
|
|
if mnt := fields[4]; strings.HasPrefix(mnt, daemon.root) {
|
|
|
|
for _, p := range regexps {
|
|
|
|
if p.MatchString(mnt) {
|
|
|
|
if err := unmount(mnt); err != nil {
|
2023-06-23 00:33:17 +00:00
|
|
|
log.G(context.TODO()).Error(err)
|
2019-08-09 12:10:07 +00:00
|
|
|
errs = append(errs, err.Error())
|
2016-03-29 22:27:04 +00:00
|
|
|
}
|
|
|
|
}
|
2016-03-18 18:50:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := sc.Err(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-08-09 12:10:07 +00:00
|
|
|
if len(errs) > 0 {
|
|
|
|
return fmt.Errorf("Error cleaning up mounts:\n%v", strings.Join(errs, "\n"))
|
2016-03-18 18:50:19 +00:00
|
|
|
}
|
|
|
|
|
2023-06-23 00:33:17 +00:00
|
|
|
log.G(context.TODO()).Debugf("Cleaning up old mountid %v: done.", id)
|
2016-03-18 18:50:19 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-01-24 23:10:01 +00:00
|
|
|
// cleanupMounts umounts used by container resources and the daemon root mount
|
2022-08-17 21:13:49 +00:00
|
|
|
func (daemon *Daemon) cleanupMounts(cfg *config.Config) error {
|
2018-01-24 23:10:01 +00:00
|
|
|
if err := daemon.cleanupMountsByID(""); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-03-13 23:38:24 +00:00
|
|
|
info, err := mountinfo.GetMounts(mountinfo.SingleEntryFilter(daemon.root))
|
2018-01-24 23:10:01 +00:00
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "error reading mount table for cleanup")
|
|
|
|
}
|
|
|
|
|
2018-03-01 05:55:57 +00:00
|
|
|
if len(info) < 1 {
|
|
|
|
// no mount found, we're done here
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-01-24 23:10:01 +00:00
|
|
|
// `info.Root` here is the root mountpoint of the passed in path (`daemon.root`).
|
|
|
|
// The ony cases that need to be cleaned up is when the daemon has performed a
|
|
|
|
// `mount --bind /daemon/root /daemon/root && mount --make-shared /daemon/root`
|
|
|
|
// This is only done when the daemon is started up and `/daemon/root` is not
|
|
|
|
// already on a shared mountpoint.
|
2018-03-01 05:55:57 +00:00
|
|
|
if !shouldUnmountRoot(daemon.root, info[0]) {
|
2018-01-24 23:10:01 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-08-17 21:13:49 +00:00
|
|
|
unmountFile := getUnmountOnShutdownPath(cfg)
|
2018-04-17 15:30:39 +00:00
|
|
|
if _, err := os.Stat(unmountFile); err != nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-06-23 00:33:17 +00:00
|
|
|
log.G(context.TODO()).WithField("mountpoint", daemon.root).Debug("unmounting daemon root")
|
2018-04-17 15:30:39 +00:00
|
|
|
if err := mount.Unmount(daemon.root); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return os.Remove(unmountFile)
|
2015-08-26 12:00:01 +00:00
|
|
|
}
|
|
|
|
|
2016-03-29 22:27:04 +00:00
|
|
|
func getCleanPatterns(id string) (regexps []*regexp.Regexp) {
|
|
|
|
var patterns []string
|
|
|
|
if id == "" {
|
|
|
|
id = "[0-9a-f]{64}"
|
daemon: fix daemon.Shutdown, daemon.Cleanup not cleaning up overlay2 mounts
While working on deprecation of the `aufs` and `overlay` storage-drivers, the
`TestCleanupMounts` had to be updated, as it was currently using `aufs` for
testing. When rewriting the test to use `overlay2` instead (using an updated
`mountsFixture`), I found out that the test was failing, and it appears that
only `overlay`, but not `overlay2` was taken into account.
These cleanup functions were added in 05cc737f5411a0effd299429140d031c4ad8dd05,
but at the time the `overlay2` storage driver was not yet implemented;
https://github.com/moby/moby/tree/05cc737f5411a0effd299429140d031c4ad8dd05/daemon/graphdriver
This omission was likely missed in 23e5c94cfb26eb72c097892712d3dbaa93ee9bc0,
because the original implementation re-used the `overlay` storage driver, but
later on it was decided to make `overlay2` a separate storage driver.
As a result of the above, `daemon.cleanupMountsByID()` would ignore any `overlay2`
mounts during `daemon.Shutdown()` and `daemon.Cleanup()`.
This patch:
- Adds a new `mountsFixtureOverlay2` with example mounts for `overlay2`
- Rewrites the tests to use `gotest.tools` for more informative output on failures.
- Adds the missing regex patterns to `daemon/getCleanPatterns()`. The patterns
are added at the start of the list to allow for the fasted match (`overlay2`
is the default for most setups, and the code is iterating over possible
options).
As a follow-up, we could consider adding additional fixtures for different
storage drivers.
Before the fix is applied:
go test -v -run TestCleanupMounts ./daemon/
=== RUN TestCleanupMounts
=== RUN TestCleanupMounts/aufs
=== RUN TestCleanupMounts/overlay2
daemon_linux_test.go:135: assertion failed: 0 (unmounted int) != 1 (int): Expected to unmount the shm (and the shm only)
--- FAIL: TestCleanupMounts (0.01s)
--- PASS: TestCleanupMounts/aufs (0.00s)
--- FAIL: TestCleanupMounts/overlay2 (0.01s)
=== RUN TestCleanupMountsByID
=== RUN TestCleanupMountsByID/aufs
=== RUN TestCleanupMountsByID/overlay2
daemon_linux_test.go:171: assertion failed: 0 (unmounted int) != 1 (int): Expected to unmount the root (and that only)
--- FAIL: TestCleanupMountsByID (0.00s)
--- PASS: TestCleanupMountsByID/aufs (0.00s)
--- FAIL: TestCleanupMountsByID/overlay2 (0.00s)
FAIL
FAIL github.com/docker/docker/daemon 0.054s
FAIL
With the fix applied:
go test -v -run TestCleanupMounts ./daemon/
=== RUN TestCleanupMounts
=== RUN TestCleanupMounts/aufs
=== RUN TestCleanupMounts/overlay2
--- PASS: TestCleanupMounts (0.00s)
--- PASS: TestCleanupMounts/aufs (0.00s)
--- PASS: TestCleanupMounts/overlay2 (0.00s)
=== RUN TestCleanupMountsByID
=== RUN TestCleanupMountsByID/aufs
=== RUN TestCleanupMountsByID/overlay2
--- PASS: TestCleanupMountsByID (0.00s)
--- PASS: TestCleanupMountsByID/aufs (0.00s)
--- PASS: TestCleanupMountsByID/overlay2 (0.00s)
PASS
ok github.com/docker/docker/daemon 0.042s
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2022-05-29 14:20:14 +00:00
|
|
|
patterns = append(patterns, "containers/"+id+"/mounts/shm", "containers/"+id+"/shm")
|
2015-08-25 07:58:33 +00:00
|
|
|
}
|
2022-05-24 15:24:04 +00:00
|
|
|
patterns = append(patterns, "overlay2/"+id+"/merged$", "zfs/graph/"+id+"$")
|
2016-03-29 22:27:04 +00:00
|
|
|
for _, p := range patterns {
|
|
|
|
r, err := regexp.Compile(p)
|
|
|
|
if err == nil {
|
|
|
|
regexps = append(regexps, r)
|
2015-08-03 22:05:34 +00:00
|
|
|
}
|
|
|
|
}
|
2016-03-29 22:27:04 +00:00
|
|
|
return
|
2015-08-03 22:05:34 +00:00
|
|
|
}
|
2017-06-01 00:11:42 +00:00
|
|
|
|
2020-03-13 23:38:24 +00:00
|
|
|
func shouldUnmountRoot(root string, info *mountinfo.Info) bool {
|
2018-01-24 23:10:01 +00:00
|
|
|
if !strings.HasSuffix(root, info.Root) {
|
|
|
|
return false
|
|
|
|
}
|
2019-08-09 10:33:15 +00:00
|
|
|
return hasMountInfoOption(info.Optional, sharedPropagationOption)
|
2018-01-24 23:10:01 +00:00
|
|
|
}
|
2018-07-17 19:11:38 +00:00
|
|
|
|
|
|
|
// setupResolvConf sets the appropriate resolv.conf file if not specified
|
|
|
|
// When systemd-resolved is running the default /etc/resolv.conf points to
|
|
|
|
// localhost. In this case fetch the alternative config file that is in a
|
|
|
|
// different path so that containers can use it
|
|
|
|
// In all the other cases fallback to the default one
|
|
|
|
func setupResolvConf(config *config.Config) {
|
|
|
|
if config.ResolvConf != "" {
|
|
|
|
return
|
|
|
|
}
|
2019-05-31 21:09:22 +00:00
|
|
|
config.ResolvConf = resolvconf.Path()
|
2018-07-17 19:11:38 +00:00
|
|
|
}
|
2023-01-16 22:25:13 +00:00
|
|
|
|
|
|
|
// ifaceAddrs returns the IPv4 and IPv6 addresses assigned to the network
|
|
|
|
// interface with name linkName.
|
|
|
|
//
|
|
|
|
// No error is returned if the named interface does not exist.
|
|
|
|
func ifaceAddrs(linkName string) (v4, v6 []*net.IPNet, err error) {
|
|
|
|
nl := ns.NlHandle()
|
|
|
|
link, err := nl.LinkByName(linkName)
|
|
|
|
if err != nil {
|
|
|
|
if !errors.As(err, new(netlink.LinkNotFoundError)) {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
return nil, nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
get := func(family int) ([]*net.IPNet, error) {
|
|
|
|
addrs, err := nl.AddrList(link, family)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
ipnets := make([]*net.IPNet, len(addrs))
|
|
|
|
for i := range addrs {
|
|
|
|
ipnets[i] = addrs[i].IPNet
|
|
|
|
}
|
|
|
|
return ipnets, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
v4, err = get(netlink.FAMILY_V4)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
v6, err = get(netlink.FAMILY_V6)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
return v4, v6, nil
|
|
|
|
}
|
2023-04-05 11:32:03 +00:00
|
|
|
|
|
|
|
var (
|
|
|
|
kernelSupportsRROOnce sync.Once
|
|
|
|
kernelSupportsRROErr error
|
|
|
|
)
|
|
|
|
|
|
|
|
func kernelSupportsRecursivelyReadOnly() error {
|
|
|
|
fn := func() error {
|
|
|
|
tmpMnt, err := os.MkdirTemp("", "moby-detect-rro")
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to create a temp directory: %w", err)
|
|
|
|
}
|
|
|
|
for {
|
|
|
|
err = unix.Mount("", tmpMnt, "tmpfs", 0, "")
|
|
|
|
if !errors.Is(err, unix.EINTR) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to mount tmpfs on %q: %w", tmpMnt, err)
|
|
|
|
}
|
|
|
|
defer func() {
|
|
|
|
var umErr error
|
|
|
|
for {
|
|
|
|
umErr = unix.Unmount(tmpMnt, 0)
|
|
|
|
if !errors.Is(umErr, unix.EINTR) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if umErr != nil {
|
2023-06-23 00:33:17 +00:00
|
|
|
log.G(context.TODO()).WithError(umErr).Warnf("Failed to unmount %q", tmpMnt)
|
2023-04-05 11:32:03 +00:00
|
|
|
}
|
|
|
|
}()
|
|
|
|
attr := &unix.MountAttr{
|
|
|
|
Attr_set: unix.MOUNT_ATTR_RDONLY,
|
|
|
|
}
|
|
|
|
for {
|
|
|
|
err = unix.MountSetattr(-1, tmpMnt, unix.AT_RECURSIVE, attr)
|
|
|
|
if !errors.Is(err, unix.EINTR) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// ENOSYS on kernel < 5.12
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to call mount_setattr: %w", err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
kernelSupportsRROOnce.Do(func() {
|
|
|
|
kernelSupportsRROErr = fn()
|
|
|
|
})
|
|
|
|
return kernelSupportsRROErr
|
|
|
|
}
|
|
|
|
|
2022-08-31 20:12:30 +00:00
|
|
|
func supportsRecursivelyReadOnly(cfg *configStore, runtime string) error {
|
2023-04-05 11:32:03 +00:00
|
|
|
if err := kernelSupportsRecursivelyReadOnly(); err != nil {
|
|
|
|
return fmt.Errorf("rro is not supported: %w (kernel is older than 5.12?)", err)
|
|
|
|
}
|
|
|
|
if runtime == "" {
|
2022-08-31 21:24:22 +00:00
|
|
|
runtime = cfg.Runtimes.Default
|
2023-04-05 11:32:03 +00:00
|
|
|
}
|
2022-08-31 20:12:30 +00:00
|
|
|
features := cfg.Runtimes.Features(runtime)
|
|
|
|
if features == nil {
|
2023-04-05 11:32:03 +00:00
|
|
|
return fmt.Errorf("rro is not supported by runtime %q: OCI features struct is not available", runtime)
|
|
|
|
}
|
2022-08-31 20:12:30 +00:00
|
|
|
for _, s := range features.MountOptions {
|
2023-04-05 11:32:03 +00:00
|
|
|
if s == "rro" {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return fmt.Errorf("rro is not supported by runtime %q", runtime)
|
|
|
|
}
|