Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions cmd/nerdctl/container/container_cp_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,6 @@ func copyOptions(cmd *cobra.Command, args []string) (types.ContainerCpOptions, e
if srcSpec.Container == nil && destSpec.Container == nil {
return types.ContainerCpOptions{}, fmt.Errorf("one of src or dest must be a container file specification")
}
if srcSpec.Path == "-" {
return types.ContainerCpOptions{}, fmt.Errorf("support for reading a tar archive from stdin is not implemented yet")
}
if destSpec.Path == "-" {
return types.ContainerCpOptions{}, fmt.Errorf("support for writing a tar archive to stdout is not implemented yet")
}

container2host := srcSpec.Container != nil
var containerReq string
Expand All @@ -128,6 +122,8 @@ func copyOptions(cmd *cobra.Command, args []string) (types.ContainerCpOptions, e
DestPath: destSpec.Path,
SrcPath: srcSpec.Path,
FollowSymLink: flagL,
FromStdin: srcSpec.Path == "-",
ToStdout: destSpec.Path == "-",
}, nil
}

Expand All @@ -138,6 +134,12 @@ func AddCpCommand(rootCmd *cobra.Command) {
var errFileSpecDoesntMatchFormat = errors.New("filespec must match the canonical format: [container:]file/path")

func parseCpFileSpec(arg string) (*copyFileSpec, error) {
if arg == "" {
return &copyFileSpec{
Path: "-",
}, nil
}

i := strings.Index(arg, ":")

// filespec starting with a semicolon is invalid
Expand Down
213 changes: 191 additions & 22 deletions cmd/nerdctl/container/container_cp_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package container
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
Expand All @@ -29,6 +30,7 @@ import (

"github.com/containerd/nerdctl/v2/pkg/containerutil"
"github.com/containerd/nerdctl/v2/pkg/rootlessutil"
"github.com/containerd/nerdctl/v2/pkg/tarutil"
"github.com/containerd/nerdctl/v2/pkg/testutil"
"github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
)
Expand All @@ -51,7 +53,9 @@ const (
pathIsADirAbsolute = string(os.PathSeparator) + "is-a-dir" + complexify
pathIsAVolumeMount = string(os.PathSeparator) + "is-a-volume-mount" + complexify

srcFileName = "test-file" + complexify
srcFileName = "test-file" + complexify
tarballName = "test-tar" + complexify
cpFolderName = "nerdctl-cp-test"

// Since nerdctl cp must NOT obey container wd, but instead resolve paths against the root, we set this
// explicitly to ensure we do the right thing wrt that.
Expand Down Expand Up @@ -400,6 +404,49 @@ func TestCopyToContainer(t *testing.T) {
},
},
},
{
description: "Copying to container, SRC_PATH is stdin",
sourceSpec: "-",
sourceIsAFile: true,
toContainer: true,
testCases: []testcases{
{
description: "DEST_PATH is a directory, relative",
destinationSpec: pathIsADirRelative,
catFile: filepath.Join(pathIsADirRelative, srcFileName),
setup: func(base *testutil.Base, container string, destPath string) {
base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK()
},
},
{
description: "DEST_PATH is a directory, absolute",
destinationSpec: pathIsADirAbsolute,
catFile: filepath.Join(pathIsADirAbsolute, srcFileName),
setup: func(base *testutil.Base, container string, destPath string) {
base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK()
},
},
{
description: "DEST_PATH is stdout",
destinationSpec: "-",
expect: icmd.Expected{
ExitCode: 1,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need error type, otherwise test is not clear why it should fail. may be also adding a bit of description on the error would help

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added errors

Err: "one of src or dest must be a container file specification",
},
},
{
description: "DEST_PATH is a file",
destinationSpec: pathIsAFileAbsolute,
setup: func(base *testutil.Base, container string, destPath string) {
base.Cmd("exec", container, "touch", destPath).AssertOK()
},
expect: icmd.Expected{
ExitCode: 1,
Err: containerutil.ErrCannotCopyDirToFile.Error(),
},
},
},
},
}

