diff --git a/plugins/app-json/appjson.go b/plugins/app-json/appjson.go index b632ef67e..1806222da 100644 --- a/plugins/app-json/appjson.go +++ b/plugins/app-json/appjson.go @@ -1,20 +1,71 @@ package appjson import ( + "bytes" "encoding/json" "fmt" "io/ioutil" "os" + "strings" "github.com/dokku/dokku/plugins/common" ) type AppJson struct { Scripts struct { - Dokku string `json:"dokku"` + Dokku struct { + Predeploy string `json:"predeploy"` + Postdeploy string `json:"postdeploy"` + } `json:"dokku"` } `json:"scripts"` } +func constructScript(command string, isHerokuishImage bool) string { + script := []string{"set -eo pipefail;"} + if os.Getenv("DOKKU_TRACE") == "1" { + script = append(script, "set -x;") + } + + if isHerokuishImage { + script = append(script, []string{ + "if [[ -d '/app' ]]; then", + " export HOME=/app;", + " cd $HOME;", + "fi;", + "if [[ -d '/app/.profile.d' ]]; then", + " for file in /app/.profile.d/*; do source $file; done;", + "fi;", + + "if [[ -d '/cache' ]]; then", + " rm -rf /tmp/cache ;", + " ln -sf /cache /tmp/cache;", + "fi;", + }...) + } + + if strings.HasPrefix(command, "/") { + commandBin := strings.Split(command, " ")[0] + script = append(script, []string{ + fmt.Sprintf("if [[ ! -x \"%s\" ]]; then", commandBin), + " echo specified binary is not executable;", + " exit 1;", + "fi;", + }...) + } + + script = append(script, fmt.Sprintf("%s || exit 1;", command)) + + if isHerokuishImage { + script = append(script, []string{ + "if [[ -d '/cache' ]]; then", + " rm -f /tmp/cache;", + "fi;", + }...) + } + + return strings.Join(script, " ") +} + // getPhaseScript extracts app.json from app image and returns the appropriate json key/value func getPhaseScript(appName string, image string, phase string) (string, error) { appJsonFile, err := ioutil.TempFile(os.TempDir(), fmt.Sprintf("dokku-%s-%s", common.MustGetEnv("DOKKU_PID"), "getPhaseScript")) @@ -29,7 +80,7 @@ func getPhaseScript(appName string, image string, phase string) (string, error) return "", nil } - b, err := ioutil.ReadAll(appJsonFile) + b, err := ioutil.ReadFile(appJsonFile.Name()) if err != nil { return "", fmt.Errorf("Cannot read app.json file: %v", err) } @@ -39,9 +90,222 @@ func getPhaseScript(appName string, image string, phase string) (string, error) return "", fmt.Errorf("Cannot parse app.json: %v", err) } - return appJson.Scripts.Dokku, nil + if phase == "predeploy" { + return appJson.Scripts.Dokku.Predeploy, nil + } + + return appJson.Scripts.Dokku.Postdeploy, nil +} + +// getReleaseCommand extracts the release command from a given app's procfile +func getReleaseCommand(appName string, image string) string { + forceExtract := "true" + if err := common.PlugnTrigger("procfile-extract", []string{appName, image, forceExtract}...); err != nil { + return "" + } + + processType := "release" + port := "5000" + b, _ := common.PlugnTriggerOutput("procfile-get-command", []string{appName, processType, port}...) + return string(b[:]) } func executeScript(appName string, imageTag string, phase string) error { + image := common.GetDeployingAppImageName(appName, imageTag, "") + command := "" + if phase == "release" { + command = getReleaseCommand(appName, imageTag) + } else { + var err error + if command, err = getPhaseScript(appName, image, phase); err != nil { + common.LogExclaim(err.Error()) + } + } + + if command == "" { + return nil + } + + common.LogExclaim(fmt.Sprintf("Command declared for %s phase: '%s'", phase, command)) + isHerokuishImage := common.IsImageHerokuishBased(image, appName) + script := constructScript(command, isHerokuishImage) + + imageSourceType := "dockerfile" + if isHerokuishImage { + imageSourceType = "herokuish" + } + + cacheDir := fmt.Sprintf("%s/cache", common.AppRoot(appName)) + cacheHostDir := fmt.Sprintf("%s/cache", common.AppHostRoot(appName)) + if !common.DirectoryExists(cacheDir) { + os.MkdirAll(cacheDir, 0755) + } + + var dockerArgs []string + if b, err := common.PlugnTriggerSetup("docker-args-deploy", []string{appName, imageTag}...).SetInput("").Output(); err != nil { + dockerArgs = append(dockerArgs, strings.Split(string(b[:]), "\n")...) + } + + if b, err := common.PlugnTriggerSetup("docker-args-process-deploy", []string{appName, imageSourceType, imageTag}...).SetInput("").Output(); err != nil { + dockerArgs = append(dockerArgs, strings.Split(string(b[:]), "\n")...) + } + + filteredArgs := []string{"restart", "cpus", "memory", "memory-swap", "memory-reservation", "gpus"} + for _, filteredArg := range filteredArgs { + // re := regexp.MustCompile("--" + filteredArg + "=[0-9A-Za-z!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~]+ ") + + var filteredDockerArgs []string + for _, dockerArg := range dockerArgs { + if !strings.HasPrefix(dockerArg, "--"+filteredArg+"=") { + filteredDockerArgs = append(filteredDockerArgs, dockerArg) + } + } + + dockerArgs = filteredDockerArgs + } + + dockerArgs = append(dockerArgs, "--label=dokku_phase_script="+phase) + dockerArgs = append(dockerArgs, "-v", cacheHostDir+":/cache") + if os.Getenv("DOKKU_TRACE") != "" { + dockerArgs = append(dockerArgs, "--env", "DOKKU_TRACE="+os.Getenv("DOKKU_TRACE")) + } + + dokkuAppShell := "/bin/bash" + if b, _ := common.PlugnTriggerOutput("config-get-global", []string{"DOKKU_APP_SHELL"}...); string(b[:]) != "" { + dokkuAppShell = string(b[:]) + } + + if b, _ := common.PlugnTriggerOutput("config-get", []string{appName, "DOKKU_APP_SHELL"}...); string(b[:]) != "" { + dokkuAppShell = string(b[:]) + } + + containerCommand := []string{dokkuAppShell, "-c", script} + containerID, _ := createdContainerID(appName, dockerArgs, image, containerCommand, phase) + common.LogVerboseQuietContainerLogs(containerID) + if !waitForExecution(containerID) { + common.LogFail(fmt.Sprintf("Execution of '%s' command failed: %s", phase, command)) + } + + if phase != "predeploy" { + return nil + } + + commitArgs := []string{"container", "commit"} + if !isHerokuishImage { + dockerfileEntrypoint, _ := common.ConfigGet(appName, "DOKKU_DOCKERFILE_CMD", "") + if dockerfileEntrypoint == "" { + dockerfileEntrypoint, _ = getEntrypointFromImage(image) + } + if dockerfileEntrypoint != "" { + commitArgs = append(commitArgs, fmt.Sprintf("--change='%s'", dockerfileEntrypoint)) + } + + dockerfileCommand, _ := common.ConfigGet(appName, "DOKKU_DOCKERFILE_CMD", "") + if dockerfileCommand == "" { + dockerfileCommand, _ = getCommandFromImage(image) + } + if dockerfileCommand != "" { + commitArgs = append(commitArgs, fmt.Sprintf("--change='%s'", dockerfileCommand)) + } + } + + commitArgs = append(commitArgs, []string{ + "--change", + "LABEL org.label-schema.schema-version=1.0", + "--change", + "LABEL org.label-schema.vendor=dokku", + "--change", + fmt.Sprintf("LABEL com.dokku.app-name=%s", appName), + "--change", + fmt.Sprintf("LABEL com.dokku.%s-phase=true", phase), + }...) + commitArgs = append(commitArgs, containerID, image) + containerCommitCmd := common.NewShellCmdWithArgs( + common.DockerBin(), + commitArgs..., + ) + containerCommitCmd.ShowOutput = false + containerCommitCmd.Command.Stderr = os.Stderr + if !containerCommitCmd.Execute() { + common.LogFail(fmt.Sprintf("Commiting of '%s' to image failed: %s", phase, command)) + } return nil } + +func getEntrypointFromImage(image string) (string, error) { + entrypoint, err := common.DockerInspect(image, "{{range .Config.Entrypoint}}{{.}} {{end}}") + if err != nil { + return "", err + } + + return fmt.Sprintf("ENTRYPOINT %s", entrypoint), err +} + +func getCommandFromImage(image string) (string, error) { + command, err := common.DockerInspect(image, "{{range .Config.Cmd}}{{.}} {{end}}") + if err != nil { + return "", err + } + + return fmt.Sprintf("CMD %s", strings.Replace(command, "/bin/sh -c", "", 1)), err +} + +func waitForExecution(containerID string) bool { + containerStartCmd := common.NewShellCmdWithArgs( + common.DockerBin(), + "container", + "start", + containerID, + ) + containerStartCmd.ShowOutput = false + containerStartCmd.Command.Stderr = os.Stderr + if !containerStartCmd.Execute() { + return false + } + + containerWaitCmd := common.NewShellCmdWithArgs( + common.DockerBin(), + "container", + "wait", + containerID, + ) + + containerWaitCmd.ShowOutput = false + containerWaitCmd.Command.Stderr = os.Stderr + b, err := containerWaitCmd.Output() + if err != nil { + return false + } + + containerExitCode := string(b[:]) + return containerExitCode != "0" +} + +func createdContainerID(appName string, dockerArgs []string, image string, command []string, phase string) (string, error) { + runLabelArgs := fmt.Sprintf("--label=com.dokku.app-name=%s", appName) + + arguments := strings.Split(common.MustGetEnv("DOKKU_GLOBAL_RUN_ARGS"), " ") + arguments = append(arguments, runLabelArgs) + arguments = append(arguments, dockerArgs...) + + arguments = append([]string{"container", "create"}, arguments...) + arguments = append(arguments, image) + arguments = append(arguments, command...) + + containerCreateCmd := common.NewShellCmdWithArgs( + common.DockerBin(), + arguments..., + ) + var stderr bytes.Buffer + containerCreateCmd.ShowOutput = false + containerCreateCmd.Command.Stderr = &stderr + + b, err := containerCreateCmd.Output() + if err != nil { + return "", err + } + + containerID := strings.TrimSpace(string(b)) + err = common.PlugnTrigger("post-container-create", []string{"app", appName, containerID, phase}...) + return containerID, err +} diff --git a/plugins/common/common.go b/plugins/common/common.go index 56d11aafd..0cfb287c3 100644 --- a/plugins/common/common.go +++ b/plugins/common/common.go @@ -35,16 +35,22 @@ func NewShellCmd(command string) *ShellCmd { items := strings.Split(command, " ") cmd := items[0] args := items[1:] + return NewShellCmdWithArgs(cmd, args...) +} + +// NewShellCmd returns a new ShellCmd struct +func NewShellCmdWithArgs(cmd string, args ...string) *ShellCmd { + commandString := strings.Join(append([]string{cmd}, args...), " ") + return &ShellCmd{ Command: exec.Command(cmd, args...), - CommandString: command, + CommandString: commandString, Args: args, ShowOutput: true, } } -// Execute is a lightweight wrapper around exec.Command -func (sc *ShellCmd) Execute() bool { +func (sc *ShellCmd) Setup() { env := os.Environ() for k, v := range sc.Env { env = append(env, fmt.Sprintf("%s=%s", k, v)) @@ -54,6 +60,12 @@ func (sc *ShellCmd) Execute() bool { sc.Command.Stdout = os.Stdout sc.Command.Stderr = os.Stderr } +} + +// Execute is a lightweight wrapper around exec.Command +func (sc *ShellCmd) Execute() bool { + sc.Setup() + if err := sc.Command.Run(); err != nil { return false } @@ -62,18 +74,16 @@ func (sc *ShellCmd) Execute() bool { // 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 - } + sc.Setup() return sc.Command.Output() } +// CombinedOutput is a lightweight wrapper around exec.Command.CombinedOutput() +func (sc *ShellCmd) CombinedOutput() ([]byte, error) { + sc.Setup() + return sc.Command.CombinedOutput() +} + // AppRoot returns the app root path func AppRoot(appName string) string { dokkuRoot := MustGetEnv("DOKKU_ROOT")