From ef06dcbc48b67b2245f4970fdae538c786d5c6eb Mon Sep 17 00:00:00 2001 From: "Yuchao.Li" Date: Thu, 26 Jan 2023 16:34:21 +0800 Subject: [PATCH] refactor(container): refactor cp flag Signed-off-by: Yuchao.Li --- cmd/nerdctl/container_cp_linux.go | 200 +++++------------------------- pkg/api/types/container_types.go | 13 ++ pkg/cmd/container/cp_linux.go | 35 ++++++ pkg/containerutil/cp_linux.go | 179 ++++++++++++++++++++++++++ 4 files changed, 259 insertions(+), 168 deletions(-) create mode 100644 pkg/cmd/container/cp_linux.go create mode 100644 pkg/containerutil/cp_linux.go diff --git a/cmd/nerdctl/container_cp_linux.go b/cmd/nerdctl/container_cp_linux.go index 1df784636f..dc412ad325 100644 --- a/cmd/nerdctl/container_cp_linux.go +++ b/cmd/nerdctl/container_cp_linux.go @@ -17,21 +17,15 @@ package main import ( - "context" "encoding/json" "fmt" "os" "os/exec" - "path/filepath" - "strconv" - "strings" "github.com/containerd/containerd" + "github.com/containerd/nerdctl/pkg/api/types" + "github.com/containerd/nerdctl/pkg/cmd/container" "github.com/containerd/nerdctl/pkg/inspecttypes/native" - "github.com/containerd/nerdctl/pkg/rootlessutil" - "github.com/containerd/nerdctl/pkg/tarutil" - securejoin "github.com/cyphar/filepath-securejoin" - "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -67,32 +61,41 @@ Using 'nerdctl cp' with untrusted or malicious containers is unsupported and may } func cpAction(cmd *cobra.Command, args []string) error { - srcSpec, err := parseCpFileSpec(args[0]) + options, err := processCpOptions(cmd, args) if err != nil { return err } - destSpec, err := parseCpFileSpec(args[1]) + return container.Cp(cmd.Context(), options) +} + +func processCpOptions(cmd *cobra.Command, args []string) (types.ContainerCpOptions, error) { + flagL, err := cmd.Flags().GetBool("follow-link") if err != nil { - return err + return types.ContainerCpOptions{}, err } - flagL, err := cmd.Flags().GetBool("follow-link") + srcSpec, err := parseCpFileSpec(args[0]) if err != nil { - return err + return types.ContainerCpOptions{}, err + } + + destSpec, err := parseCpFileSpec(args[1]) + if err != nil { + return types.ContainerCpOptions{}, err } if (srcSpec.Container != nil && destSpec.Container != nil) || (len(srcSpec.Path) == 0 && len(destSpec.Path) == 0) { - return fmt.Errorf("one of src or dest must be a local file specification") + return types.ContainerCpOptions{}, fmt.Errorf("one of src or dest must be a local file specification") } if srcSpec.Container == nil && destSpec.Container == nil { - return fmt.Errorf("one of src or dest must be a container file specification") + return types.ContainerCpOptions{}, fmt.Errorf("one of src or dest must be a container file specification") } if srcSpec.Path == "-" { - return fmt.Errorf("support for reading a tar archive from stdin is not implemented yet") + return types.ContainerCpOptions{}, fmt.Errorf("support for reading a tar archive from stdin is not implemented yet") } if destSpec.Path == "-" { - return fmt.Errorf("support for writing a tar archive to stdout is not implemented yet") + return types.ContainerCpOptions{}, fmt.Errorf("support for writing a tar archive to stdout is not implemented yet") } container2host := srcSpec.Container != nil @@ -105,170 +108,31 @@ func cpAction(cmd *cobra.Command, args []string) error { ctx := cmd.Context() // cp works in the host namespace (for inspecting file permissions), so we can't directly use the Go client. + selfExe, inspectArgs := globalFlags(cmd) inspectArgs = append(inspectArgs, "container", "inspect", "--mode=native", "--format={{json .Process}}", container) inspectCmd := exec.CommandContext(ctx, selfExe, inspectArgs...) inspectCmd.Stderr = os.Stderr inspectOut, err := inspectCmd.Output() if err != nil { - return fmt.Errorf("failed to execute %v: %w", inspectCmd.Args, err) + return types.ContainerCpOptions{}, fmt.Errorf("failed to execute %v: %w", inspectCmd.Args, err) } var proc native.Process if err := json.Unmarshal(inspectOut, &proc); err != nil { - return err + return types.ContainerCpOptions{}, err } if proc.Status.Status != containerd.Running { - return fmt.Errorf("expected container status %v, got %v", containerd.Running, proc.Status.Status) + return types.ContainerCpOptions{}, fmt.Errorf("expected container status %v, got %v", containerd.Running, proc.Status.Status) } if proc.Pid <= 0 { - return fmt.Errorf("got non-positive PID %v", proc.Pid) - } - return kopy(ctx, container2host, proc.Pid, destSpec.Path, srcSpec.Path, flagL) -} - -// kopy implements `nerdctl cp`. -// -// See https://docs.docker.com/engine/reference/commandline/cp/ for the specification. -func kopy(ctx context.Context, container2host bool, pid int, dst, src string, followSymlink bool) error { - tarBinary, isGNUTar, err := tarutil.FindTarBinary() - if err != nil { - return err - } - logrus.Debugf("Detected tar binary %q (GNU=%v)", tarBinary, isGNUTar) - var srcFull, dstFull string - root := fmt.Sprintf("/proc/%d/root", pid) - if container2host { - srcFull, err = securejoin.SecureJoin(root, src) - dstFull = dst - } else { - srcFull = src - dstFull, err = securejoin.SecureJoin(root, dst) - } - if err != nil { - return err + return types.ContainerCpOptions{}, fmt.Errorf("got non-positive PID %v", proc.Pid) } - var ( - srcIsDir bool - dstExists bool - dstExistsAsDir bool - ) - st, err := os.Stat(srcFull) - if err != nil { - return err - } - srcIsDir = st.IsDir() - - // dst may not exist yet, so err is negligible - if st, err := os.Stat(dstFull); err == nil { - dstExists = true - dstExistsAsDir = st.IsDir() - } - dstEndsWithSep := strings.HasSuffix(dst, string(os.PathSeparator)) - srcEndsWithSlashDot := strings.HasSuffix(src, string(os.PathSeparator)+".") - if !srcIsDir && dstEndsWithSep && !dstExistsAsDir { - // The error is specified in https://docs.docker.com/engine/reference/commandline/cp/ - // See the `DEST_PATH does not exist and ends with /` case. - return fmt.Errorf("the destination directory must exists: %w", err) - } - if !srcIsDir && srcEndsWithSlashDot { - return fmt.Errorf("the source is not a directory") - } - if srcIsDir && dstExists && !dstExistsAsDir { - return fmt.Errorf("cannot copy a directory to a file") - } - if srcIsDir && !dstExists { - if err := os.MkdirAll(dstFull, 0755); err != nil { - return err - } - } - - var tarCDir, tarCArg string - if srcIsDir { - if !dstExists || srcEndsWithSlashDot { - // the content of the source directory is copied into this directory - tarCDir = srcFull - tarCArg = "." - } else { - // the source directory is copied into this directory - tarCDir = filepath.Dir(srcFull) - tarCArg = filepath.Base(srcFull) - } - } else { - // Prepare a single-file directory to create an archive of the source file - td, err := os.MkdirTemp("", "nerdctl-cp") - if err != nil { - return err - } - defer os.RemoveAll(td) - tarCDir = td - cp := []string{"cp", "-a"} - if followSymlink { - cp = append(cp, "-L") - } - if dstEndsWithSep || dstExistsAsDir { - tarCArg = filepath.Base(srcFull) - } else { - // Handle `nerdctl cp /path/to/file some-container:/path/to/file-with-another-name` - tarCArg = filepath.Base(dstFull) - } - cp = append(cp, srcFull, filepath.Join(td, tarCArg)) - cpCmd := exec.CommandContext(ctx, cp[0], cp[1:]...) - logrus.Debugf("executing %v", cpCmd.Args) - if out, err := cpCmd.CombinedOutput(); err != nil { - return fmt.Errorf("failed to execute %v: %w (out=%q)", cpCmd.Args, err, string(out)) - } - } - tarC := []string{tarBinary} - if followSymlink { - tarC = append(tarC, "-h") - } - tarC = append(tarC, "-c", "-f", "-", tarCArg) - tarXDir := dstFull - if !srcIsDir && !dstEndsWithSep && !dstExistsAsDir { - tarXDir = filepath.Dir(dstFull) - } - tarX := []string{tarBinary, "-x"} - if container2host && isGNUTar { - tarX = append(tarX, "--no-same-owner") - } - tarX = append(tarX, "-f", "-") - if rootlessutil.IsRootless() { - nsenter := []string{"nsenter", "-t", strconv.Itoa(pid), "-U", "--preserve-credentials", "--"} - if container2host { - tarC = append(nsenter, tarC...) - } else { - tarX = append(nsenter, tarX...) - } - } - - tarCCmd := exec.CommandContext(ctx, tarC[0], tarC[1:]...) - tarCCmd.Dir = tarCDir - tarCCmd.Stdin = nil - tarCCmd.Stderr = os.Stderr - - tarXCmd := exec.CommandContext(ctx, tarX[0], tarX[1:]...) - tarXCmd.Dir = tarXDir - tarXCmd.Stdin, err = tarCCmd.StdoutPipe() - if err != nil { - return err - } - tarXCmd.Stdout = os.Stderr - tarXCmd.Stderr = os.Stderr - - logrus.Debugf("executing %v in %q", tarCCmd.Args, tarCCmd.Dir) - if err := tarCCmd.Start(); err != nil { - return fmt.Errorf("failed to execute %v: %w", tarCCmd.Args, err) - } - logrus.Debugf("executing %v in %q", tarXCmd.Args, tarXCmd.Dir) - if err := tarXCmd.Start(); err != nil { - return fmt.Errorf("failed to execute %v: %w", tarXCmd.Args, err) - } - if err := tarCCmd.Wait(); err != nil { - return fmt.Errorf("failed to wait %v: %w", tarCCmd.Args, err) - } - if err := tarXCmd.Wait(); err != nil { - return fmt.Errorf("failed to wait %v: %w", tarXCmd.Args, err) - } - return nil + return types.ContainerCpOptions{ + Container2Host: container2host, + Pid: proc.Pid, + DestPath: destSpec.Path, + SrcPath: srcSpec.Path, + FollowSymLink: flagL, + }, nil } diff --git a/pkg/api/types/container_types.go b/pkg/api/types/container_types.go index 7341b99007..21fe91884e 100644 --- a/pkg/api/types/container_types.go +++ b/pkg/api/types/container_types.go @@ -195,3 +195,16 @@ type ContainerListOptions struct { // Filters matches containers based on given conditions. Filters []string } + +// ContainerCpOptions specifies options for `nerdctl (container) cp` +type ContainerCpOptions struct { + Container2Host bool + // Process id + Pid int + // Destination path to copy file to. + DestPath string + // Source path to copy file from. + SrcPath string + // Follow symbolic links in SRC_PATH + FollowSymLink bool +} diff --git a/pkg/cmd/container/cp_linux.go b/pkg/cmd/container/cp_linux.go new file mode 100644 index 0000000000..c1d7dcca8e --- /dev/null +++ b/pkg/cmd/container/cp_linux.go @@ -0,0 +1,35 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package container + +import ( + "context" + + "github.com/containerd/nerdctl/pkg/api/types" + "github.com/containerd/nerdctl/pkg/containerutil" +) + +// Cp copies files/folders between a running container and the local filesystem. +func Cp(ctx context.Context, options types.ContainerCpOptions) error { + return containerutil.CopyFiles( + ctx, + options.Container2Host, + options.Pid, + options.DestPath, + options.SrcPath, + options.FollowSymLink) +} diff --git a/pkg/containerutil/cp_linux.go b/pkg/containerutil/cp_linux.go new file mode 100644 index 0000000000..497ab47419 --- /dev/null +++ b/pkg/containerutil/cp_linux.go @@ -0,0 +1,179 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package containerutil + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + + "github.com/containerd/nerdctl/pkg/rootlessutil" + "github.com/containerd/nerdctl/pkg/tarutil" + securejoin "github.com/cyphar/filepath-securejoin" + "github.com/sirupsen/logrus" +) + +// CopyFiles implements `nerdctl cp`. +// +// See https://docs.docker.com/engine/reference/commandline/cp/ for the specification. +func CopyFiles(ctx context.Context, container2host bool, pid int, dst, src string, followSymlink bool) error { + tarBinary, isGNUTar, err := tarutil.FindTarBinary() + if err != nil { + return err + } + logrus.Debugf("Detected tar binary %q (GNU=%v)", tarBinary, isGNUTar) + var srcFull, dstFull string + root := fmt.Sprintf("/proc/%d/root", pid) + if container2host { + srcFull, err = securejoin.SecureJoin(root, src) + dstFull = dst + } else { + srcFull = src + dstFull, err = securejoin.SecureJoin(root, dst) + } + if err != nil { + return err + } + var ( + srcIsDir bool + dstExists bool + dstExistsAsDir bool + ) + st, err := os.Stat(srcFull) + if err != nil { + return err + } + srcIsDir = st.IsDir() + + // dst may not exist yet, so err is negligible + if st, err := os.Stat(dstFull); err == nil { + dstExists = true + dstExistsAsDir = st.IsDir() + } + dstEndsWithSep := strings.HasSuffix(dst, string(os.PathSeparator)) + srcEndsWithSlashDot := strings.HasSuffix(src, string(os.PathSeparator)+".") + if !srcIsDir && dstEndsWithSep && !dstExistsAsDir { + // The error is specified in https://docs.docker.com/engine/reference/commandline/cp/ + // See the `DEST_PATH does not exist and ends with /` case. + return fmt.Errorf("the destination directory must exists: %w", err) + } + if !srcIsDir && srcEndsWithSlashDot { + return fmt.Errorf("the source is not a directory") + } + if srcIsDir && dstExists && !dstExistsAsDir { + return fmt.Errorf("cannot copy a directory to a file") + } + if srcIsDir && !dstExists { + if err := os.MkdirAll(dstFull, 0755); err != nil { + return err + } + } + + var tarCDir, tarCArg string + if srcIsDir { + if !dstExists || srcEndsWithSlashDot { + // the content of the source directory is copied into this directory + tarCDir = srcFull + tarCArg = "." + } else { + // the source directory is copied into this directory + tarCDir = filepath.Dir(srcFull) + tarCArg = filepath.Base(srcFull) + } + } else { + // Prepare a single-file directory to create an archive of the source file + td, err := os.MkdirTemp("", "nerdctl-cp") + if err != nil { + return err + } + defer os.RemoveAll(td) + tarCDir = td + cp := []string{"cp", "-a"} + if followSymlink { + cp = append(cp, "-L") + } + if dstEndsWithSep || dstExistsAsDir { + tarCArg = filepath.Base(srcFull) + } else { + // Handle `nerdctl cp /path/to/file some-container:/path/to/file-with-another-name` + tarCArg = filepath.Base(dstFull) + } + cp = append(cp, srcFull, filepath.Join(td, tarCArg)) + cpCmd := exec.CommandContext(ctx, cp[0], cp[1:]...) + logrus.Debugf("executing %v", cpCmd.Args) + if out, err := cpCmd.CombinedOutput(); err != nil { + return fmt.Errorf("failed to execute %v: %w (out=%q)", cpCmd.Args, err, string(out)) + } + } + tarC := []string{tarBinary} + if followSymlink { + tarC = append(tarC, "-h") + } + tarC = append(tarC, "-c", "-f", "-", tarCArg) + + tarXDir := dstFull + if !srcIsDir && !dstEndsWithSep && !dstExistsAsDir { + tarXDir = filepath.Dir(dstFull) + } + tarX := []string{tarBinary, "-x"} + if container2host && isGNUTar { + tarX = append(tarX, "--no-same-owner") + } + tarX = append(tarX, "-f", "-") + if rootlessutil.IsRootless() { + nsenter := []string{"nsenter", "-t", strconv.Itoa(pid), "-U", "--preserve-credentials", "--"} + if container2host { + tarC = append(nsenter, tarC...) + } else { + tarX = append(nsenter, tarX...) + } + } + + tarCCmd := exec.CommandContext(ctx, tarC[0], tarC[1:]...) + tarCCmd.Dir = tarCDir + tarCCmd.Stdin = nil + tarCCmd.Stderr = os.Stderr + + tarXCmd := exec.CommandContext(ctx, tarX[0], tarX[1:]...) + tarXCmd.Dir = tarXDir + tarXCmd.Stdin, err = tarCCmd.StdoutPipe() + if err != nil { + return err + } + tarXCmd.Stdout = os.Stderr + tarXCmd.Stderr = os.Stderr + + logrus.Debugf("executing %v in %q", tarCCmd.Args, tarCCmd.Dir) + if err := tarCCmd.Start(); err != nil { + return fmt.Errorf("failed to execute %v: %w", tarCCmd.Args, err) + } + logrus.Debugf("executing %v in %q", tarXCmd.Args, tarXCmd.Dir) + if err := tarXCmd.Start(); err != nil { + return fmt.Errorf("failed to execute %v: %w", tarXCmd.Args, err) + } + if err := tarCCmd.Wait(); err != nil { + return fmt.Errorf("failed to wait %v: %w", tarCCmd.Args, err) + } + if err := tarXCmd.Wait(); err != nil { + return fmt.Errorf("failed to wait %v: %w", tarXCmd.Args, err) + } + return nil +}