github.com/containerd/nerdctl@v1.7.7/pkg/containerutil/cp_linux.go (about)

     1  /*
     2     Copyright The containerd Authors.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package containerutil
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"io/fs"
    24  	"os"
    25  	"os/exec"
    26  	"path"
    27  	"path/filepath"
    28  	"strconv"
    29  	"strings"
    30  
    31  	"github.com/containerd/containerd"
    32  	"github.com/containerd/containerd/mount"
    33  	"github.com/containerd/errdefs"
    34  	"github.com/containerd/log"
    35  	"github.com/containerd/nerdctl/pkg/rootlessutil"
    36  	"github.com/containerd/nerdctl/pkg/tarutil"
    37  	securejoin "github.com/cyphar/filepath-securejoin"
    38  )
    39  
    40  // CopyFiles implements `nerdctl cp`.
    41  // See https://docs.docker.com/engine/reference/commandline/cp/ for the specification.
    42  func CopyFiles(ctx context.Context, client *containerd.Client, container containerd.Container, container2host bool, dst, src string, snapshotter string, followSymlink bool) error {
    43  	tarBinary, isGNUTar, err := tarutil.FindTarBinary()
    44  	if err != nil {
    45  		return err
    46  	}
    47  	log.G(ctx).Debugf("Detected tar binary %q (GNU=%v)", tarBinary, isGNUTar)
    48  	var srcFull, dstFull, root, mountDestination, containerPath string
    49  	var cleanup func()
    50  	task, err := container.Task(ctx, nil)
    51  	if err != nil {
    52  		// FIXME: Rootless does not support copying into/out of stopped/created containers as we need to nsenter into the user namespace of the
    53  		// pid of the running container with --preserve-credentials to preserve uid/gid mapping and copy files into the container.
    54  		if rootlessutil.IsRootless() {
    55  			return errors.New("cannot use cp with stopped containers in rootless mode")
    56  		}
    57  		// if the task is simply not found, we should try to mount the snapshot. any other type of error from Task() is fatal here.
    58  		if !errdefs.IsNotFound(err) {
    59  			return err
    60  		}
    61  		if container2host {
    62  			containerPath = src
    63  		} else {
    64  			containerPath = dst
    65  		}
    66  		// Check if containerPath is in a volume
    67  		root, mountDestination, err = getContainerMountInfo(ctx, container, containerPath, container2host)
    68  		if err != nil {
    69  			return err
    70  		}
    71  		// if containerPath is in a volume and not read-only in case of host2container copy then handle volume paths,
    72  		// else containerPath is not in volume so mount container snapshot for copy
    73  		if root != "" {
    74  			dst, src = handleVolumePaths(container2host, dst, src, mountDestination)
    75  		} else {
    76  			root, cleanup, err = mountSnapshotForContainer(ctx, client, container, snapshotter)
    77  			if cleanup != nil {
    78  				defer cleanup()
    79  			}
    80  			if err != nil {
    81  				return err
    82  			}
    83  		}
    84  	} else {
    85  		status, err := task.Status(ctx)
    86  		if err != nil {
    87  			return err
    88  		}
    89  		if status.Status == containerd.Running {
    90  			root = fmt.Sprintf("/proc/%d/root", task.Pid())
    91  		} else {
    92  			if rootlessutil.IsRootless() {
    93  				return fmt.Errorf("cannot use cp with stopped containers in rootless mode")
    94  			}
    95  			if container2host {
    96  				containerPath = src
    97  			} else {
    98  				containerPath = dst
    99  			}
   100  			root, mountDestination, err = getContainerMountInfo(ctx, container, containerPath, container2host)
   101  			if err != nil {
   102  				return err
   103  			}
   104  			// if containerPath is in a volume and not read-only in case of host2container copy then handle volume paths,
   105  			// else containerPath is not in volume so mount container snapshot for copy
   106  			if root != "" {
   107  				dst, src = handleVolumePaths(container2host, dst, src, mountDestination)
   108  			} else {
   109  				root, cleanup, err = mountSnapshotForContainer(ctx, client, container, snapshotter)
   110  				if cleanup != nil {
   111  					defer cleanup()
   112  				}
   113  				if err != nil {
   114  					return err
   115  				}
   116  			}
   117  		}
   118  	}
   119  	if container2host {
   120  		srcFull, err = securejoin.SecureJoin(root, src)
   121  		dstFull = dst
   122  	} else {
   123  		srcFull = src
   124  		dstFull, err = securejoin.SecureJoin(root, dst)
   125  	}
   126  	if err != nil {
   127  		return err
   128  	}
   129  	var (
   130  		srcIsDir       bool
   131  		dstExists      bool
   132  		dstExistsAsDir bool
   133  		st             fs.FileInfo
   134  	)
   135  	st, err = os.Stat(srcFull)
   136  	if err != nil {
   137  		return err
   138  	}
   139  	srcIsDir = st.IsDir()
   140  
   141  	// dst may not exist yet, so err is negligible
   142  	if st, err := os.Stat(dstFull); err == nil {
   143  		dstExists = true
   144  		dstExistsAsDir = st.IsDir()
   145  	}
   146  	dstEndsWithSep := strings.HasSuffix(dst, string(os.PathSeparator))
   147  	srcEndsWithSlashDot := strings.HasSuffix(src, string(os.PathSeparator)+".")
   148  	if !srcIsDir && dstEndsWithSep && !dstExistsAsDir {
   149  		// The error is specified in https://docs.docker.com/engine/reference/commandline/cp/
   150  		// See the `DEST_PATH does not exist and ends with /` case.
   151  		return fmt.Errorf("the destination directory must exists: %w", err)
   152  	}
   153  	if !srcIsDir && srcEndsWithSlashDot {
   154  		return fmt.Errorf("the source is not a directory")
   155  	}
   156  	if srcIsDir && dstExists && !dstExistsAsDir {
   157  		return fmt.Errorf("cannot copy a directory to a file")
   158  	}
   159  	if srcIsDir && !dstExists {
   160  		if err := os.MkdirAll(dstFull, 0o755); err != nil {
   161  			return err
   162  		}
   163  	}
   164  
   165  	var tarCDir, tarCArg string
   166  	if srcIsDir {
   167  		if !dstExists || srcEndsWithSlashDot {
   168  			// the content of the source directory is copied into this directory
   169  			tarCDir = srcFull
   170  			tarCArg = "."
   171  		} else {
   172  			// the source directory is copied into this directory
   173  			tarCDir = filepath.Dir(srcFull)
   174  			tarCArg = filepath.Base(srcFull)
   175  		}
   176  	} else {
   177  		// Prepare a single-file directory to create an archive of the source file
   178  		td, err := os.MkdirTemp("", "nerdctl-cp")
   179  		if err != nil {
   180  			return err
   181  		}
   182  		defer os.RemoveAll(td)
   183  		tarCDir = td
   184  		cp := []string{"cp", "-a"}
   185  		if followSymlink {
   186  			cp = append(cp, "-L")
   187  		}
   188  		if dstEndsWithSep || dstExistsAsDir {
   189  			tarCArg = filepath.Base(srcFull)
   190  		} else {
   191  			// Handle `nerdctl cp /path/to/file some-container:/path/to/file-with-another-name`
   192  			tarCArg = filepath.Base(dstFull)
   193  		}
   194  		cp = append(cp, srcFull, filepath.Join(td, tarCArg))
   195  		cpCmd := exec.CommandContext(ctx, cp[0], cp[1:]...)
   196  		log.G(ctx).Debugf("executing %v", cpCmd.Args)
   197  		if out, err := cpCmd.CombinedOutput(); err != nil {
   198  			return fmt.Errorf("failed to execute %v: %w (out=%q)", cpCmd.Args, err, string(out))
   199  		}
   200  	}
   201  	tarC := []string{tarBinary}
   202  	if followSymlink {
   203  		tarC = append(tarC, "-h")
   204  	}
   205  	tarC = append(tarC, "-c", "-f", "-", tarCArg)
   206  
   207  	tarXDir := dstFull
   208  	if !srcIsDir && !dstEndsWithSep && !dstExistsAsDir {
   209  		tarXDir = filepath.Dir(dstFull)
   210  	}
   211  	tarX := []string{tarBinary, "-x"}
   212  	if container2host && isGNUTar {
   213  		tarX = append(tarX, "--no-same-owner")
   214  	}
   215  	tarX = append(tarX, "-f", "-")
   216  
   217  	if rootlessutil.IsRootless() {
   218  		nsenter := []string{"nsenter", "-t", strconv.Itoa(int(task.Pid())), "-U", "--preserve-credentials", "--"}
   219  		if container2host {
   220  			tarC = append(nsenter, tarC...)
   221  		} else {
   222  			tarX = append(nsenter, tarX...)
   223  		}
   224  	}
   225  
   226  	tarCCmd := exec.CommandContext(ctx, tarC[0], tarC[1:]...)
   227  	tarCCmd.Dir = tarCDir
   228  	tarCCmd.Stdin = nil
   229  	tarCCmd.Stderr = os.Stderr
   230  
   231  	tarXCmd := exec.CommandContext(ctx, tarX[0], tarX[1:]...)
   232  	tarXCmd.Dir = tarXDir
   233  	tarXCmd.Stdin, err = tarCCmd.StdoutPipe()
   234  	if err != nil {
   235  		return err
   236  	}
   237  	tarXCmd.Stdout = os.Stderr
   238  	tarXCmd.Stderr = os.Stderr
   239  
   240  	log.G(ctx).Debugf("executing %v in %q", tarCCmd.Args, tarCCmd.Dir)
   241  	if err := tarCCmd.Start(); err != nil {
   242  		return fmt.Errorf("failed to execute %v: %w", tarCCmd.Args, err)
   243  	}
   244  	log.G(ctx).Debugf("executing %v in %q", tarXCmd.Args, tarXCmd.Dir)
   245  	if err := tarXCmd.Start(); err != nil {
   246  		return fmt.Errorf("failed to execute %v: %w", tarXCmd.Args, err)
   247  	}
   248  	if err := tarCCmd.Wait(); err != nil {
   249  		return fmt.Errorf("failed to wait %v: %w", tarCCmd.Args, err)
   250  	}
   251  	if err := tarXCmd.Wait(); err != nil {
   252  		return fmt.Errorf("failed to wait %v: %w", tarXCmd.Args, err)
   253  	}
   254  	return nil
   255  }
   256  
   257  func mountSnapshotForContainer(ctx context.Context, client *containerd.Client, container containerd.Container, snapshotter string) (string, func(), error) {
   258  	cinfo, err := container.Info(ctx)
   259  	if err != nil {
   260  		return "", nil, err
   261  	}
   262  	snapKey := cinfo.SnapshotKey
   263  	resp, err := client.SnapshotService(snapshotter).Mounts(ctx, snapKey)
   264  	if err != nil {
   265  		return "", nil, err
   266  	}
   267  	tempDir, err := os.MkdirTemp("", "nerdctl-cp-")
   268  	if err != nil {
   269  		return "", nil, err
   270  	}
   271  	err = mount.All(resp, tempDir)
   272  	if err != nil {
   273  		return "", nil, fmt.Errorf("failed to mount snapshot with error %s", err.Error())
   274  	}
   275  	cleanup := func() {
   276  		err = mount.Unmount(tempDir, 0)
   277  		if err != nil {
   278  			log.G(ctx).Warnf("failed to unmount %s with error %s", tempDir, err.Error())
   279  			return
   280  		}
   281  		os.RemoveAll(tempDir)
   282  	}
   283  	return tempDir, cleanup, nil
   284  }
   285  
   286  func getContainerMountInfo(ctx context.Context, con containerd.Container, containerPath string, container2host bool) (string, string, error) {
   287  	filePath := filepath.Clean(containerPath)
   288  	spec, err := con.Spec(ctx)
   289  	if err != nil {
   290  		return "", "", err
   291  	}
   292  	// read-only applies only while copying into container from host
   293  	if !container2host && spec.Root.Readonly {
   294  		return "", "", fmt.Errorf("container rootfs: %s is marked read-only", spec.Root.Path)
   295  	}
   296  
   297  	for _, mount := range spec.Mounts {
   298  		if isSelfOrAscendant(filePath, mount.Destination) {
   299  			// read-only applies only while copying into container from host
   300  			if !container2host {
   301  				for _, option := range mount.Options {
   302  					if option == "ro" {
   303  						return "", "", fmt.Errorf("mount point %s is marked read-only", filePath)
   304  					}
   305  				}
   306  			}
   307  			return mount.Source, mount.Destination, nil
   308  		}
   309  	}
   310  	return "", "", nil
   311  }
   312  
   313  func isSelfOrAscendant(filePath, potentialAncestor string) bool {
   314  	if filePath == "/" || filePath == "" || potentialAncestor == "" {
   315  		return false
   316  	}
   317  	filePath = filepath.Clean(filePath)
   318  	potentialAncestor = filepath.Clean(potentialAncestor)
   319  	if filePath == potentialAncestor {
   320  		return true
   321  	}
   322  	return isSelfOrAscendant(path.Dir(filePath), potentialAncestor)
   323  }
   324  
   325  // When the path is in volume remove directory that volume is mounted on from the path
   326  func handleVolumePaths(container2host bool, dst string, src string, mountDestination string) (string, string) {
   327  	if container2host {
   328  		return dst, strings.TrimPrefix(filepath.Clean(src), mountDestination)
   329  	}
   330  	return strings.TrimPrefix(filepath.Clean(dst), mountDestination), src
   331  }