Skip to content

Commit

Permalink
refactor(container): refactor cp flag
Browse files Browse the repository at this point in the history
Signed-off-by: Yuchao.Li <mimelyc@gmail.com>
  • Loading branch information
MimeLyc committed Jan 27, 2023
1 parent b0701ba commit 3ef661f
Show file tree
Hide file tree
Showing 5 changed files with 328 additions and 249 deletions.
43 changes: 0 additions & 43 deletions cmd/nerdctl/container_cp.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,52 +17,9 @@
package main

import (
"errors"
"path/filepath"
"strings"

"github.com/spf13/cobra"
)

var errFileSpecDoesntMatchFormat = errors.New("filespec must match the canonical format: [container:]file/path")

func parseCpFileSpec(arg string) (*cpFileSpec, error) {
i := strings.Index(arg, ":")

// filespec starting with a semicolon is invalid
if i == 0 {
return nil, errFileSpecDoesntMatchFormat
}

if filepath.IsAbs(arg) {
// Explicit local absolute path, e.g., `C:\foo` or `/foo`.
return &cpFileSpec{
Container: nil,
Path: arg,
}, nil
}

parts := strings.SplitN(arg, ":", 2)

if len(parts) == 1 || strings.HasPrefix(parts[0], ".") {
// Either there's no `:` in the arg
// OR it's an explicit local relative path like `./file:name.txt`.
return &cpFileSpec{
Path: arg,
}, nil
}

return &cpFileSpec{
Container: &parts[0],
Path: parts[1],
}, nil
}

type cpFileSpec struct {
Container *string
Path string
}

func cpShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveFilterFileExt
}
219 changes: 13 additions & 206 deletions cmd/nerdctl/container_cp_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,8 @@
package main

import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"

"github.com/containerd/containerd"
"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/containerd/nerdctl/pkg/api/types"
"github.com/containerd/nerdctl/pkg/cmd/container"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -67,208 +54,28 @@ 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])
if err != nil {
return err
}

destSpec, err := parseCpFileSpec(args[1])
if err != nil {
return err
}
options, err := processCpOptions(cmd)

flagL, err := cmd.Flags().GetBool("follow-link")
if err != nil {
return 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")
}
if srcSpec.Container == nil && destSpec.Container == nil {
return 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")
}
if destSpec.Path == "-" {
return fmt.Errorf("support for writing a tar archive to stdout is not implemented yet")
}

container2host := srcSpec.Container != nil
var container string
if container2host {
container = *srcSpec.Container
} else {
container = *destSpec.Container
}
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)
}
var proc native.Process
if err := json.Unmarshal(inspectOut, &proc); err != nil {
return err
}
if proc.Status.Status != containerd.Running {
return 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)
return container.Cp(ctx, args, options)
}

// 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
}
var (
srcIsDir bool
dstExists bool
dstExistsAsDir bool
)
st, err := os.Stat(srcFull)
func processCpOptions(cmd *cobra.Command) (types.ContainerCpOptions, error) {
flagL, err := cmd.Flags().GetBool("follow-link")
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")
return types.ContainerCpOptions{}, err
}
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
selfExe, inspectArgs := globalFlags(cmd)

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{
FollowLink: flagL,
NerdctlCmd: selfExe,
NerdctlArgs: inspectArgs,
}, nil
}
9 changes: 9 additions & 0 deletions pkg/api/types/container_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,12 @@ type ContainerExecOptions struct {
// Username or UID (format: <name|uid>[:<group|gid>])
User string
}

// ContainerExecOptions specifies options for `nerdctl (container) cp`
type ContainerCpOptions struct {
// Follow symbolic links in SRC_PATH
FollowLink bool
NerdctlCmd string
// Nerdctl Args for reconstructing additional inspect command
NerdctlArgs []string
}
Loading

0 comments on commit 3ef661f

Please sign in to comment.