diff --git a/README.md b/README.md index ec42b63fab..cb08bcf180 100644 --- a/README.md +++ b/README.md @@ -249,10 +249,12 @@ It does not necessarily mean that the corresponding features are missing in cont + - [Container management](#container-management) - [:whale: :blue_square: nerdctl run](#whale-blue_square-nerdctl-run) - [:whale: :blue_square: nerdctl exec](#whale-blue_square-nerdctl-exec) - [:whale: :blue_square: nerdctl create](#whale-blue_square-nerdctl-create) + - [:whale: nerdctl cp](#whale-nerdctl-cp) - [:whale: :blue_square: nerdctl ps](#whale-blue_square-nerdctl-ps) - [:whale: :blue_square: nerdctl inspect](#whale-blue_square-nerdctl-inspect) - [:whale: nerdctl logs](#whale-nerdctl-logs) @@ -301,7 +303,6 @@ It does not necessarily mean that the corresponding features are missing in cont - [:nerd_face: :blue_square: nerdctl namespace ls](#nerd_face-blue_square-nerdctl-namespace-ls) - [:nerd_face: :blue_square: nerdctl namespace remove](#nerd_face-blue_square-nerdctl-namespace-remove) - [:nerd_face: :blue_square: nerdctl namespace update](#nerd_face-blue_square-nerdctl-namespace-update) - - [AppArmor profile management](#apparmor-profile-management) - [:nerd_face: nerdctl apparmor inspect](#nerd_face-nerdctl-apparmor-inspect) - [:nerd_face: nerdctl apparmor load](#nerd_face-nerdctl-apparmor-load) @@ -620,6 +621,7 @@ Flags: Unimplemented `docker exec` flags: `--detach-keys` + ### :whale: :blue_square: nerdctl create Create a new container. @@ -629,6 +631,21 @@ Usage: `nerdctl create [OPTIONS] IMAGE [COMMAND] [ARG...]` The `nerdctl create` command similar to `nerdctl run -d` except the container is never started. You can then use the `nerdctl start ` command to start the container at any point. +### :whale: nerdctl cp +Copy files/folders between a running container and the local filesystem + +Usage: +- `nerdctl cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH|-` +- `nerdctl cp [OPTIONS] SRC_PATH|- CONTAINER:DEST_PATH` + +: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. + +Flags: +- :whale: `-L, --follow-link` Always follow symbol link in SRC_PATH. + +Unimplemented `docker cp` flags: `--archive` + ### :whale: :blue_square: nerdctl ps List containers. @@ -1422,7 +1439,6 @@ See [`./docs/config.md`](./docs/config.md). ## Unimplemented Docker commands Container management: - `docker attach` -- `docker cp` - `docker diff` - `docker rename` diff --git a/cmd/nerdctl/container.go b/cmd/nerdctl/container.go index 52f5d47a38..8a03444a4f 100644 --- a/cmd/nerdctl/container.go +++ b/cmd/nerdctl/container.go @@ -48,6 +48,7 @@ func newContainerCommand() *cobra.Command { newUnpauseCommand(), newCommitCommand(), ) + addCpCommand(containerCommand) return containerCommand } diff --git a/cmd/nerdctl/cp.go b/cmd/nerdctl/cp.go new file mode 100644 index 0000000000..a80135b16b --- /dev/null +++ b/cmd/nerdctl/cp.go @@ -0,0 +1,68 @@ +/* + 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 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 +} diff --git a/cmd/nerdctl/cp_linux.go b/cmd/nerdctl/cp_linux.go new file mode 100644 index 0000000000..c6fa2c89d0 --- /dev/null +++ b/cmd/nerdctl/cp_linux.go @@ -0,0 +1,273 @@ +/* + 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 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/spf13/cobra" +) + +func newCpCommand() *cobra.Command { + + shortHelp := "Copy files/folders between a running container and the local filesystem." + + longHelp := shortHelp + ` +This command requires 'tar' to be installed on the host (not in the container). +Using GNU tar is recommended. +The path of the 'tar' binary can be specified with an environment variable '$TAR'. + +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. +` + + usage := `cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH|- + nerdctl cp [OPTIONS] SRC_PATH|- CONTAINER:DEST_PATH` + var cpCommand = &cobra.Command{ + Use: usage, + Args: cobra.ExactArgs(2), + Short: shortHelp, + Long: longHelp, + RunE: cpAction, + ValidArgsFunction: cpShellComplete, + SilenceUsage: true, + SilenceErrors: true, + } + + cpCommand.Flags().BoolP("follow-link", "L", false, "Always follow symbolic link in SRC_PATH.") + + return cpCommand +} + +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 + } + + 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) +} + +// 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 + ) + if st, err := os.Stat(srcFull); err != nil { + return err + } else { + 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(int(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 +} diff --git a/cmd/nerdctl/cp_linux_test.go b/cmd/nerdctl/cp_linux_test.go new file mode 100644 index 0000000000..843f396cb2 --- /dev/null +++ b/cmd/nerdctl/cp_linux_test.go @@ -0,0 +1,207 @@ +/* + 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 main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "syscall" + "testing" + + "github.com/containerd/nerdctl/pkg/testutil" + "gotest.tools/v3/assert" +) + +func TestCopyToContainer(t *testing.T) { + t.Parallel() + base := testutil.NewBase(t) + testContainer := testutil.Identifier(t) + + base.Cmd("run", "-d", "--name", testContainer, testutil.CommonImage, "sleep", "1h").AssertOK() + defer base.Cmd("rm", "-f", testContainer).Run() + + srcUID := os.Geteuid() + srcDir := t.TempDir() + srcFile := filepath.Join(srcDir, "test-file") + srcFileContent := []byte("test-file-content") + err := os.WriteFile(srcFile, srcFileContent, 0644) + assert.NilError(t, err) + + assertCat := func(catPath string) { + t.Logf("catPath=%q", catPath) + base.Cmd("exec", testContainer, "cat", catPath).AssertOutExactly(string(srcFileContent)) + base.Cmd("exec", testContainer, "stat", "-c", "%u", catPath).AssertOutExactly(fmt.Sprintf("%d\n", srcUID)) + } + + // For the test matrix, see https://docs.docker.com/engine/reference/commandline/cp/ + t.Run("SRC_PATH specifies a file", func(t *testing.T) { + srcPath := srcFile + t.Run("DEST_PATH does not exist", func(t *testing.T) { + destPath := "/dest-no-exist-no-slash" + base.Cmd("cp", srcPath, testContainer+":"+destPath).AssertOK() + catPath := destPath + assertCat(catPath) + }) + t.Run("DEST_PATH does not exist and ends with /", func(t *testing.T) { + destPath := "/dest-no-exist-with-slash/" + base.Cmd("cp", srcPath, testContainer+":"+destPath).AssertFail() + }) + t.Run("DEST_PATH exists and is a file", func(t *testing.T) { + destPath := "/dest-file-exists" + base.Cmd("exec", testContainer, "touch", destPath).AssertOK() + base.Cmd("cp", srcPath, testContainer+":"+destPath).AssertOK() + catPath := destPath + assertCat(catPath) + }) + t.Run("DEST_PATH exists and is a directory", func(t *testing.T) { + destPath := "/dest-dir-exists" + base.Cmd("exec", testContainer, "mkdir", "-p", destPath).AssertOK() + base.Cmd("cp", srcPath, testContainer+":"+destPath).AssertOK() + catPath := filepath.Join(destPath, filepath.Base(srcFile)) + assertCat(catPath) + }) + }) + t.Run("SRC_PATH specifies a directory", func(t *testing.T) { + srcPath := srcDir + t.Run("DEST_PATH does not exist", func(t *testing.T) { + destPath := "/dest2-no-exist" + base.Cmd("cp", srcPath, testContainer+":"+destPath).AssertOK() + catPath := filepath.Join(destPath, filepath.Base(srcFile)) + assertCat(catPath) + }) + t.Run("DEST_PATH exists and is a file", func(t *testing.T) { + destPath := "/dest2-file-exists" + base.Cmd("exec", testContainer, "touch", destPath).AssertOK() + base.Cmd("cp", srcPath, testContainer+":"+destPath).AssertFail() + }) + t.Run("DEST_PATH exists and is a directory", func(t *testing.T) { + t.Run("SRC_PATH does not end with `/.`", func(t *testing.T) { + destPath := "/dest2-dir-exists" + base.Cmd("exec", testContainer, "mkdir", "-p", destPath).AssertOK() + base.Cmd("cp", srcPath, testContainer+":"+destPath).AssertOK() + catPath := filepath.Join(destPath, strings.TrimPrefix(srcFile, filepath.Dir(srcDir)+"/")) + assertCat(catPath) + }) + t.Run("SRC_PATH does end with `/.`", func(t *testing.T) { + srcPath += "/." + destPath := "/dest2-dir2-exists" + base.Cmd("exec", testContainer, "mkdir", "-p", destPath).AssertOK() + base.Cmd("cp", srcPath, testContainer+":"+destPath).AssertOK() + catPath := filepath.Join(destPath, filepath.Base(srcFile)) + t.Logf("catPath=%q", catPath) + assertCat(catPath) + }) + }) + }) +} + +func TestCopyFromContainer(t *testing.T) { + t.Parallel() + base := testutil.NewBase(t) + testContainer := testutil.Identifier(t) + + base.Cmd("run", "-d", "--name", testContainer, testutil.CommonImage, "sleep", "1h").AssertOK() + defer base.Cmd("rm", "-f", testContainer).Run() + + euid := os.Geteuid() + srcUID := 42 + srcDir := "/test-dir" + srcFile := filepath.Join(srcDir, "test-file") + srcFileContent := []byte("test-file-content") + mkSrcScript := fmt.Sprintf("mkdir -p %q && echo -n %q >%q && chown %d %q", srcDir, srcFileContent, srcFile, srcUID, srcFile) + base.Cmd("exec", testContainer, "sh", "-euc", mkSrcScript).AssertOK() + + assertCat := func(catPath string) { + t.Logf("catPath=%q", catPath) + got, err := os.ReadFile(catPath) + assert.NilError(t, err) + assert.DeepEqual(t, srcFileContent, got) + st, err := os.Stat(catPath) + assert.NilError(t, err) + stSys := st.Sys().(*syscall.Stat_t) + // stSys.Uid matches euid, not srcUID + assert.DeepEqual(t, uint32(euid), uint32(stSys.Uid)) + } + + td := t.TempDir() + // For the test matrix, see https://docs.docker.com/engine/reference/commandline/cp/ + t.Run("SRC_PATH specifies a file", func(t *testing.T) { + srcPath := srcFile + t.Run("DEST_PATH does not exist", func(t *testing.T) { + destPath := filepath.Join(td, "dest-no-exist-no-slash") + base.Cmd("cp", testContainer+":"+srcPath, destPath).AssertOK() + catPath := destPath + assertCat(catPath) + }) + t.Run("DEST_PATH does not exist and ends with /", func(t *testing.T) { + destPath := td + "/dest-no-exist-with-slash/" // Avoid filepath.Join, to forcibly append "/" + base.Cmd("cp", testContainer+":"+srcPath, destPath).AssertFail() + }) + t.Run("DEST_PATH exists and is a file", func(t *testing.T) { + destPath := filepath.Join(td, "dest-file-exists") + err := os.WriteFile(destPath, []byte(""), 0644) + assert.NilError(t, err) + base.Cmd("cp", testContainer+":"+srcPath, destPath).AssertOK() + catPath := destPath + assertCat(catPath) + }) + t.Run("DEST_PATH exists and is a directory", func(t *testing.T) { + destPath := filepath.Join(td, "dest-dir-exists") + err := os.Mkdir(destPath, 0755) + assert.NilError(t, err) + base.Cmd("cp", testContainer+":"+srcPath, destPath).AssertOK() + catPath := filepath.Join(destPath, filepath.Base(srcFile)) + assertCat(catPath) + }) + }) + t.Run("SRC_PATH specifies a directory", func(t *testing.T) { + srcPath := srcDir + t.Run("DEST_PATH does not exist", func(t *testing.T) { + destPath := filepath.Join(td, "dest2-no-exist") + base.Cmd("cp", testContainer+":"+srcPath, destPath).AssertOK() + catPath := filepath.Join(destPath, filepath.Base(srcFile)) + assertCat(catPath) + }) + t.Run("DEST_PATH exists and is a file", func(t *testing.T) { + destPath := filepath.Join(td, "dest2-file-exists") + err := os.WriteFile(destPath, []byte(""), 0644) + assert.NilError(t, err) + base.Cmd("cp", srcPath, testContainer+":"+destPath).AssertFail() + }) + t.Run("DEST_PATH exists and is a directory", func(t *testing.T) { + t.Run("SRC_PATH does not end with `/.`", func(t *testing.T) { + destPath := filepath.Join(td, "dest2-dir-exists") + err := os.Mkdir(destPath, 0755) + assert.NilError(t, err) + base.Cmd("cp", testContainer+":"+srcPath, destPath).AssertOK() + catPath := filepath.Join(destPath, strings.TrimPrefix(srcFile, filepath.Dir(srcDir)+"/")) + assertCat(catPath) + }) + t.Run("SRC_PATH does end with `/.`", func(t *testing.T) { + srcPath += "/." + destPath := filepath.Join(td, "dest2-dir2-exists") + err := os.Mkdir(destPath, 0755) + assert.NilError(t, err) + base.Cmd("cp", testContainer+":"+srcPath, destPath).AssertOK() + catPath := filepath.Join(destPath, filepath.Base(srcFile)) + assertCat(catPath) + }) + }) + }) +} diff --git a/cmd/nerdctl/main.go b/cmd/nerdctl/main.go index 923a5e0565..ecfd7b7ebb 100644 --- a/cmd/nerdctl/main.go +++ b/cmd/nerdctl/main.go @@ -292,6 +292,7 @@ Config file ($NERDCTL_TOML): %s newIPFSCommand(), ) addApparmorCommand(rootCmd) + addCpCommand(rootCmd) return rootCmd, nil } diff --git a/cmd/nerdctl/main_freebsd.go b/cmd/nerdctl/main_freebsd.go index 451a054714..30ed7a9c1e 100644 --- a/cmd/nerdctl/main_freebsd.go +++ b/cmd/nerdctl/main_freebsd.go @@ -31,3 +31,7 @@ func shellCompleteCgroupManagerNames(cmd *cobra.Command, args []string, toComple func addApparmorCommand(rootCmd *cobra.Command) { // NOP } + +func addCpCommand(rootCmd *cobra.Command) { + // NOP +} diff --git a/cmd/nerdctl/main_linux.go b/cmd/nerdctl/main_linux.go index 3283cafa08..a20cb00dbb 100644 --- a/cmd/nerdctl/main_linux.go +++ b/cmd/nerdctl/main_linux.go @@ -39,8 +39,17 @@ func appNeedsRootlessParentMain(cmd *cobra.Command, args []string) bool { switch commands[1] { // completion, login, logout: false, because it shouldn't require the daemon to be running // apparmor: false, because it requires the initial mount namespace to access /sys/kernel/security - case "", "completion", "login", "logout", "apparmor": + // cp: false, because it requires the initial mount namespace to inspect file owners + case "", "completion", "login", "logout", "apparmor", "cp": return false + case "container": + if len(commands) < 3 { + return true + } + switch commands[2] { + case "cp": + return false + } } return true } @@ -59,3 +68,7 @@ func shellCompleteCgroupManagerNames(cmd *cobra.Command, args []string, toComple func addApparmorCommand(rootCmd *cobra.Command) { rootCmd.AddCommand(newApparmorCommand()) } + +func addCpCommand(rootCmd *cobra.Command) { + rootCmd.AddCommand(newCpCommand()) +} diff --git a/cmd/nerdctl/main_windows.go b/cmd/nerdctl/main_windows.go index a39914984d..e8c48782a2 100644 --- a/cmd/nerdctl/main_windows.go +++ b/cmd/nerdctl/main_windows.go @@ -39,3 +39,7 @@ func shellCompleteCgroupManagerNames(cmd *cobra.Command, args []string, toComple func addApparmorCommand(rootCmd *cobra.Command) { // NOP } + +func addCpCommand(rootCmd *cobra.Command) { + // NOP +} diff --git a/pkg/tarutil/tarutil.go b/pkg/tarutil/tarutil.go new file mode 100644 index 0000000000..74019144d4 --- /dev/null +++ b/pkg/tarutil/tarutil.go @@ -0,0 +1,57 @@ +/* + 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 tarutil + +import ( + "fmt" + "os" + "os/exec" + "strings" + + "github.com/sirupsen/logrus" +) + +// FindTarBinary returns a path to the tar binary and whether it is GNU tar. +func FindTarBinary() (string, bool, error) { + isGNU := func(exe string) bool { + v, err := exec.Command(exe, "--version").Output() + if err != nil { + logrus.Warnf("Failed to detect whether %q is GNU tar or not", exe) + return false + } + if !strings.Contains(string(v), "GNU tar") { + logrus.Warnf("%q does not seem GNU tar", exe) + return false + } + return true + } + if v := os.Getenv("TAR"); v != "" { + if exe, err := exec.LookPath(v); err == nil { + return exe, isGNU(exe), nil + } + } + if exe, err := exec.LookPath("gnutar"); err == nil { + return exe, true, nil + } + if exe, err := exec.LookPath("gtar"); err == nil { + return exe, true, nil + } + if exe, err := exec.LookPath("tar"); err == nil { + return exe, isGNU(exe), nil + } + return "", false, fmt.Errorf("failed to find `tar` binary") +}