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 }