for _, tg := range testGroups {
Expand Down Expand Up @@ -540,6 +587,19 @@ func TestCopyFromContainer(t *testing.T) {
assert.NilError(t, err)
},
},
{
description: "DEST_PATH is stdout",
destinationSpec: "-",
// Extra dir to account for folder created from extracted tar file
catFile: filepath.Join(pathIsADirAbsolute, filepath.Base(srcDirName), srcFileName),
expect: icmd.Expected{
ExitCode: 0,
},
setup: func(base *testutil.Base, container string, destPath string) {
err := os.MkdirAll(destPath, dirPerm)
assert.NilError(t, err)
},
},
},
},
{
Expand Down Expand Up @@ -682,6 +742,19 @@ func TestCopyFromContainer(t *testing.T) {
assert.NilError(t, err)
},
},
{
description: "DEST_PATH is stdout",
destinationSpec: "-",
catFile: filepath.Join(pathIsADirAbsolute, srcDirName, srcFileName),
expect: icmd.Expected{
ExitCode: 0,
},
setup: func(base *testutil.Base, container string, destPath string) {
// Don't make the topmost dir as this is where the tarball must extract
err := os.MkdirAll(filepath.Dir(destPath), dirPerm)
assert.NilError(t, err)
},
},
},
},

Expand Down Expand Up @@ -713,6 +786,18 @@ func TestCopyFromContainer(t *testing.T) {
assert.NilError(t, err)
},
},
{
description: "DEST_PATH is stdout",
destinationSpec: "-",
catFile: filepath.Join(pathIsADirAbsolute, srcFileName),
expect: icmd.Expected{
ExitCode: 0,
},
setup: func(base *testutil.Base, container string, destPath string) {
err := os.MkdirAll(destPath, dirPerm)
assert.NilError(t, err)
},
},
},
},
}
Expand Down Expand Up @@ -749,7 +834,12 @@ func cpTestHelper(t *testing.T, tg *testgroup) {
// Get the source path
groupSourceSpec := tg.sourceSpec
groupSourceDir := groupSourceSpec
if tg.sourceIsAFile {
fromStdin := false
if tg.sourceSpec == "-" {
groupSourceSpec = filepath.Join(srcDirName, tarballName)
groupSourceDir = srcDirName
fromStdin = true
} else if tg.sourceIsAFile {
groupSourceDir = filepath.Dir(groupSourceSpec)
}

Expand Down Expand Up @@ -794,25 +884,37 @@ func cpTestHelper(t *testing.T, tg *testgroup) {

// Prepare the specs and derived variables
sourceSpec := groupSourceSpec
catFile := testCase.catFile

destinationSpec := testCase.destinationSpec
toStdout := false
// tarball destination just sets up the dir to extract to
if destinationSpec == "-" {
toStdout = true
destinationSpec = filepath.Dir(catFile)
}

// If the test case does not specify a catFile, start with the destination spec
catFile := testCase.catFile
if catFile == "" {
catFile = destinationSpec
}

sourceFile := filepath.Join(groupSourceDir, srcFileName)
if copyToContainer {
// Use an absolute path for evaluation
if !filepath.IsAbs(catFile) {
catFile = filepath.Join(string(os.PathSeparator), catFile)
}
// If the sourceFile is still relative, make it absolute to the temp
sourceFile = filepath.Join(tempDir, sourceFile)
// If the spec path for source on the host was absolute, make sure we put that under tempDir
if filepath.IsAbs(sourceSpec) {
sourceSpec = tempDir + sourceSpec

if fromStdin {
sourceFile = filepath.Join(tempDir, groupSourceDir, tarballName)
} else {
// Use an absolute path for evaluation
// If the sourceFile is still relative, make it absolute to the temp
sourceFile = filepath.Join(tempDir, sourceFile)
// If the spec path for source on the host was absolute, make sure we put that under tempDir
if filepath.IsAbs(sourceSpec) {
sourceSpec = tempDir + sourceSpec
}
}
} else {
// If we are copying to host, we need to make sure we have an absolute path to cat, relative to temp,
Expand All @@ -835,11 +937,29 @@ func cpTestHelper(t *testing.T, tg *testgroup) {
}

createFileOnHost := func() {
// Create file on the host
err := os.MkdirAll(filepath.Dir(sourceFile), dirPerm)
assert.NilError(t, err)
err = os.WriteFile(sourceFile, sourceFileContent, filePerm)
assert.NilError(t, err)
switch fromStdin {
case true:
d := filepath.Dir(sourceFile)
tarCpFolder := filepath.Join(d, cpFolderName)
tarBinary, _, err := tarutil.FindTarBinary()
assert.NilError(t, err)

err = os.MkdirAll(tarCpFolder, dirPerm)
assert.NilError(t, err)
err = os.WriteFile(filepath.Join(tarCpFolder, srcFileName), sourceFileContent, filePerm)
assert.NilError(t, err)

err = exec.Command(tarBinary, "-cf", sourceFile, "-C", tarCpFolder, ".").Run()
assert.NilError(t, err)
err = os.RemoveAll(tarCpFolder)
assert.NilError(t, err)
case false:
// Create file on the host
err := os.MkdirAll(filepath.Dir(sourceFile), dirPerm)
assert.NilError(t, err)
err = os.WriteFile(sourceFile, sourceFileContent, filePerm)
assert.NilError(t, err)
}
}

// Setup: create volume, containers, create the source file
Expand Down Expand Up @@ -906,10 +1026,46 @@ func cpTestHelper(t *testing.T, tg *testgroup) {
// Build the final src and dest specifiers, including `containerXYZ:`
container := ""
if copyToContainer {
if fromStdin {
if toStdout {
nerdctlCmd := base.Cmd("cp", "-", "-")
nerdctlCmd.Run()
nerdctlCmd.Assert(testCase.expect)
} else {
sourceSpec = "-"
f, err := os.Open(sourceFile)
assert.NilError(t, err)
nerdctlCmd := base.Cmd("cp", sourceSpec, containerRunning+":"+destinationSpec)
nerdctlCmd.Stdin = f

nerdctlCmd.Run()
nerdctlCmd.Assert(testCase.expect)
f.Close()
}
} else {
base.Cmd("cp", sourceSpec, containerRunning+":"+destinationSpec).Assert(testCase.expect)
}
container = containerRunning
base.Cmd("cp", sourceSpec, containerRunning+":"+destinationSpec).Assert(testCase.expect)
} else {
base.Cmd("cp", containerRunning+":"+sourceSpec, destinationSpec).Assert(testCase.expect)
nerdctlCmd := base.Cmd("cp", containerRunning+":"+sourceSpec, destinationSpec)
if toStdout {
out := nerdctlCmd.Out()
nerdctlCmd.Assert(testCase.expect)

// Since we can't check tar file directly easily, extract to the same destination
tarDst := filepath.Dir(catFile)
tarBinary, _, err := tarutil.FindTarBinary()
assert.NilError(t, err)

tarCmd := exec.Command(tarBinary, "-C", tarDst, "-xf", "-")
tarCmd.Stdin = strings.NewReader(out)
tarCmd.Stdout = os.Stdout

tarCmd.Run()
assert.NilError(t, tarCmd.Err)
} else {
nerdctlCmd.Assert(testCase.expect)
}
}

// Run the actual test for the running container
Expand All @@ -932,19 +1088,32 @@ func cpTestHelper(t *testing.T, tg *testgroup) {
// ... and for the stopped container
container = ""
var cmd *testutil.Cmd
if copyToContainer {
if fromStdin && toStdout {
cmd = base.Cmd("cp", "-", "-")
} else if copyToContainer {
container = containerStopped
cmd = base.Cmd("cp", sourceSpec, containerStopped+":"+destinationSpec)
if fromStdin {
f, err := os.Open(sourceFile)
assert.NilError(t, err)
defer f.Close()
cmd.Stdin = f
}
} else {
cmd = base.Cmd("cp", containerStopped+":"+sourceSpec, destinationSpec)
}

if rootlessutil.IsRootless() && !nerdtest.IsDocker() {
cmd.Assert(
icmd.Expected{
ExitCode: 1,
Err: containerutil.ErrRootlessCannotCp.Error(),
})
if fromStdin && toStdout {
// Regular assert test case should work fine if src and dst are invalid
cmd.Assert(testCase.expect)
} else {
cmd.Assert(
icmd.Expected{
ExitCode: 1,
Err: containerutil.ErrRootlessCannotCp.Error(),
})
}
return
}

Expand Down
2 changes: 2 additions & 0 deletions docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,8 @@ Usage:
- `nerdctl cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH|-`
- `nerdctl cp [OPTIONS] SRC_PATH|- CONTAINER:DEST_PATH`

Using `-` as the `SRC_PATH` streams the contents of `STDIN` as a tar archive. The command extracts the content of the tar to the `DEST_PATH` in container's filesystem. In this case, `DEST_PATH` must specify a directory. Using `-` as the `DEST_PATH` streams the contents of the resource as a tar archive to `STDOUT`.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, copied this exact wording from https://docs.docker.com/reference/cli/docker/container/cp/#corner-cases. LMK if this is a problem in any way.


:warning: `nerdctl cp` is designed only for use with trusted, cooperating containers.
Using `nerdctl cp` with untrusted or malicious containers is unsupported and may not provide protection against unexpected behavior.

Expand Down
4 changes: 4 additions & 0 deletions pkg/api/types/container_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,10 @@ type ContainerCpOptions struct {
SrcPath string
// Follow symbolic links in SRC_PATH
FollowSymLink bool
// true if copying to container from tarball in stdin
FromStdin bool
// true if copying from container to stdout in tarball format
ToStdout bool
}

// ContainerStatsOptions specifies options for `nerdctl stats`.
Expand Down
Loading
Loading