package appjson import ( "bytes" "encoding/json" "fmt" "io/ioutil" "os" "strings" "github.com/dokku/dokku/plugins/common" ) type AppJson struct { Scripts struct { 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")) if err != nil { return "", fmt.Errorf("Cannot create temporary file: %v", err) } defer os.Remove(appJsonFile.Name()) common.CopyFromImage(appName, image, "app.json", appJsonFile.Name()) if !common.FileExists(appJsonFile.Name()) { return "", nil } b, err := ioutil.ReadFile(appJsonFile.Name()) if err != nil { return "", fmt.Errorf("Cannot read app.json file: %v", err) } var appJson AppJson if err = json.Unmarshal(b, &appJson); err != nil { return "", fmt.Errorf("Cannot parse app.json: %v", err) } 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 strings.TrimSpace(string(b[:])) } func executeScript(appName string, imageTag string, phase string) error { image := common.GetDeployingAppImageName(appName, imageTag, "") command := "" phaseSource := "" if phase == "release" { command = getReleaseCommand(appName, imageTag) phaseSource = "Procfile" } else { var err error phaseSource = "app.json" if command, err = getPhaseScript(appName, image, phase); err != nil { common.LogExclaim(err.Error()) } } if command == "" { return nil } common.LogInfo1(fmt.Sprintf("Executing %s command from %s: '%s'", phase, phaseSource, 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(strings.TrimSpace(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(strings.TrimSpace(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"}...); strings.TrimSpace(string(b[:])) != "" { dokkuAppShell = strings.TrimSpace(string(b[:])) } if b, _ := common.PlugnTriggerOutput("config-get", []string{appName, "DOKKU_APP_SHELL"}...); strings.TrimSpace(string(b[:])) != "" { dokkuAppShell = strings.TrimSpace(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, "--change", dockerfileEntrypoint) } dockerfileCommand, _ := common.ConfigGet(appName, "DOKKU_DOCKERFILE_CMD", "") if dockerfileCommand == "" { dockerfileCommand, _ = getCommandFromImage(image) } if dockerfileCommand != "" { commitArgs = append(commitArgs, "--change", 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 := strings.TrimSpace(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 }