package common import ( "bufio" "bytes" "errors" "fmt" "io/ioutil" "os" "os/exec" "path/filepath" "reflect" "regexp" "sort" "strings" "unicode" sh "github.com/codeskyblue/go-sh" ) type errfunc func() error // ShellCmd represents a shell command to be run for dokku type ShellCmd struct { Env map[string]string Command *exec.Cmd CommandString string Args []string ShowOutput bool } // NewShellCmd returns a new ShellCmd struct func NewShellCmd(command string) *ShellCmd { items := strings.Split(command, " ") cmd := items[0] args := items[1:] return &ShellCmd{ Command: exec.Command(cmd, args...), CommandString: command, Args: args, ShowOutput: true, } } // Execute is a lightweight wrapper around exec.Command func (sc *ShellCmd) Execute() bool { env := os.Environ() for k, v := range sc.Env { env = append(env, fmt.Sprintf("%s=%s", k, v)) } sc.Command.Env = env if sc.ShowOutput { sc.Command.Stdout = os.Stdout sc.Command.Stderr = os.Stderr } if err := sc.Command.Run(); err != nil { return false } return true } // Output is a lightweight wrapper around exec.Command.Output() func (sc *ShellCmd) Output() ([]byte, error) { env := os.Environ() for k, v := range sc.Env { env = append(env, fmt.Sprintf("%s=%s", k, v)) } sc.Command.Env = env if sc.ShowOutput { sc.Command.Stdout = os.Stdout sc.Command.Stderr = os.Stderr } return sc.Command.Output() } // AppRoot returns the app root path func AppRoot(appName string) string { dokkuRoot := MustGetEnv("DOKKU_ROOT") return fmt.Sprintf("%v/%v", dokkuRoot, appName) } // AskForDestructiveConfirmation checks for confirmation on destructive actions func AskForDestructiveConfirmation(name string, objectType string) error { LogWarn("WARNING: Potentially Destructive Action") LogWarn(fmt.Sprintf("This command will destroy %v %v.", objectType, name)) LogWarn(fmt.Sprintf("To proceed, type \"%v\"", name)) fmt.Print("> ") var response string _, err := fmt.Scanln(&response) if err != nil { return err } if response != name { LogStderr("Confirmation did not match test. Aborted.") os.Exit(1) return nil } 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" { appName = "" } b, _ := PlugnTriggerOutput("config-get", []string{appName, "DOKKU_SCHEDULER"}...) value := string(b[:]) if value != "" { return value } b, _ = PlugnTriggerOutput("config-get-global", []string{"DOKKU_SCHEDULER"}...) value = string(b[:]) if value != "" { return value } return "docker-local" } // GetDeployingAppImageName returns deploying image identifier for a given app, tag tuple. validate if tag is presented func GetDeployingAppImageName(appName, imageTag, imageRepo string) (imageName string) { if appName == "" { LogFail("(GetDeployingAppImageName) APP must not be empty") } b, err := PlugnTriggerOutput("deployed-app-repository", []string{appName}...) if err != nil { LogFail(err.Error()) } imageRemoteRepository := string(b[:]) b, err = PlugnTriggerOutput("deployed-app-image-tag", []string{appName}...) if err != nil { LogFail(err.Error()) } newImageTag := string(b[:]) b, err = PlugnTriggerOutput("deployed-app-image-repo", []string{appName}...) if err != nil { LogFail(err.Error()) } newImageRepo := string(b[:]) if newImageRepo != "" { imageRepo = newImageRepo } if newImageTag != "" { imageTag = newImageTag } if imageRepo == "" { imageRepo = GetAppImageRepo(appName) } if imageTag == "" { imageTag = "latest" } imageName = fmt.Sprintf("%s%s:%s", imageRemoteRepository, imageRepo, imageTag) if !VerifyImage(imageName) { LogFail(fmt.Sprintf("App image (%s) not found", imageName)) } return } // GetAppImageRepo is the central definition of a dokku image repo pattern func GetAppImageRepo(appName string) string { return strings.Join([]string{"dokku", appName}, "/") } // GetAppContainerIDs returns a list of docker container ids for given app and optional container_type func GetAppContainerIDs(appName string, containerType string) ([]string, error) { var containerIDs []string if err := VerifyAppName(appName); err != nil { return containerIDs, err } appRoot := AppRoot(appName) containerFilePath := fmt.Sprintf("%v/CONTAINER", appRoot) _, err := os.Stat(containerFilePath) if !os.IsNotExist(err) { containerIDs = append(containerIDs, ReadFirstLine(containerFilePath)) } containerPattern := fmt.Sprintf("%v/CONTAINER.*", appRoot) if containerType != "" { containerPattern = fmt.Sprintf("%v/CONTAINER.%v.*", appRoot, containerType) if strings.Contains(".", containerType) { containerPattern = fmt.Sprintf("%v/CONTAINER.%v", appRoot, containerType) } } files, _ := filepath.Glob(containerPattern) for _, containerFile := range files { containerIDs = append(containerIDs, ReadFirstLine(containerFile)) } return containerIDs, nil } // GetAppRunningContainerIDs return a list of running docker container ids for given app and optional container_type func GetAppRunningContainerIDs(appName string, containerType string) ([]string, error) { var runningContainerIDs []string if err := VerifyAppName(appName); err != nil { return runningContainerIDs, err } if !IsDeployed(appName) { LogFail(fmt.Sprintf("App %v has not been deployed", appName)) } containerIDs, err := GetAppContainerIDs(appName, containerType) if err != nil { return runningContainerIDs, nil } for _, containerID := range containerIDs { if ContainerIsRunning(containerID) { runningContainerIDs = append(runningContainerIDs, containerID) } } return runningContainerIDs, nil } // GetRunningImageTag retrieves current image tag for a given app and returns empty string if no deployed containers are found func GetRunningImageTag(appName string) (string, error) { if err := VerifyAppName(appName); err != nil { return "", err } containerIDs, err := GetAppContainerIDs(appName, "") if err != nil { return "", err } for _, containerID := range containerIDs { if image, err := DockerInspect(containerID, "{{ .Config.Image }}"); err == nil { return strings.Split(image, ":")[1], nil } } 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" } // DirectoryExists returns if a path exists and is a directory func DirectoryExists(filePath string) bool { fi, err := os.Stat(filePath) if err != nil { return false } return fi.IsDir() } // 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() 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") files, err := ioutil.ReadDir(dokkuRoot) if err != nil { err = fmt.Errorf("You haven't deployed any applications yet") return } for _, f := range files { appRoot := AppRoot(f.Name()) if !DirectoryExists(appRoot) { continue } if f.Name() == "tls" || strings.HasPrefix(f.Name(), ".") { continue } apps = append(apps, f.Name()) } if len(apps) == 0 { err = fmt.Errorf("You haven't deployed any applications yet") return } return } // FileToSlice reads in all the lines from a file into a string slice func FileToSlice(filePath string) (lines []string, err error) { f, err := os.Open(filePath) if err != nil { return } defer f.Close() scanner := bufio.NewScanner(f) for scanner.Scan() { text := strings.TrimSpace(scanner.Text()) if text == "" { continue } lines = append(lines, text) } err = scanner.Err() return } // FileExists returns if a path exists and is a file func FileExists(filePath string) bool { fi, err := os.Stat(filePath) if err != nil { return false } return fi.Mode().IsRegular() } // GetAppImageName returns image identifier for a given app, tag tuple. validate if tag is presented func GetAppImageName(appName, imageTag, imageRepo string) (imageName string) { err := VerifyAppName(appName) if err != nil { LogFail(err.Error()) } if imageRepo == "" { imageRepo = GetAppImageRepo(appName) } if imageTag == "" { imageName = fmt.Sprintf("%v:latest", imageRepo) } else { imageName = fmt.Sprintf("%v:%v", imageRepo, imageTag) if !VerifyImage(imageName) { LogFail(fmt.Sprintf("App image (%s) not found", imageName)) } } return } // IsDeployed returns true if given app has a running container func IsDeployed(appName string) bool { files, err := ioutil.ReadDir(AppRoot(appName)) if err != nil { return false } for _, f := range files { if f.Name() == "CONTAINER" || strings.HasPrefix(f.Name(), "CONTAINER.") { return true } } 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) if val == "" { LogFail(fmt.Sprintf("%s not set!", key)) } return } // GetenvWithDefault returns env variable or defaultValue if it's not set func GetenvWithDefault(key string, defaultValue string) (val string) { val = os.Getenv(key) if val == "" { val = defaultValue } return } // ReadFirstLine gets the first line of a file that has contents and returns it // if there are no contents, an empty string is returned // will also return an empty string if the file does not exist func ReadFirstLine(filename string) (text string) { if !FileExists(filename) { return } f, err := os.Open(filename) if err != nil { return } defer f.Close() scanner := bufio.NewScanner(f) for scanner.Scan() { if text = strings.TrimSpace(scanner.Text()); text == "" { continue } return } return } // ReportSingleApp is an internal function that displays a report for an app func ReportSingleApp(reportType string, appName string, infoFlag string, infoFlags map[string]string, trimPrefix bool, uppercaseFirstCharacter bool) error { flags := []string{} for key := range infoFlags { flags = append(flags, key) } sort.Strings(flags) if len(infoFlag) == 0 { LogInfo2Quiet(fmt.Sprintf("%s %v information", appName, reportType)) for _, k := range flags { v := infoFlags[k] prefix := "--" if trimPrefix { prefix = fmt.Sprintf("--%v-", reportType) } key := strings.Replace(strings.Replace(strings.TrimPrefix(k, prefix), "-", " ", -1), ".", " ", -1) if uppercaseFirstCharacter { key = UcFirst(key) } LogVerbose(fmt.Sprintf("%s%s", RightPad(fmt.Sprintf("%s:", key), 31, " "), v)) } return nil } for _, k := range flags { if infoFlag == k { v := infoFlags[k] fmt.Println(v) return nil } } keys := reflect.ValueOf(infoFlags).MapKeys() strkeys := make([]string, len(keys)) for i := 0; i < len(keys); i++ { strkeys[i] = keys[i].String() } return fmt.Errorf("Invalid flag passed, valid flags: %s", strings.Join(strkeys, ", ")) } // RightPad right-pads the string with pad up to len runes func RightPad(str string, length int, pad string) string { return str + times(pad, length-len(str)) } // StripInlineComments removes bash-style comment from input line func StripInlineComments(text string) string { bytes := []byte(text) re := regexp.MustCompile("(?s)#.*") bytes = re.ReplaceAll(bytes, nil) return strings.TrimSpace(string(bytes)) } // SuppressOutput suppresses the output of a function unless there is an error func SuppressOutput(f errfunc) error { rescueStdout := os.Stdout r, w, _ := os.Pipe() os.Stdout = w err := f() w.Close() out, _ := ioutil.ReadAll(r) os.Stdout = rescueStdout if err != nil { fmt.Printf(string(out[:])) } return err } // ToBool returns a bool value for a given string func ToBool(s string) bool { return s == "true" } // UcFirst uppercases the first character in a string func UcFirst(str string) string { for i, v := range str { return string(unicode.ToUpper(v)) + str[i+1:] } return "" } // IsValidAppName verifies app name format func IsValidAppName(appName string) error { if appName == "" { return fmt.Errorf("APP must not be null") } r, _ := regexp.Compile("^[a-z0-9].*") if r.MatchString(appName) { return nil } appRoot := AppRoot(appName) if DirectoryExists(appRoot) { os.RemoveAll(appRoot) } return errors.New("App name must begin with lowercase alphanumeric character") } // VerifyAppName verifies app name format and app existence" func VerifyAppName(appName string) error { if err := IsValidAppName(appName); err != nil { return err } appRoot := AppRoot(appName) if !DirectoryExists(appRoot) { return fmt.Errorf("App %s does not exist", appName) } 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() } // PlugnTriggerOutput fire the given plugn trigger with the given args func PlugnTriggerOutput(triggerName string, args ...string) ([]byte, error) { return PlugnTriggerSetup(triggerName, args...).Output() } // PlugnTriggerSetup sets up a plugn trigger call func PlugnTriggerSetup(triggerName string, args ...string) *sh.Session { shellArgs := make([]interface{}, len(args)+2) shellArgs[0] = "trigger" shellArgs[1] = triggerName for i, arg := range args { shellArgs[i+2] = arg } return sh.Command("plugn", shellArgs...) } func times(str string, n int) (out string) { for i := 0; i < n; i++ { out += str } 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() }