From bcc5e51a948c1847fab1c932f1a343696a96cafd Mon Sep 17 00:00:00 2001 From: Daniel J Walsh Date: Mon, 29 Apr 2019 09:41:18 -0400 Subject: [PATCH] Add support for Overlay volumes into the container. Overlay mounts allow buildah bud and buildah from to specify a directory on the disk that will be mounted as an overlay into the container, where the overlay can be written to but when the RUN or buildah run exits, the modified files will dissapear. The basic idea is to be able to mount cache from the disk for things like yum/dnf/apt to be able to be used and modified in the contianer on a run command, but to be kept fresh for each RUN. Signed-off-by: Daniel J Walsh Closes: #1560 Approved by: giuseppe --- buildah.go | 2 ++ chroot/run.go | 12 +++++++++-- docs/buildah-bud.md | 21 ++++++++++++++++++- docs/buildah-from.md | 37 ++++++++++++++++++++++++--------- new.go | 1 + pkg/overlay/overlay.go | 46 ++++++++++++++++++++++++++++++++++++++++++ pkg/parse/parse.go | 8 ++++++-- run_linux.go | 35 ++++++++++++++++++++++++++------ tests/overlay.bats | 28 +++++++++++++++++++++++++ 9 files changed, 170 insertions(+), 20 deletions(-) create mode 100644 pkg/overlay/overlay.go create mode 100644 tests/overlay.bats diff --git a/buildah.go b/buildah.go index e29e69383..33b7afccd 100644 --- a/buildah.go +++ b/buildah.go @@ -191,6 +191,8 @@ type Builder struct { TopLayer string // Format for the build Image Format string + // TempVolumes are temporary mount points created during container runs + TempVolumes map[string]bool } // BuilderInfo are used as objects to display container information diff --git a/chroot/run.go b/chroot/run.go index 1c3ac65f3..c65926c8e 100644 --- a/chroot/run.go +++ b/chroot/run.go @@ -1071,7 +1071,7 @@ func setupChrootBindMounts(spec *specs.Spec, bundlePath string) (undoBinds func( } } // Skip anything that isn't a bind or tmpfs mount. - if m.Type != "bind" && m.Type != "tmpfs" { + if m.Type != "bind" && m.Type != "tmpfs" && m.Type != "overlay" { logrus.Debugf("skipping mount of type %q on %q", m.Type, m.Destination) continue } @@ -1083,10 +1083,12 @@ func setupChrootBindMounts(spec *specs.Spec, bundlePath string) (undoBinds func( if err != nil { return undoBinds, errors.Wrapf(err, "error examining %q for mounting in mount namespace", m.Source) } + case "overlay": + fallthrough case "tmpfs": srcinfo, err = os.Stat("/") if err != nil { - return undoBinds, errors.Wrapf(err, "error examining / to use as a template for a tmpfs") + return undoBinds, errors.Wrapf(err, "error examining / to use as a template for a %s", m.Type) } } target := filepath.Join(spec.Root.Path, m.Destination) @@ -1145,6 +1147,12 @@ func setupChrootBindMounts(spec *specs.Spec, bundlePath string) (undoBinds func( return undoBinds, errors.Wrapf(err, "error mounting tmpfs to %q in mount namespace (%q, %q)", m.Destination, target, strings.Join(m.Options, ",")) } logrus.Debugf("mounted a tmpfs to %q", target) + case "overlay": + // Mount a overlay. + if err := mount.Mount(m.Source, target, m.Type, strings.Join(append(m.Options, "private"), ",")); err != nil { + return undoBinds, errors.Wrapf(err, "error mounting overlay to %q in mount namespace (%q, %q)", m.Destination, target, strings.Join(m.Options, ",")) + } + logrus.Debugf("mounted a overlay to %q", target) } if err = unix.Statfs(target, &fs); err != nil { return undoBinds, errors.Wrapf(err, "error checking if directory %q was bound read-only", target) diff --git a/docs/buildah-bud.md b/docs/buildah-bud.md index 36d3fe58c..867ce4011 100644 --- a/docs/buildah-bud.md +++ b/docs/buildah-bud.md @@ -531,7 +531,7 @@ process. container. The `OPTIONS` are a comma delimited list and can be: * [rw|ro] - * [z|Z] + * [z|Z|O] * [`[r]shared`|`[r]slave`|`[r]private`] The `CONTAINER-DIR` must be an absolute path such as `/src/docs`. The `HOST-DIR` @@ -547,6 +547,8 @@ You can add the `:ro` or `:rw` suffix to a volume to mount it read-only or read-write mode, respectively. By default, the volumes are mounted read-write. See examples. + `Labeling Volume Mounts` + Labeling systems like SELinux require that proper labels are placed on volume content mounted into a container. Without a label, the security system might prevent the processes running inside the container from using the content. By @@ -560,6 +562,21 @@ content label. Shared volume labels allow all containers to read/write content. The `Z` option tells Buildah to label the content with a private unshared label. Only the current container can use a private volume. + `Overlay Volume Mounts` + + The `:O` flag tells Buildah to mount the directory from the host as a temporary storage using the Overlay file system. The `RUN` command containers are allowed to modify contents within the mountpoint and are stored in the container storage in a separate directory. In Ovelay FS terms the source directory will be the lower, and the container storage directory will be the upper. Modifications to the mount point are destroyed when the `RUN` command finishes executing, similar to a tmpfs mount point. + + Any subsequent execution of `RUN` commands sees the original source directory content, any changes from previous RUN commands no longer exists. + + One use case of the `overlay` mount is sharing the package cache from the host into the container to allow speeding up builds. + + Note: + + - Overlay mounts are not currently supported in rootless mode. + - The `O` flag is not allowed to be specified with the `Z` or `z` flags. Content mounted into the container is labeled with the private label. + On SELinux systems, labels in the source directory needs to be readable by the container label. If not, SELinux container separation must be disabled for the container to work. + - Modification of the directory volume mounted into the container with an overlay mount can cause unexpected failures. It is recommended that you do not modify the directory until the container finishes running. + By default bind mounted volumes are `private`. That means any mounts done inside container will not be visible on the host and vice versa. This behavior can be changed by specifying a volume mount propagation property. @@ -621,6 +638,8 @@ buildah bud --security-opt label=level:s0:c100,c200 --cgroup-parent /path/to/cgr buildah bud --volume /home/test:/myvol:ro,Z -t imageName . +buildah bud -v /var/lib/dnf:/var/lib/dnf:O -t imageName . + buildah bud --layers -t imageName . buildah bud --no-cache -t imageName . diff --git a/docs/buildah-from.md b/docs/buildah-from.md index 66382686d..27d58fd1f 100644 --- a/docs/buildah-from.md +++ b/docs/buildah-from.md @@ -174,7 +174,7 @@ value can be entered. The password is entered without echo. **--http-proxy** By default proxy environment variables are passed into the container if set -for the buildah process. This can be disabled by setting the `--http-proxy` +for the Buildah process. This can be disabled by setting the `--http-proxy` option to `false`. The environment variables passed in include `http_proxy`, `https_proxy`, `ftp_proxy`, `no_proxy`, and also the upper case versions of those. @@ -187,7 +187,7 @@ Sets the configuration for IPC namespaces when the container is subsequently used for `buildah run`. The configured value can be "" (the empty string) or "container" to indicate that a new IPC namespace should be created, or it can be "host" to indicate -that the IPC namespace in which `buildah` itself is being run should be reused, +that the IPC namespace in which `Buildah` itself is being run should be reused, or it can be the path to an IPC namespace which is already in use by another process. @@ -237,7 +237,7 @@ Sets the configuration for network namespaces when the container is subsequently used for `buildah run`. The configured value can be "" (the empty string) or "container" to indicate that a new network namespace should be created, or it can be "host" to indicate -that the network namespace in which `buildah` itself is being run should be +that the network namespace in which `Buildah` itself is being run should be reused, or it can be the path to a network namespace which is already in use by another process. @@ -247,7 +247,7 @@ Sets the configuration for PID namespaces when the container is subsequently used for `buildah run`. The configured value can be "" (the empty string) or "container" to indicate that a new PID namespace should be created, or it can be "host" to indicate -that the PID namespace in which `buildah` itself is being run should be reused, +that the PID namespace in which `Buildah` itself is being run should be reused, or it can be the path to a PID namespace which is already in use by another process. @@ -329,7 +329,7 @@ Sets the configuration for user namespaces when the container is subsequently used for `buildah run`. The configured value can be "" (the empty string) or "container" to indicate that a new user namespace should be created, it can be "host" to indicate that -the user namespace in which `buildah` itself is being run should be reused, or +the user namespace in which `Buildah` itself is being run should be reused, or it can be the path to an user namespace which is already in use by another process. @@ -383,7 +383,7 @@ filesytem level, on the container's contents, can be found in entries in the Commands run using `buildah run` will default to being run in their own user namespaces, configured using the UID and GID maps. If --userns-gid-map-group is specified, but --userns-uid-map-user is not -specified, `buildah` will assume that the specified group name is also a +specified, `Buildah` will assume that the specified group name is also a suitable user name to use as the default setting for this option. **--userns-gid-map-group** *group* @@ -394,7 +394,7 @@ filesytem level, on the container's contents, can be found in entries in the Commands run using `buildah run` will default to being run in their own user namespaces, configured using the UID and GID maps. If --userns-uid-map-user is specified, but --userns-gid-map-group is not -specified, `buildah` will assume that the specified user name is also a +specified, `Buildah` will assume that the specified user name is also a suitable group name to use as the default setting for this option. **--uts** *how* @@ -403,7 +403,7 @@ Sets the configuration for UTS namespaces when the container is subsequently used for `buildah run`. The configured value can be "" (the empty string) or "container" to indicate that a new UTS namespace should be created, or it can be "host" to indicate -that the UTS namespace in which `buildah` itself is being run should be reused, +that the UTS namespace in which `Buildah` itself is being run should be reused, or it can be the path to a UTS namespace which is already in use by another process. @@ -414,7 +414,7 @@ process. container. The `OPTIONS` are a comma delimited list and can be: * [rw|ro] - * [z|Z] + * [z|Z|O] * [`[r]shared`|`[r]slave`|`[r]private`|`[r]unbindable`] The `CONTAINER-DIR` must be an absolute path such as `/src/docs`. The `HOST-DIR` @@ -430,6 +430,8 @@ You can add the `:ro` or `:rw` suffix to a volume to mount it read-only or read-write mode, respectively. By default, the volumes are mounted read-write. See examples. + `Labeling Volume Mounts` + Labeling systems like SELinux require that proper labels are placed on volume content mounted into a container. Without a label, the security system might prevent the processes running inside the container from using the content. By @@ -443,6 +445,21 @@ content label. Shared volume labels allow all containers to read/write content. The `Z` option tells Buildah to label the content with a private unshared label. Only the current container can use a private volume. + `Overlay Volume Mounts` + + The `:O` flag tells Buildah to mount the directory from the host as a temporary storage using the Overlay file system. The `RUN` command containers are allowed to modify contents within the mountpoint and are stored in the container storage in a separate directory. In Ovelay FS terms the source directory will be the lower, and the container storage directory will be the upper. Modifications to the mount point are destroyed when the `RUN` command finishes executing, similar to a tmpfs mount point. + + Any subsequent execution of `RUN` commands sees the original source directory content, any changes from previous RUN commands no longer exists. + + One use case of the `overlay` mount is sharing the package cache from the host into the container to allow speeding up builds. + + Note: + + - Overlay mounts are not currently supported in rootless mode. + - The `O` flag is not allowed to be specified with the `Z` or `z` flags. Content mounted into the container is labeled with the private label. + On SELinux systems, labels in the source directory needs to be readable by the container label. If not, SELinux container separation must be disabled for the container to work. + - Modification of the directory volume mounted into the container with an overlay mount can cause unexpected failures. It is recommended that you do not modify the directory until the container finishes running. + By default bind mounted volumes are `private`. That means any mounts done inside container will not be visible on the host and vice versa. This behavior can be changed by specifying a volume mount propagation property. @@ -502,6 +519,8 @@ buildah from --ulimit nofile=1024:1028 --cgroup-parent /path/to/cgroup/parent my buildah from --volume /home/test:/myvol:ro,Z myregistry/myrepository/imagename:imagetag +buildah from -v /var/lib/yum:/var/lib/yum:O myregistry/myrepository/imagename:imagetag + ## Files **registries.conf** (`/etc/containers/registries.conf`) diff --git a/new.go b/new.go index 29546caba..ecd1666bf 100644 --- a/new.go +++ b/new.go @@ -351,6 +351,7 @@ func newBuilder(ctx context.Context, store storage.Store, options BuilderOptions TopLayer: topLayer, Args: options.Args, Format: options.Format, + TempVolumes: map[string]bool{}, } if options.Mount { diff --git a/pkg/overlay/overlay.go b/pkg/overlay/overlay.go new file mode 100644 index 000000000..31f0c2cec --- /dev/null +++ b/pkg/overlay/overlay.go @@ -0,0 +1,46 @@ +package overlay + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/containers/storage" + "github.com/containers/storage/pkg/idtools" + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" +) + +// MountTemp creates a subdir of the contentDir based on the source directory +// from the source system. It then mounds up the source directory on to the +// generated mount point and returns the mount point to the caller. +func MountTemp(store storage.Store, containerId, source, dest string, rootUID, rootGID int) (specs.Mount, string, error) { + mount := specs.Mount{} + + contentDir, err := store.ContainerDirectory(containerId) + if err != nil { + return mount, "", err + } + upperDir := filepath.Join(contentDir, "upper") + workDir := filepath.Join(contentDir, "work") + if err := idtools.MkdirAllAs(upperDir, 0700, rootUID, rootGID); err != nil { + return mount, "", errors.Wrapf(err, "failed to create the overlay %s directory", upperDir) + } + if err := idtools.MkdirAllAs(workDir, 0700, rootUID, rootGID); err != nil { + return mount, "", errors.Wrapf(err, "failed to create the overlay %s directory", workDir) + } + + mount.Source = "overlay" + mount.Destination = dest + mount.Type = "overlay" + mount.Options = strings.Split(fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s,private", source, upperDir, workDir), ",") + + return mount, contentDir, nil +} + +// RemoveTemp removes temporary mountpoint and all content from its parent +// directory +func RemoveTemp(contentDir string) error { + return os.RemoveAll(contentDir) +} diff --git a/pkg/parse/parse.go b/pkg/parse/parse.go index 3dfc2b98e..6c58f1194 100644 --- a/pkg/parse/parse.go +++ b/pkg/parse/parse.go @@ -14,6 +14,7 @@ import ( "unicode" "github.com/containers/buildah" + "github.com/containers/buildah/pkg/unshare" "github.com/containers/image/types" "github.com/containers/storage/pkg/idtools" "github.com/docker/go-units" @@ -216,9 +217,12 @@ func ValidateVolumeOpts(option string) error { return errors.Errorf("invalid options %q, can only specify 1 'rw' or 'ro' option", option) } foundRWRO++ - case "z", "Z": + case "z", "Z", "O": + if opt == "O" && unshare.IsRootless() { + return errors.Errorf("invalid options %q, overlay mounts not supported in rootless mode", option) + } if foundLabelChange > 1 { - return errors.Errorf("invalid options %q, can only specify 1 'z' or 'Z' option", option) + return errors.Errorf("invalid options %q, can only specify 1 'z', 'Z', or 'O' option", option) } foundLabelChange++ case "private", "rprivate", "shared", "rshared", "slave", "rslave", "unbindable", "runbindable": diff --git a/run_linux.go b/run_linux.go index e5239b3a8..81ce2b944 100644 --- a/run_linux.go +++ b/run_linux.go @@ -23,6 +23,7 @@ import ( "github.com/containernetworking/cni/libcni" "github.com/containers/buildah/bind" "github.com/containers/buildah/chroot" + "github.com/containers/buildah/pkg/overlay" "github.com/containers/buildah/pkg/secrets" "github.com/containers/buildah/pkg/unshare" "github.com/containers/buildah/util" @@ -184,6 +185,7 @@ func (b *Builder) Run(command []string, options RunOptions) error { if err != nil { return errors.Wrapf(err, "error resolving mountpoints for container %q", b.ContainerID) } + defer b.cleanupTempVolumes() if options.CNIConfigDir == "" { options.CNIConfigDir = b.CNIConfigDir @@ -438,7 +440,7 @@ func (b *Builder) setupMounts(mountPoint string, spec *specs.Spec, bundlePath st } // Get the list of explicitly-specified volume mounts. - volumes, err := runSetupVolumeMounts(spec.Linux.MountLabel, volumeMounts, optionMounts) + volumes, err := b.runSetupVolumeMounts(spec.Linux.MountLabel, volumeMounts, optionMounts, int(rootUID), int(rootGID)) if err != nil { return err } @@ -1537,11 +1539,21 @@ func addRlimits(ulimit []string, g *generate.Generator) error { return nil } -func runSetupVolumeMounts(mountLabel string, volumeMounts []string, optionMounts []specs.Mount) ([]specs.Mount, error) { - var mounts []specs.Mount +func (b *Builder) cleanupTempVolumes() { + for tempVolume, val := range b.TempVolumes { + if val { + if err := overlay.RemoveTemp(tempVolume); err != nil { + logrus.Errorf(err.Error()) + } + b.TempVolumes[tempVolume] = false + } + } +} + +func (b *Builder) runSetupVolumeMounts(mountLabel string, volumeMounts []string, optionMounts []specs.Mount, rootUID, rootGID int) (mounts []specs.Mount, Err error) { parseMount := func(host, container string, options []string) (specs.Mount, error) { - var foundrw, foundro, foundz, foundZ bool + var foundrw, foundro, foundz, foundZ, foundO bool var rootProp string for _, opt := range options { switch opt { @@ -1553,6 +1565,8 @@ func runSetupVolumeMounts(mountLabel string, volumeMounts []string, optionMounts foundz = true case "Z": foundZ = true + case "O": + foundO = true case "private", "rprivate", "slave", "rslave", "shared", "rshared": rootProp = opt } @@ -1570,6 +1584,14 @@ func runSetupVolumeMounts(mountLabel string, volumeMounts []string, optionMounts return specs.Mount{}, errors.Wrapf(err, "relabeling %q failed", host) } } + if foundO { + overlayMount, contentDir, err := overlay.MountTemp(b.store, b.ContainerID, host, container, rootUID, rootGID) + if err == nil { + + b.TempVolumes[contentDir] = true + } + return overlayMount, err + } if rootProp == "" { options = append(options, "private") } @@ -1577,13 +1599,14 @@ func runSetupVolumeMounts(mountLabel string, volumeMounts []string, optionMounts Destination: container, Type: "bind", Source: host, - Options: options, + Options: append(options, "rbind"), }, nil } + // Bind mount volumes specified for this particular Run() invocation for _, i := range optionMounts { logrus.Debugf("setting up mounted volume at %q", i.Destination) - mount, err := parseMount(i.Source, i.Destination, append(i.Options, "rbind")) + mount, err := parseMount(i.Source, i.Destination, i.Options) if err != nil { return nil, err } diff --git a/tests/overlay.bats b/tests/overlay.bats new file mode 100644 index 000000000..b3323c16e --- /dev/null +++ b/tests/overlay.bats @@ -0,0 +1,28 @@ +#!/usr/bin/env bats + +load helpers + +@test "overlay specific level" { + if test -o "$BUILDAH_ISOLATION" = "rootless" ; then + skip "BUILDAH_ISOLATION = $BUILDAH_ISOLATION" + fi + image=alpine + mkdir ${TESTDIR}/lower + touch ${TESTDIR}/lower/foo + +cid=$(buildah --debug=false from -v ${TESTDIR}/lower:/lower:O --quiet --signature-policy ${TESTSDIR}/policy.json $image) + + # This should succeed + run_buildah --debug=false run $cid ls /lower/foo + + # Create and remove content in the overlay directory, should succeed + run_buildah --debug=false run $cid touch /lower/bar + run_buildah --debug=false run $cid rm /lower/foo + + # This should fail, second runs of containers go back to original + run_buildah 1 --debug=false run $cid ls /lower/bar + + # This should fail + run ls ${TESTDIR}/lower/bar + [ "$status" -ne 0 ] +}