75f6929b44
[pkg/archive] Update archive/copy path handling - Remove unused TarOptions.Name field. - Add new TarOptions.RebaseNames field. - Update some of the logic around path dir/base splitting. - Update some of the logic behind archive entry name rebasing. [api/types] Add LinkTarget field to PathStat [daemon] Fix stat, archive, extract of symlinks These operations *should* resolve symlinks that are in the path but if the resource itself is a symlink then it *should not* be resolved. This patch puts this logic into a common function `resolvePath` which resolves symlinks of the path's dir in scope of the container rootfs but does not resolve the final element of the path. Now archive, extract, and stat operations will return symlinks if the path is indeed a symlink. [api/client] Update cp path hanling [docs/reference/api] Update description of stat Add the linkTarget field to the header of the archive endpoint. Remove path field. [integration-cli] Fix/Add cp symlink test cases Copying a symlink should do just that: copy the symlink NOT copy the target of the symlink. Also, the resulting file from the copy should have the name of the symlink NOT the name of the target file. Copying to a symlink should copy to the symlink target and not modify the symlink itself. Docker-DCO-1.1-Signed-off-by: Josh Hawn <josh.hawn@docker.com> (github: jlhawn)
316 lines
7.9 KiB
Go
316 lines
7.9 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/docker/docker/pkg/archive"
|
|
"github.com/go-check/check"
|
|
)
|
|
|
|
type fileType uint32
|
|
|
|
const (
|
|
ftRegular fileType = iota
|
|
ftDir
|
|
ftSymlink
|
|
)
|
|
|
|
type fileData struct {
|
|
filetype fileType
|
|
path string
|
|
contents string
|
|
}
|
|
|
|
func (fd fileData) creationCommand() string {
|
|
var command string
|
|
|
|
switch fd.filetype {
|
|
case ftRegular:
|
|
// Don't overwrite the file if it already exists!
|
|
command = fmt.Sprintf("if [ ! -f %s ]; then echo %q > %s; fi", fd.path, fd.contents, fd.path)
|
|
case ftDir:
|
|
command = fmt.Sprintf("mkdir -p %s", fd.path)
|
|
case ftSymlink:
|
|
command = fmt.Sprintf("ln -fs %s %s", fd.contents, fd.path)
|
|
}
|
|
|
|
return command
|
|
}
|
|
|
|
func mkFilesCommand(fds []fileData) string {
|
|
commands := make([]string, len(fds))
|
|
|
|
for i, fd := range fds {
|
|
commands[i] = fd.creationCommand()
|
|
}
|
|
|
|
return strings.Join(commands, " && ")
|
|
}
|
|
|
|
var defaultFileData = []fileData{
|
|
{ftRegular, "file1", "file1"},
|
|
{ftRegular, "file2", "file2"},
|
|
{ftRegular, "file3", "file3"},
|
|
{ftRegular, "file4", "file4"},
|
|
{ftRegular, "file5", "file5"},
|
|
{ftRegular, "file6", "file6"},
|
|
{ftRegular, "file7", "file7"},
|
|
{ftDir, "dir1", ""},
|
|
{ftRegular, "dir1/file1-1", "file1-1"},
|
|
{ftRegular, "dir1/file1-2", "file1-2"},
|
|
{ftDir, "dir2", ""},
|
|
{ftRegular, "dir2/file2-1", "file2-1"},
|
|
{ftRegular, "dir2/file2-2", "file2-2"},
|
|
{ftDir, "dir3", ""},
|
|
{ftRegular, "dir3/file3-1", "file3-1"},
|
|
{ftRegular, "dir3/file3-2", "file3-2"},
|
|
{ftDir, "dir4", ""},
|
|
{ftRegular, "dir4/file3-1", "file4-1"},
|
|
{ftRegular, "dir4/file3-2", "file4-2"},
|
|
{ftDir, "dir5", ""},
|
|
{ftSymlink, "symlinkToFile1", "file1"},
|
|
{ftSymlink, "symlinkToDir1", "dir1"},
|
|
{ftSymlink, "brokenSymlinkToFileX", "fileX"},
|
|
{ftSymlink, "brokenSymlinkToDirX", "dirX"},
|
|
{ftSymlink, "symlinkToAbsDir", "/root"},
|
|
}
|
|
|
|
func defaultMkContentCommand() string {
|
|
return mkFilesCommand(defaultFileData)
|
|
}
|
|
|
|
func makeTestContentInDir(c *check.C, dir string) {
|
|
for _, fd := range defaultFileData {
|
|
path := filepath.Join(dir, filepath.FromSlash(fd.path))
|
|
switch fd.filetype {
|
|
case ftRegular:
|
|
if err := ioutil.WriteFile(path, []byte(fd.contents+"\n"), os.FileMode(0666)); err != nil {
|
|
c.Fatal(err)
|
|
}
|
|
case ftDir:
|
|
if err := os.Mkdir(path, os.FileMode(0777)); err != nil {
|
|
c.Fatal(err)
|
|
}
|
|
case ftSymlink:
|
|
if err := os.Symlink(fd.contents, path); err != nil {
|
|
c.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
type testContainerOptions struct {
|
|
addContent bool
|
|
readOnly bool
|
|
volumes []string
|
|
workDir string
|
|
command string
|
|
}
|
|
|
|
func makeTestContainer(c *check.C, options testContainerOptions) (containerID string) {
|
|
if options.addContent {
|
|
mkContentCmd := defaultMkContentCommand()
|
|
if options.command == "" {
|
|
options.command = mkContentCmd
|
|
} else {
|
|
options.command = fmt.Sprintf("%s && %s", defaultMkContentCommand(), options.command)
|
|
}
|
|
}
|
|
|
|
if options.command == "" {
|
|
options.command = "#(nop)"
|
|
}
|
|
|
|
args := []string{"run", "-d"}
|
|
|
|
for _, volume := range options.volumes {
|
|
args = append(args, "-v", volume)
|
|
}
|
|
|
|
if options.workDir != "" {
|
|
args = append(args, "-w", options.workDir)
|
|
}
|
|
|
|
if options.readOnly {
|
|
args = append(args, "--read-only")
|
|
}
|
|
|
|
args = append(args, "busybox", "/bin/sh", "-c", options.command)
|
|
|
|
out, status := dockerCmd(c, args...)
|
|
if status != 0 {
|
|
c.Fatalf("failed to run container, status %d: %s", status, out)
|
|
}
|
|
|
|
containerID = strings.TrimSpace(out)
|
|
|
|
out, status = dockerCmd(c, "wait", containerID)
|
|
if status != 0 {
|
|
c.Fatalf("failed to wait for test container container, status %d: %s", status, out)
|
|
}
|
|
|
|
if exitCode := strings.TrimSpace(out); exitCode != "0" {
|
|
logs, status := dockerCmd(c, "logs", containerID)
|
|
if status != 0 {
|
|
logs = "UNABLE TO GET LOGS"
|
|
}
|
|
c.Fatalf("failed to make test container, exit code (%s): %s", exitCode, logs)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func makeCatFileCommand(path string) string {
|
|
return fmt.Sprintf("if [ -f %s ]; then cat %s; fi", path, path)
|
|
}
|
|
|
|
func cpPath(pathElements ...string) string {
|
|
localizedPathElements := make([]string, len(pathElements))
|
|
for i, path := range pathElements {
|
|
localizedPathElements[i] = filepath.FromSlash(path)
|
|
}
|
|
return strings.Join(localizedPathElements, string(filepath.Separator))
|
|
}
|
|
|
|
func cpPathTrailingSep(pathElements ...string) string {
|
|
return fmt.Sprintf("%s%c", cpPath(pathElements...), filepath.Separator)
|
|
}
|
|
|
|
func containerCpPath(containerID string, pathElements ...string) string {
|
|
joined := strings.Join(pathElements, "/")
|
|
return fmt.Sprintf("%s:%s", containerID, joined)
|
|
}
|
|
|
|
func containerCpPathTrailingSep(containerID string, pathElements ...string) string {
|
|
return fmt.Sprintf("%s/", containerCpPath(containerID, pathElements...))
|
|
}
|
|
|
|
func runDockerCp(c *check.C, src, dst string) (err error) {
|
|
c.Logf("running `docker cp %s %s`", src, dst)
|
|
|
|
args := []string{"cp", src, dst}
|
|
|
|
out, _, err := runCommandWithOutput(exec.Command(dockerBinary, args...))
|
|
if err != nil {
|
|
err = fmt.Errorf("error executing `docker cp` command: %s: %s", err, out)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func startContainerGetOutput(c *check.C, cID string) (out string, err error) {
|
|
c.Logf("running `docker start -a %s`", cID)
|
|
|
|
args := []string{"start", "-a", cID}
|
|
|
|
out, _, err = runCommandWithOutput(exec.Command(dockerBinary, args...))
|
|
if err != nil {
|
|
err = fmt.Errorf("error executing `docker start` command: %s: %s", err, out)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func getTestDir(c *check.C, label string) (tmpDir string) {
|
|
var err error
|
|
|
|
if tmpDir, err = ioutil.TempDir("", label); err != nil {
|
|
c.Fatalf("unable to make temporary directory: %s", err)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func isCpNotExist(err error) bool {
|
|
return strings.Contains(err.Error(), "no such file or directory") || strings.Contains(err.Error(), "cannot find the file specified")
|
|
}
|
|
|
|
func isCpDirNotExist(err error) bool {
|
|
return strings.Contains(err.Error(), archive.ErrDirNotExists.Error())
|
|
}
|
|
|
|
func isCpNotDir(err error) bool {
|
|
return strings.Contains(err.Error(), archive.ErrNotDirectory.Error()) || strings.Contains(err.Error(), "filename, directory name, or volume label syntax is incorrect")
|
|
}
|
|
|
|
func isCpCannotCopyDir(err error) bool {
|
|
return strings.Contains(err.Error(), archive.ErrCannotCopyDir.Error())
|
|
}
|
|
|
|
func isCpCannotCopyReadOnly(err error) bool {
|
|
return strings.Contains(err.Error(), "marked read-only")
|
|
}
|
|
|
|
func isCannotOverwriteNonDirWithDir(err error) bool {
|
|
return strings.Contains(err.Error(), "cannot overwrite non-directory")
|
|
}
|
|
|
|
func fileContentEquals(c *check.C, filename, contents string) (err error) {
|
|
c.Logf("checking that file %q contains %q\n", filename, contents)
|
|
|
|
fileBytes, err := ioutil.ReadFile(filename)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
expectedBytes, err := ioutil.ReadAll(strings.NewReader(contents))
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if !bytes.Equal(fileBytes, expectedBytes) {
|
|
err = fmt.Errorf("file content not equal - expected %q, got %q", string(expectedBytes), string(fileBytes))
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func symlinkTargetEquals(c *check.C, symlink, expectedTarget string) (err error) {
|
|
c.Logf("checking that the symlink %q points to %q\n", symlink, expectedTarget)
|
|
|
|
actualTarget, err := os.Readlink(symlink)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if actualTarget != expectedTarget {
|
|
return fmt.Errorf("symlink target points to %q not %q", actualTarget, expectedTarget)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func containerStartOutputEquals(c *check.C, cID, contents string) (err error) {
|
|
c.Logf("checking that container %q start output contains %q\n", cID, contents)
|
|
|
|
out, err := startContainerGetOutput(c, cID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if out != contents {
|
|
err = fmt.Errorf("output contents not equal - expected %q, got %q", contents, out)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func defaultVolumes(tmpDir string) []string {
|
|
if SameHostDaemon.Condition() {
|
|
return []string{
|
|
"/vol1",
|
|
fmt.Sprintf("%s:/vol2", tmpDir),
|
|
fmt.Sprintf("%s:/vol3", filepath.Join(tmpDir, "vol3")),
|
|
fmt.Sprintf("%s:/vol_ro:ro", filepath.Join(tmpDir, "vol_ro")),
|
|
}
|
|
}
|
|
|
|
// Can't bind-mount volumes with separate host daemon.
|
|
return []string{"/vol1", "/vol2", "/vol3", "/vol_ro:/vol_ro:ro"}
|
|
}
|