diff --git a/plugins/common/common.go b/plugins/common/common.go index 9dfa5c811..c4d072c0f 100644 --- a/plugins/common/common.go +++ b/plugins/common/common.go @@ -118,6 +118,107 @@ func CommandUsage(helpHeader string, helpContent string) { fmt.Println(columnize.Format(content, config)) } +// CopyFromImage copies a file from named image to destination +func CopyFromImage(appName string, image string, source string, destination string) error { + if err := VerifyAppName(appName); err != nil { + return err + } + + if !VerifyImage(image) { + return fmt.Errorf("Invalid docker image for copying content") + } + + workDir := "" + if !IsAbsPath(source) { + if IsImageHerokuishBased(image, appName) { + workDir = "/app" + } else { + workDir, _ = DockerInspect(image, "{{.Config.WorkingDir}}") + } + + if workDir != "" { + source = fmt.Sprintf("%s/%s", workDir, source) + } + } + + tmpFile, err := ioutil.TempFile(os.TempDir(), fmt.Sprintf("dokku-%s-%s", MustGetEnv("DOKKU_PID"), "CopyFromImage")) + if err != nil { + return fmt.Errorf("Cannot create temporary file: %v", err) + } + + defer tmpFile.Close() + defer os.Remove(tmpFile.Name()) + + globalRunArgs := MustGetEnv("DOKKU_GLOBAL_RUN_ARGS") + createLabelArgs := []string{"--label", fmt.Sprintf("com.dokku.app-name=%s", appName), globalRunArgs} + containerID, err := DockerContainerCreate(image, createLabelArgs) + if err != nil { + return fmt.Errorf("Unable to create temporary container: %v", err) + } + + // docker cp exits with status 1 when run as non-root user when it tries to chown the file + // after successfully copying the file. Thus, we suppress stderr. + // ref: https://github.com/dotcloud/docker/issues/3986 + containerCopyCmd := NewShellCmd(strings.Join([]string{ + DockerBin(), + "container", + "cp", + fmt.Sprintf("%s:%s", containerID, source), + tmpFile.Name(), + }, " ")) + containerCopyCmd.ShowOutput = false + fileCopied := containerCopyCmd.Execute() + + containerRemoveCmd := NewShellCmd(strings.Join([]string{ + DockerBin(), + "container", + "rm", + "--force", + containerID, + }, " ")) + containerRemoveCmd.ShowOutput = false + containerRemoveCmd.Execute() + + if !fileCopied { + return fmt.Errorf("Unable to copy file %s from image", source) + } + + fi, err := os.Stat(tmpFile.Name()) + if err != nil { + return err + } + + if fi.Size() == 0 { + return fmt.Errorf("Unable to copy file %s from image", source) + } + + // workaround for CHECKS file when owner is root. seems to only happen when running inside docker + dos2unixCmd := NewShellCmd(strings.Join([]string{ + "dos2unix", + "-l", + "-n", + tmpFile.Name(), + destination, + }, " ")) + dos2unixCmd.ShowOutput = false + dos2unixCmd.Execute() + + // add trailing newline for certain places where file parsing depends on it + b, err := sh.Command("tail", "-c1", destination).Output() + if string(b[:]) != "" { + f, err := os.OpenFile(destination, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return err + } + defer f.Close() + if _, err := f.WriteString("\n"); err != nil { + return fmt.Errorf("Unable to append trailing newline to copied file: %v", err) + } + } + + return nil +} + // DockerCleanup cleans up all exited/dead containers and removes all dangling images func DockerCleanup(appName string, forceCleanup bool) error { if !forceCleanup { @@ -337,6 +438,29 @@ func DirectoryExists(filePath string) bool { return fi.IsDir() } +// DockerContainerCreate creates a new container and returns the container ID +func DockerContainerCreate(image string, containerCreateArgs []string) (string, error) { + cmd := []string{ + DockerBin(), + "container", + "create", + } + + cmd = append(cmd, containerCreateArgs...) + cmd = append(cmd, image) + + var stderr bytes.Buffer + containerCreateCmd := NewShellCmd(strings.Join(cmd, " ")) + containerCreateCmd.ShowOutput = false + containerCreateCmd.Command.Stderr = &stderr + b, err := containerCreateCmd.Output() + if err != nil { + return "", errors.New(strings.TrimSpace(stderr.String())) + } + + return string(b[:]), nil +} + // DockerInspect runs an inspect command with a given format against a container id func DockerInspect(containerOrImageID, format string) (output string, err error) { b, err := sh.Command(DockerBin(), "inspect", "--format", format, containerOrImageID).Output() @@ -430,6 +554,11 @@ func GetAppImageName(appName, imageTag, imageRepo string) (imageName string) { return } +// IsAbsPath returns 0 if input path is absolute +func IsAbsPath(path string) bool { + return strings.HasPrefix(path, "/") +} + // IsDeployed returns true if given app has a running container func IsDeployed(appName string) bool { files, err := ioutil.ReadDir(AppRoot(appName))