buildah/pkg/overlay/overlay.go

262 lines
10 KiB
Go

package overlay
import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/sirupsen/logrus"
"go.podman.io/storage/pkg/idtools"
"go.podman.io/storage/pkg/mount"
"go.podman.io/storage/pkg/system"
"go.podman.io/storage/pkg/unshare"
)
// Options for MountWithOptions().
type Options struct {
// The Upper directory is normally writable layer in an overlay mount.
// Note!! : Following API does not handles escaping or validates correctness of the values
// passed to UpperDirOptionFragment instead API will try to pass values as is it
// to the `mount` command. It is user's responsibility to make sure they pre-validate
// these values. Invalid inputs may lead to undefined behaviour.
// This is provided as-is, use it if it works for you, we can/will change/break that in the future.
// See discussion here for more context: https://github.com/containers/buildah/pull/3715#discussion_r786036959
// TODO: Should we address above comment and handle escaping of metacharacters like
// `comma`, `backslash` ,`colon` and any other special characters
UpperDirOptionFragment string
// The Workdir is used to prepare files as they are switched between the layers.
// Note!! : Following API does not handles escaping or validates correctness of the values
// passed to WorkDirOptionFragment instead API will try to pass values as is it
// to the `mount` command. It is user's responsibility to make sure they pre-validate
// these values. Invalid inputs may lead to undefined behaviour.
// This is provided as-is, use it if it works for you, we can/will change/break that in the future.
// See discussion here for more context: https://github.com/containers/buildah/pull/3715#discussion_r786036959
// TODO: Should we address above comment and handle escaping of metacharacters like
// `comma`, `backslash` ,`colon` and any other special characters
WorkDirOptionFragment string
// Graph options being used by the caller, will be searched when choosing mount program
GraphOpts []string
// Mark if following overlay is read only
ReadOnly bool
// Deprecated: RootUID is not used
RootUID int
// Deprecated: RootGID is not used
RootGID int
// Force overlay mounting and return a bind mount, rather than
// attempting to optimize by having the runtime actually mount and
// manage the overlay filesystem.
ForceMount bool
// MountLabel is a label to force for the overlay filesystem.
MountLabel string
}
// TempDir generates a uniquely-named directory under ${containerDir}/overlay
// which can be used as a parent directory for the upper and working
// directories for an overlay mount, creates "upper" and "work" directories
// beneath it, and then returns the path of the new directory.
func TempDir(containerDir string, rootUID, rootGID int) (string, error) {
contentDir := filepath.Join(containerDir, "overlay")
if err := idtools.MkdirAllAs(contentDir, 0o700, rootUID, rootGID); err != nil {
return "", fmt.Errorf("failed to create the overlay %s directory: %w", contentDir, err)
}
contentDir, err := os.MkdirTemp(contentDir, "")
if err != nil {
return "", fmt.Errorf("failed to create the overlay tmpdir in %s directory: %w", contentDir, err)
}
return contentDir, generateOverlayStructure(contentDir, rootUID, rootGID)
}
// GenerateStructure generates an overlay directory structure for container content
func GenerateStructure(containerDir, containerID, name string, rootUID, rootGID int) (string, error) {
contentDir := filepath.Join(containerDir, "overlay-containers", containerID, name)
if err := idtools.MkdirAllAs(contentDir, 0o700, rootUID, rootGID); err != nil {
return "", fmt.Errorf("failed to create the overlay %s directory: %w", contentDir, err)
}
return contentDir, generateOverlayStructure(contentDir, rootUID, rootGID)
}
// generateOverlayStructure generates upper, work and merge directories under the specified directory
func generateOverlayStructure(containerDir string, rootUID, rootGID int) error {
upperDir := filepath.Join(containerDir, "upper")
workDir := filepath.Join(containerDir, "work")
if err := idtools.MkdirAllAs(upperDir, 0o700, rootUID, rootGID); err != nil {
return fmt.Errorf("creating overlay upper directory %s: %w", upperDir, err)
}
if err := idtools.MkdirAllAs(workDir, 0o700, rootUID, rootGID); err != nil {
return fmt.Errorf("creating overlay work directory %s: %w", workDir, err)
}
mergeDir := filepath.Join(containerDir, "merge")
if err := idtools.MkdirAllAs(mergeDir, 0o700, rootUID, rootGID); err != nil {
return fmt.Errorf("creating overlay merge directory %s: %w", mergeDir, err)
}
return nil
}
// Mount creates a subdir of the contentDir based on the source directory
// from the source system. It then mounts up the source directory on to the
// generated mount point and returns the mount point to the caller.
func Mount(contentDir, source, dest string, rootUID, rootGID int, graphOptions []string) (mount specs.Mount, Err error) {
overlayOpts := Options{GraphOpts: graphOptions, ReadOnly: false, RootUID: rootUID, RootGID: rootGID}
return MountWithOptions(contentDir, source, dest, &overlayOpts)
}
// MountReadOnly creates a subdir of the contentDir based on the source directory
// from the source system. It then mounts up the source directory on to the
// generated mount point and returns the mount point to the caller. Note that no
// upper layer will be created rendering it a read-only mount
func MountReadOnly(contentDir, source, dest string, rootUID, rootGID int, graphOptions []string) (mount specs.Mount, Err error) {
overlayOpts := Options{GraphOpts: graphOptions, ReadOnly: true, RootUID: rootUID, RootGID: rootGID}
return MountWithOptions(contentDir, source, dest, &overlayOpts)
}
// findMountProgram finds if any mount program is specified in the graph options.
func findMountProgram(graphOptions []string) string {
mountMap := map[string]struct{}{
".mount_program": {},
"overlay.mount_program": {},
"overlay2.mount_program": {},
}
for _, i := range graphOptions {
s := strings.SplitN(i, "=", 2)
if len(s) != 2 {
continue
}
key := s[0]
val := s[1]
if _, has := mountMap[key]; has {
return val
}
}
return ""
}
// mountWithMountProgram mounts an overlay at mergeDir using the specified
// mount program and overlay options.
func mountWithMountProgram(mountProgram, overlayOptions, mergeDir string) error {
cmd := exec.Command(mountProgram, "-o", overlayOptions, mergeDir)
if err := cmd.Run(); err != nil {
return fmt.Errorf("exec %s: %w", mountProgram, err)
}
return nil
}
// mountNatively mounts an overlay at mergeDir using the kernel's mount()
// system call.
func mountNatively(overlayOptions, mergeDir string) error {
return mount.Mount("overlay", mergeDir, "overlay", overlayOptions)
}
// Convert ":" to "\:", the path which will be overlay mounted need to be escaped
func escapeColon(source string) string {
return strings.ReplaceAll(source, ":", "\\:")
}
// RemoveTemp unmounts a filesystem mounted at ${contentDir}/merge, and then
// removes ${contentDir}, which is typically a path returned by TempDir(),
// along with any contents it might still have.
func RemoveTemp(contentDir string) error {
if err := Unmount(contentDir); err != nil {
return err
}
return os.RemoveAll(contentDir)
}
// Unmount the overlay mountpoint at ${contentDir}/merge, where ${contentDir}
// is typically a path returned by TempDir(). The mountpoint itself is left
// unmodified.
func Unmount(contentDir string) error {
mergeDir := filepath.Join(contentDir, "merge")
if unshare.IsRootless() {
// Attempt to unmount the FUSE mount using either fusermount or fusermount3.
// If they fail, fallback to unix.Unmount
for _, v := range []string{"fusermount3", "fusermount"} {
err := exec.Command(v, "-u", mergeDir).Run()
if err != nil && !errors.Is(err, exec.ErrNotFound) {
logrus.Debugf("Error unmounting %s with %s - %v", mergeDir, v, err)
}
if err == nil {
return nil
}
}
// If fusermount|fusermount3 failed to unmount the FUSE file system, attempt unmount
}
// Ignore EINVAL as the specified merge dir is not a mount point
if err := system.Unmount(mergeDir); err != nil && !errors.Is(err, os.ErrNotExist) && !errors.Is(err, syscall.EINVAL) {
return fmt.Errorf("unmount overlay %s: %w", mergeDir, err)
}
return nil
}
// recreate removes a directory tree and then recreates the top of that tree
// with the same mode and ownership.
func recreate(contentDir string) error {
st, err := system.Stat(contentDir)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil
}
return fmt.Errorf("failed to stat overlay upper directory: %w", err)
}
if err := os.RemoveAll(contentDir); err != nil {
return err
}
if err := idtools.MkdirAllAs(contentDir, os.FileMode(st.Mode()), int(st.UID()), int(st.GID())); err != nil {
return fmt.Errorf("failed to create overlay directory: %w", err)
}
return nil
}
// CleanupMount removes all temporary mountpoint content
func CleanupMount(contentDir string) (Err error) {
if err := recreate(filepath.Join(contentDir, "upper")); err != nil {
return err
}
if err := recreate(filepath.Join(contentDir, "work")); err != nil {
return err
}
return nil
}
// CleanupContent removes every temporary mountpoint created under
// ${containerDir}/overlay as a result of however many calls to TempDir(),
// roughly equivalent to calling RemoveTemp() for each of the directories whose
// paths it returned, and then removes ${containerDir} itself.
func CleanupContent(containerDir string) (Err error) {
contentDir := filepath.Join(containerDir, "overlay")
files, err := os.ReadDir(contentDir)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil
}
return fmt.Errorf("read directory: %w", err)
}
for _, f := range files {
dir := filepath.Join(contentDir, f.Name())
if err := Unmount(dir); err != nil {
return err
}
}
if err := os.RemoveAll(contentDir); err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("failed to cleanup overlay directory: %w", err)
}
return nil
}