From 74f27480fc6ac4751f1b2a53a3ed51ecfa2ab468 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sat, 10 Oct 2020 19:39:40 -0400 Subject: [PATCH] refactor: move all docker-related functions to new file --- plugins/common/common.go | 345 +------------------------------------- plugins/common/docker.go | 350 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 353 insertions(+), 342 deletions(-) create mode 100644 plugins/common/docker.go diff --git a/plugins/common/common.go b/plugins/common/common.go index d90d9162d..61197e621 100644 --- a/plugins/common/common.go +++ b/plugins/common/common.go @@ -1,7 +1,6 @@ package common import ( - "bytes" "errors" "fmt" "io/ioutil" @@ -127,163 +126,6 @@ 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 { - skipCleanup := false - if appName != "" { - b, _ := PlugnTriggerOutput("config-get", []string{appName, "DOKKU_SKIP_CLEANUP"}...) - output := string(b[:]) - if output == "true" { - skipCleanup = true - } - } - - if skipCleanup || os.Getenv("DOKKU_SKIP_CLEANUP") == "true" { - LogInfo1("DOKKU_SKIP_CLEANUP set. Skipping dokku cleanup") - return nil - } - } - - LogInfo1("Cleaning up...") - scheduler := GetAppScheduler(appName) - if appName == "--global" { - appName = "" - } - - forceCleanupArg := "false" - if forceCleanup { - forceCleanupArg = "true" - } - - if err := PlugnTrigger("scheduler-docker-cleanup", []string{scheduler, appName, forceCleanupArg}...); err != nil { - return fmt.Errorf("Failure while cleaning up app: %s", err) - } - - // delete all non-running and dead containers - exitedContainerIDs, _ := listContainers("exited", appName) - deadContainerIDs, _ := listContainers("dead", appName) - containerIDs := append(exitedContainerIDs, deadContainerIDs...) - - if len(containerIDs) > 0 { - removeContainers(containerIDs) - } - - // delete dangling images - imageIDs, _ := listDanglingImages(appName) - if len(imageIDs) > 0 { - RemoveImages(imageIDs) - } - - if appName != "" { - // delete unused images - pruneUnusedImages(appName) - } - - return nil -} - // GetAppScheduler fetches the scheduler for a given application func GetAppScheduler(appName string) string { if appName == "--global" { @@ -428,51 +270,6 @@ func GetRunningImageTag(appName string) (string, error) { return "", errors.New("No image tag found") } -// ContainerIsRunning checks to see if a container is running -func ContainerIsRunning(containerID string) bool { - b, err := DockerInspect(containerID, "'{{.State.Running}}'") - if err != nil { - return false - } - return strings.TrimSpace(string(b[:])) == "true" -} - -// 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 strings.TrimSpace(string(b[:])), nil -} - -// DockerInspect runs an inspect command with a given format against a container or image ID -func DockerInspect(containerOrImageID, format string) (output string, err error) { - b, err := sh.Command(DockerBin(), "inspect", "--format", format, containerOrImageID).Output() - if err != nil { - return "", err - } - output = strings.TrimSpace(string(b[:])) - if strings.HasPrefix(output, "'") && strings.HasSuffix(output, "'") { - output = strings.TrimSuffix(strings.TrimPrefix(output, "'"), "'") - } - return -} - // DokkuApps returns a list of all local apps func DokkuApps() (apps []string, err error) { dokkuRoot := MustGetEnv("DOKKU_ROOT") @@ -538,15 +335,6 @@ func IsDeployed(appName string) bool { return false } -// IsImageHerokuishBased returns true if app image is based on herokuish -func IsImageHerokuishBased(image string, appName string) bool { - output, err := DockerInspect(image, "{{range .Config.Env}}{{if eq . \"USER=herokuishuser\" }}{{println .}}{{end}}{{end}}") - if err != nil { - return false - } - return output != "" -} - // MustGetEnv returns env variable or fails if it's not set func MustGetEnv(key string) (val string) { val = os.Getenv(key) @@ -626,10 +414,10 @@ func ShiftString(a []string) (string, []string) { // StripInlineComments removes bash-style comment from input line func StripInlineComments(text string) string { - bytes := []byte(text) + b := []byte(text) re := regexp.MustCompile("(?s)#.*") - bytes = re.ReplaceAll(bytes, nil) - return strings.TrimSpace(string(bytes)) + b = re.ReplaceAll(b, nil) + return strings.TrimSpace(string(b)) } // SuppressOutput suppresses the output of a function unless there is an error @@ -697,23 +485,6 @@ func VerifyAppName(appName string) error { return nil } -// VerifyImage returns true if docker image exists in local repo -func VerifyImage(image string) bool { - imageCmd := NewShellCmd(strings.Join([]string{DockerBin(), "image", "inspect", image}, " ")) - imageCmd.ShowOutput = false - return imageCmd.Execute() -} - -// DockerBin returns a string which contains a path to the current docker binary -func DockerBin() string { - dockerBin := os.Getenv("DOCKER_BIN") - if dockerBin == "" { - dockerBin = "docker" - } - - return dockerBin -} - // PlugnTrigger fire the given plugn trigger with the given args func PlugnTrigger(triggerName string, args ...string) error { return PlugnTriggerSetup(triggerName, args...).Run() @@ -741,113 +512,3 @@ func times(str string, n int) (out string) { } return } - -func listContainers(status string, appName string) ([]string, error) { - command := []string{ - DockerBin(), - "container", - "list", - "--quiet", - "--all", - "--filter", - fmt.Sprintf("status=%v", status), - "--filter", - fmt.Sprintf("label=%v", os.Getenv("DOKKU_CONTAINER_LABEL")), - } - - if appName != "" { - command = append(command, []string{"--filter", fmt.Sprintf("label=com.dokku.app-name=%v", appName)}...) - } - - var stderr bytes.Buffer - listCmd := NewShellCmd(strings.Join(command, " ")) - listCmd.ShowOutput = false - listCmd.Command.Stderr = &stderr - b, err := listCmd.Output() - - if err != nil { - return []string{}, errors.New(strings.TrimSpace(stderr.String())) - } - - output := strings.Split(strings.TrimSpace(string(b[:])), "\n") - return output, nil -} - -func listDanglingImages(appName string) ([]string, error) { - command := []string{ - DockerBin(), - "image", - "list", - "--quiet", - "--filter", - "dangling=true", - } - - if appName != "" { - command = append(command, []string{"--filter", fmt.Sprintf("label=com.dokku.app-name=%v", appName)}...) - } - - var stderr bytes.Buffer - listCmd := NewShellCmd(strings.Join(command, " ")) - listCmd.ShowOutput = false - listCmd.Command.Stderr = &stderr - b, err := listCmd.Output() - - if err != nil { - return []string{}, errors.New(strings.TrimSpace(stderr.String())) - } - - output := strings.Split(strings.TrimSpace(string(b[:])), "\n") - return output, nil -} - -func removeContainers(containerIDs []string) { - command := []string{ - DockerBin(), - "container", - "rm", - } - - command = append(command, containerIDs...) - - var stderr bytes.Buffer - rmCmd := NewShellCmd(strings.Join(command, " ")) - rmCmd.ShowOutput = false - rmCmd.Command.Stderr = &stderr - rmCmd.Execute() -} - -// RemoveImages removes images by ID -func RemoveImages(imageIDs []string) { - command := []string{ - DockerBin(), - "image", - "rm", - } - - command = append(command, imageIDs...) - - var stderr bytes.Buffer - rmCmd := NewShellCmd(strings.Join(command, " ")) - rmCmd.ShowOutput = false - rmCmd.Command.Stderr = &stderr - rmCmd.Execute() -} - -func pruneUnusedImages(appName string) { - command := []string{ - DockerBin(), - "image", - "prune", - "--all", - "--force", - "--filter", - fmt.Sprintf("label=com.dokku.app-name=%v", appName), - } - - var stderr bytes.Buffer - pruneCmd := NewShellCmd(strings.Join(command, " ")) - pruneCmd.ShowOutput = false - pruneCmd.Command.Stderr = &stderr - pruneCmd.Execute() -} diff --git a/plugins/common/docker.go b/plugins/common/docker.go new file mode 100644 index 000000000..dff5c95c2 --- /dev/null +++ b/plugins/common/docker.go @@ -0,0 +1,350 @@ +package common + +import ( + "bytes" + "errors" + "fmt" + "io/ioutil" + "os" + "strings" + + "github.com/codeskyblue/go-sh" +) + +// ContainerIsRunning checks to see if a container is running +func ContainerIsRunning(containerID string) bool { + b, err := DockerInspect(containerID, "'{{.State.Running}}'") + if err != nil { + return false + } + return strings.TrimSpace(string(b[:])) == "true" +} + +// 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 +} + +// DockerBin returns a string which contains a path to the current docker binary +func DockerBin() string { + dockerBin := os.Getenv("DOCKER_BIN") + if dockerBin == "" { + dockerBin = "docker" + } + + return dockerBin +} + +// DockerCleanup cleans up all exited/dead containers and removes all dangling images +func DockerCleanup(appName string, forceCleanup bool) error { + if !forceCleanup { + skipCleanup := false + if appName != "" { + b, _ := PlugnTriggerOutput("config-get", []string{appName, "DOKKU_SKIP_CLEANUP"}...) + output := string(b[:]) + if output == "true" { + skipCleanup = true + } + } + + if skipCleanup || os.Getenv("DOKKU_SKIP_CLEANUP") == "true" { + LogInfo1("DOKKU_SKIP_CLEANUP set. Skipping dokku cleanup") + return nil + } + } + + LogInfo1("Cleaning up...") + scheduler := GetAppScheduler(appName) + if appName == "--global" { + appName = "" + } + + forceCleanupArg := "false" + if forceCleanup { + forceCleanupArg = "true" + } + + if err := PlugnTrigger("scheduler-docker-cleanup", []string{scheduler, appName, forceCleanupArg}...); err != nil { + return fmt.Errorf("Failure while cleaning up app: %s", err) + } + + // delete all non-running and dead containers + exitedContainerIDs, _ := listContainers("exited", appName) + deadContainerIDs, _ := listContainers("dead", appName) + containerIDs := append(exitedContainerIDs, deadContainerIDs...) + + if len(containerIDs) > 0 { + removeContainers(containerIDs) + } + + // delete dangling images + imageIDs, _ := listDanglingImages(appName) + if len(imageIDs) > 0 { + RemoveImages(imageIDs) + } + + if appName != "" { + // delete unused images + pruneUnusedImages(appName) + } + + return nil +} + +// 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 strings.TrimSpace(string(b[:])), nil +} + +// DockerInspect runs an inspect command with a given format against a container or image ID +func DockerInspect(containerOrImageID, format string) (output string, err error) { + b, err := sh.Command(DockerBin(), "inspect", "--format", format, containerOrImageID).Output() + if err != nil { + return "", err + } + output = strings.TrimSpace(string(b[:])) + if strings.HasPrefix(output, "'") && strings.HasSuffix(output, "'") { + output = strings.TrimSuffix(strings.TrimPrefix(output, "'"), "'") + } + return +} + +// IsImageHerokuishBased returns true if app image is based on herokuish +func IsImageHerokuishBased(image string, appName string) bool { + output, err := DockerInspect(image, "{{range .Config.Env}}{{if eq . \"USER=herokuishuser\" }}{{println .}}{{end}}{{end}}") + if err != nil { + return false + } + return output != "" +} + +// RemoveImages removes images by ID +func RemoveImages(imageIDs []string) { + command := []string{ + DockerBin(), + "image", + "rm", + } + + command = append(command, imageIDs...) + + var stderr bytes.Buffer + rmCmd := NewShellCmd(strings.Join(command, " ")) + rmCmd.ShowOutput = false + rmCmd.Command.Stderr = &stderr + rmCmd.Execute() +} + +// VerifyImage returns true if docker image exists in local repo +func VerifyImage(image string) bool { + imageCmd := NewShellCmd(strings.Join([]string{DockerBin(), "image", "inspect", image}, " ")) + imageCmd.ShowOutput = false + return imageCmd.Execute() +} + +func listContainers(status string, appName string) ([]string, error) { + command := []string{ + DockerBin(), + "container", + "list", + "--quiet", + "--all", + "--filter", + fmt.Sprintf("status=%v", status), + "--filter", + fmt.Sprintf("label=%v", os.Getenv("DOKKU_CONTAINER_LABEL")), + } + + if appName != "" { + command = append(command, []string{"--filter", fmt.Sprintf("label=com.dokku.app-name=%v", appName)}...) + } + + var stderr bytes.Buffer + listCmd := NewShellCmd(strings.Join(command, " ")) + listCmd.ShowOutput = false + listCmd.Command.Stderr = &stderr + b, err := listCmd.Output() + + if err != nil { + return []string{}, errors.New(strings.TrimSpace(stderr.String())) + } + + output := strings.Split(strings.TrimSpace(string(b[:])), "\n") + return output, nil +} + +func listDanglingImages(appName string) ([]string, error) { + command := []string{ + DockerBin(), + "image", + "list", + "--quiet", + "--filter", + "dangling=true", + } + + if appName != "" { + command = append(command, []string{"--filter", fmt.Sprintf("label=com.dokku.app-name=%v", appName)}...) + } + + var stderr bytes.Buffer + listCmd := NewShellCmd(strings.Join(command, " ")) + listCmd.ShowOutput = false + listCmd.Command.Stderr = &stderr + b, err := listCmd.Output() + + if err != nil { + return []string{}, errors.New(strings.TrimSpace(stderr.String())) + } + + output := strings.Split(strings.TrimSpace(string(b[:])), "\n") + return output, nil +} + +func pruneUnusedImages(appName string) { + command := []string{ + DockerBin(), + "image", + "prune", + "--all", + "--force", + "--filter", + fmt.Sprintf("label=com.dokku.app-name=%v", appName), + } + + var stderr bytes.Buffer + pruneCmd := NewShellCmd(strings.Join(command, " ")) + pruneCmd.ShowOutput = false + pruneCmd.Command.Stderr = &stderr + pruneCmd.Execute() +} + +func removeContainers(containerIDs []string) { + command := []string{ + DockerBin(), + "container", + "rm", + } + + command = append(command, containerIDs...) + + var stderr bytes.Buffer + rmCmd := NewShellCmd(strings.Join(command, " ")) + rmCmd.ShowOutput = false + rmCmd.Command.Stderr = &stderr + rmCmd.Execute() +}