diff --git a/docs/advanced-usage/deployment-tasks.md b/docs/advanced-usage/deployment-tasks.md index 5536689cb..44de27596 100644 --- a/docs/advanced-usage/deployment-tasks.md +++ b/docs/advanced-usage/deployment-tasks.md @@ -32,6 +32,8 @@ Each "phase" has different expectations and limitations: - Priming or invalidating cache stores - Running database migrations +Additionally, if using a Dockerfile with an `ENTRYPOINT`, the deployment task is passed to that entrypoint as is. + Please keep the above in mind when utilizing deployment tasks. > To execute commands on the host during a release phase, see the [plugin creation documentation](/docs/development/plugin-creation.md) docs for more information on building your own custom plugin. diff --git a/docs/configuration/environment-variables.md b/docs/configuration/environment-variables.md index ebe7f511c..839883ee6 100644 --- a/docs/configuration/environment-variables.md +++ b/docs/configuration/environment-variables.md @@ -110,9 +110,7 @@ The following config variables have special meanings and can be set in a variety | `DOKKU_DISABLE_ANSI_PREFIX_REMOVAL` | none | `dokku config:set`
`/etc/environment`
`~dokku/.dokkurc`
`~dokku/.dokkurc/*` | Disables removal of the ANSI prefix during deploys. Can be used in cases where the client deployer does not understand ansi escape codes. | | `DOKKU_DISABLE_APP_AUTOCREATION` | none | `dokku config:set` | Disables automatic creation of a non-existent app on deploy. | | `DOKKU_DOCKER_STOP_TIMEOUT` | `10` | `dokku config:set` | Configurable grace period given to the `docker stop` command. If a container has not stopped by this time, a `kill -9` signal or equivalent is sent in order to force-terminate the container. Both the `ps:stop` and `apps:destroy` commands _also_ respect this value. If not specified, the docker defaults for the [docker stop command](https://docs.docker.com/engine/reference/commandline/stop/) will be used.| -| `DOKKU_DOCKERFILE_CMD` | dockerfile cmd | `dokku config:set` | | | `DOKKU_DOCKERFILE_CACHE_BUILD` | none | `dokku config:set` | | -| `DOKKU_DOCKERFILE_ENTRYPOINT` | dockerfile entrypoint | `dokku config:set` | | | `DOKKU_DOCKERFILE_PORTS` | dockerfile ports | `dokku config:set` | | | `DOKKU_DOCKERFILE_START_CMD` | none | `dokku config:set` | | | `DOKKU_PARALLEL_ARGUMENTS`. | none | `dokku config:set` | Allows passing custom arguments to parallel for `ps:*all` commands | diff --git a/docs/deployment/methods/buildpacks.md b/docs/deployment/methods/buildpacks.md index 728dd0d38..5a64f58e9 100644 --- a/docs/deployment/methods/buildpacks.md +++ b/docs/deployment/methods/buildpacks.md @@ -164,7 +164,7 @@ dokku buildpacks:report node-js-app --buildpacks-list If an application was previously deployed via Dockerfile, the following commands should be run before a buildpack deploy will succeed: ```shell -dokku config:unset --no-restart node-js-app DOKKU_DOCKERFILE_CMD DOKKU_DOCKERFILE_ENTRYPOINT DOKKU_PROXY_PORT_MAP +dokku config:unset --no-restart node-js-app DOKKU_PROXY_PORT_MAP ``` ### Using a specific buildpack version diff --git a/docs/development/plugin-triggers.md b/docs/development/plugin-triggers.md index 05dd41e2b..660b0696e 100644 --- a/docs/development/plugin-triggers.md +++ b/docs/development/plugin-triggers.md @@ -1447,6 +1447,51 @@ APP="$1"; curl "https://dokku.me/starting/${APP}" || true ``` +### `procfile-extract` + +- Description: Extracts a Procfile from a given image to a path +- Invoked by: `internally` +- Arguments: `$APP $IMAGE $FORCE_EXTRACT $PROCFILE_PATH` +- Example: + +```shell +#!/usr/bin/env bash + +set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x + +# TODO +``` + +### `procfile-get-command` + +- Description: Fetches the command for a specific process type +- Invoked by: `internally` +- Arguments: `$APP $PROCESS_TYPE $PORT $PROCFILE_PATH` +- Example: + +```shell +#!/usr/bin/env bash + +set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x + +# TODO +``` + +### `procfile-remove` + +- Description: Removes the extracted Procfile +- Invoked by: `internally` +- Arguments: `$APP $PROCFILE_PATH` +- Example: + +```shell +#!/usr/bin/env bash + +set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x + +# TODO +``` + ### `proxy-build-config` - Description: Builds the proxy implementation configuration for a given app diff --git a/plugins/20_events/procfile-extract b/plugins/20_events/procfile-extract new file mode 120000 index 000000000..5178a749f --- /dev/null +++ b/plugins/20_events/procfile-extract @@ -0,0 +1 @@ +hook \ No newline at end of file diff --git a/plugins/20_events/procfile-get-command b/plugins/20_events/procfile-get-command new file mode 120000 index 000000000..5178a749f --- /dev/null +++ b/plugins/20_events/procfile-get-command @@ -0,0 +1 @@ +hook \ No newline at end of file diff --git a/plugins/20_events/procfile-remove b/plugins/20_events/procfile-remove new file mode 120000 index 000000000..5178a749f --- /dev/null +++ b/plugins/20_events/procfile-remove @@ -0,0 +1 @@ +hook \ No newline at end of file diff --git a/plugins/20_events/profile-get-command b/plugins/20_events/profile-get-command new file mode 120000 index 000000000..5178a749f --- /dev/null +++ b/plugins/20_events/profile-get-command @@ -0,0 +1 @@ +hook \ No newline at end of file diff --git a/plugins/app-json/.gitignore b/plugins/app-json/.gitignore new file mode 100644 index 000000000..d04745dda --- /dev/null +++ b/plugins/app-json/.gitignore @@ -0,0 +1,4 @@ +/triggers/* +/triggers +/pre-* +/post-* diff --git a/plugins/app-json/Makefile b/plugins/app-json/Makefile new file mode 100644 index 000000000..1640a6195 --- /dev/null +++ b/plugins/app-json/Makefile @@ -0,0 +1,5 @@ +TRIGGERS = triggers/post-deploy triggers/pre-deploy +BUILD = triggers +PLUGIN_NAME = app-json + +include ../../common.mk diff --git a/plugins/app-json/appjson.go b/plugins/app-json/appjson.go new file mode 100644 index 000000000..05057cffa --- /dev/null +++ b/plugins/app-json/appjson.go @@ -0,0 +1,380 @@ +package appjson + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "os" + "strings" + + "github.com/dokku/dokku/plugins/common" + shellquote "github.com/kballard/go-shellquote" +) + +// AppJSON is a struct that represents an app.json file as understood by Dokku +type AppJSON struct { + Scripts struct { + Dokku struct { + Predeploy string `json:"predeploy"` + Postdeploy string `json:"postdeploy"` + } `json:"dokku"` + } `json:"scripts"` +} + +func constructScript(command string, shell string, isHerokuishImage bool, hasEntrypoint bool) []string { + if hasEntrypoint { + return []string{command} + } + + 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 []string{shell, "-c", 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) + } + + if strings.TrimSpace(string(b)) == "" { + return "", nil + } + + 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" + + err := common.SuppressOutput(func() error { + return common.PlugnTrigger("procfile-extract", []string{appName, image, forceExtract}...) + }) + + if err != nil { + return "" + } + + processType := "release" + port := "5000" + b, _ := common.PlugnTriggerOutput("procfile-get-command", []string{appName, processType, port}...) + return strings.TrimSpace(string(b[:])) +} + +func getDokkuAppShell(appName string) string { + 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[:])) + } + + return dokkuAppShell +} + +func executeScript(appName string, imageTag string, phase string) error { + image := common.GetDeployingAppImageName(appName, imageTag, "") + command := "" + phaseSource := "" + if phase == "release" { + command = getReleaseCommand(appName, image) + 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 task from %s: %s", phase, phaseSource, command)) + isHerokuishImage := common.IsImageHerokuishBased(image, appName) + dockerfileEntrypoint := "" + dockerfileCommand := "" + if !isHerokuishImage { + dockerfileEntrypoint, _ = getEntrypointFromImage(image) + dockerfileCommand, _ = getCommandFromImage(image) + } + + hasEntrypoint := dockerfileEntrypoint != "" + dokkuAppShell := getDokkuAppShell(appName) + script := constructScript(command, dokkuAppShell, isHerokuishImage, hasEntrypoint) + + 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 { + words, err := shellquote.Split(strings.TrimSpace(string(b[:]))) + if err != nil { + return err + } + + dockerArgs = append(dockerArgs, words...) + } + + if b, err := common.PlugnTriggerSetup("docker-args-process-deploy", []string{appName, imageSourceType, imageTag}...).SetInput("").Output(); err == nil { + words, err := shellquote.Split(strings.TrimSpace(string(b[:]))) + if err != nil { + return err + } + + dockerArgs = append(dockerArgs, words...) + } + + 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")) + } + + containerID, err := createdContainerID(appName, dockerArgs, image, script, phase) + if err != nil { + common.LogFail(fmt.Sprintf("Failed to create %s execution container: %s", phase, err.Error())) + } + + if !waitForExecution(containerID) { + common.LogInfo2Quiet(fmt.Sprintf("Start of %s %s task (%s) output", appName, phase, containerID[0:9])) + common.LogVerboseQuietContainerLogs(containerID) + common.LogInfo2Quiet(fmt.Sprintf("End of %s %s task (%s) output", appName, phase, containerID[0:9])) + common.LogFail(fmt.Sprintf("Execution of %s task failed: %s", phase, command)) + } + + common.LogInfo2Quiet(fmt.Sprintf("Start of %s %s task (%s) output", appName, phase, containerID[0:9])) + common.LogVerboseQuietContainerLogs(containerID) + common.LogInfo2Quiet(fmt.Sprintf("End of %s %s task (%s) output", appName, phase, containerID[0:9])) + + if phase != "predeploy" { + return nil + } + + commitArgs := []string{"container", "commit"} + if !isHerokuishImage { + if dockerfileEntrypoint != "" { + commitArgs = append(commitArgs, "--change", dockerfileEntrypoint) + } + + 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) { + output, err := common.DockerInspect(image, "{{json .Config.Entrypoint}}") + if err != nil { + return "", err + } + if output == "null" { + return "", err + } + + var entrypoint []string + if err = json.Unmarshal([]byte(output), &entrypoint); err != nil { + return "", err + } + + if len(entrypoint) == 3 && entrypoint[0] == "/bin/sh" && entrypoint[1] == "-c" { + return fmt.Sprintf("ENTRYPOINT %s", entrypoint[2]), nil + } + + serializedEntrypoint, err := json.Marshal(entrypoint) + return fmt.Sprintf("ENTRYPOINT %s", string(serializedEntrypoint)), err +} + +func getCommandFromImage(image string) (string, error) { + output, err := common.DockerInspect(image, "{{json .Config.Cmd}}") + if err != nil { + return "", err + } + if output == "null" { + return "", err + } + + var command []string + if err = json.Unmarshal([]byte(output), &command); err != nil { + return "", err + } + + if len(command) == 3 && command[0] == "/bin/sh" && command[1] == "-c" { + return fmt.Sprintf("CMD %s", command[2]), nil + } + + serializedEntrypoint, err := json.Marshal(command) + return fmt.Sprintf("CMD %s", string(serializedEntrypoint)), 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 "", errors.New(stderr.String()) + } + + containerID := strings.TrimSpace(string(b)) + err = common.PlugnTrigger("post-container-create", []string{"app", appName, containerID, phase}...) + return containerID, err +} diff --git a/plugins/app-json/go.mod b/plugins/app-json/go.mod new file mode 100644 index 000000000..388f20ed8 --- /dev/null +++ b/plugins/app-json/go.mod @@ -0,0 +1,12 @@ +module github.com/dokku/dokku/plugins/app-json + +go 1.14 + +require ( + github.com/codeskyblue/go-sh v0.0.0-20190412065543-76bd3d59ff27 // indirect + github.com/dokku/dokku/plugins/common v0.0.0-00010101000000-000000000000 + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 + github.com/ryanuber/columnize v1.1.2-0.20190319233515-9e6335e58db3 // indirect +) + +replace github.com/dokku/dokku/plugins/common => ../common diff --git a/plugins/app-json/go.sum b/plugins/app-json/go.sum new file mode 100644 index 000000000..ee7942d28 --- /dev/null +++ b/plugins/app-json/go.sum @@ -0,0 +1,8 @@ +github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q= +github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= +github.com/codeskyblue/go-sh v0.0.0-20190412065543-76bd3d59ff27 h1:HHUr4P/aKh4quafGxDT9LDasjGdlGkzLbfmmrlng3kA= +github.com/codeskyblue/go-sh v0.0.0-20190412065543-76bd3d59ff27/go.mod h1:VQx0hjo2oUeQkQUET7wRwradO6f+fN5jzXgB/zROxxE= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/ryanuber/columnize v1.1.2-0.20190319233515-9e6335e58db3 h1:utdYOikI1XjNtTFGCwSM6OmFJblU4ld4gACoJsbadJg= +github.com/ryanuber/columnize v1.1.2-0.20190319233515-9e6335e58db3/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= diff --git a/plugins/app-json/internal-functions b/plugins/app-json/internal-functions deleted file mode 100755 index 3f866a266..000000000 --- a/plugins/app-json/internal-functions +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" -source "$PLUGIN_AVAILABLE_PATH/config/functions" -source "$PLUGIN_AVAILABLE_PATH/ps/functions" - -get_phase_script() { - declare desc="extracts app.json from app image and returns the appropriate json key/value" - declare IMAGE_TAG="$1" PHASE_SCRIPT_KEY="$2" - local APP_JSON_FILE=$(mktemp "/tmp/dokku-${DOKKU_PID}-${FUNCNAME[0]}.XXXXXX") - trap "rm -rf '$APP_JSON_FILE' >/dev/null" RETURN INT TERM - - copy_from_image "$IMAGE" "app.json" "$APP_JSON_FILE" 2>/dev/null || true - - if [[ ! -f "$APP_JSON_FILE" ]]; then - return 0 - fi - - get_json_value ".scripts.dokku.${PHASE_SCRIPT_KEY}" <"$APP_JSON_FILE" -} - -get_release_cmd() { - declare desc="extracts the release command from a given app's procfile" - declare APP="$1" IMAGE_TAG="$2" - - extract_procfile "$APP" "$IMAGE_TAG" >/dev/null - trap "remove_procfile $APP" RETURN INT TERM EXIT - - get_cmd_from_procfile "$APP" "release" "5000" -} - -execute_script() { - declare desc="executes appropriate phase script key from app.json" - declare APP="$1" IMAGE_TAG="$2" PHASE_SCRIPT_KEY="$3" - local IMAGE id SCRIPT_CMD - - local DOCKER_COMMIT_LABEL_ARGS=("--change" "\"LABEL org.label-schema.schema-version=1.0\"" "--change" "\"LABEL org.label-schema.vendor=dokku\"" "--change" "\"LABEL com.dokku.app-name=$APP\"") - local DOCKER_RUN_LABEL_ARGS="--label=com.dokku.app-name=$APP" - - IMAGE=$(get_deploying_app_image_name "$APP" "$IMAGE_TAG") - if [[ "$PHASE_SCRIPT_KEY" == "release" ]]; then - SCRIPT_CMD=$(get_release_cmd "$APP" "$IMAGE_TAG" 2>/dev/null || true) - else - SCRIPT_CMD=$(get_phase_script "$IMAGE" "$PHASE_SCRIPT_KEY" 2>/dev/null || true) - fi - - if [[ -z "$SCRIPT_CMD" ]]; then - return - fi - - dokku_log_exclaim "${PHASE_SCRIPT_KEY^} command declared: '$SCRIPT_CMD'" - local COMMAND - COMMAND="set -eo pipefail; [[ \$DOKKU_TRACE ]] && set -x ; " - COMMAND+=" if [[ -d '/app' ]]; then " - COMMAND+=" export HOME=/app ; " - COMMAND+=" cd \$HOME ; " - COMMAND+=" fi ; " - COMMAND+=" if [[ -d '/app/.profile.d' ]]; then " - COMMAND+=" for file in /app/.profile.d/*; do source \$file; done ; " - COMMAND+=" fi ; " - COMMAND+=" if [[ -d '/cache' ]]; then " - COMMAND+=" rm -rf /tmp/cache ; " - COMMAND+=" ln -sf /cache /tmp/cache ; " - COMMAND+=" fi ; " - - if [[ "$SCRIPT_CMD" == /* ]]; then - local SCRIPT_BIN="$(echo "$SCRIPT_CMD" | cut -d' ' -f1)" - COMMAND+=" if [[ ! -x \"$SCRIPT_BIN\" ]]; then " - COMMAND+=" echo specified binary is not executable ; " - COMMAND+=" exit 1 ; " - COMMAND+=" fi ; " - fi - - COMMAND+=" $SCRIPT_CMD || exit 1;" - COMMAND+=" if [[ -d '/cache' ]]; then " - COMMAND+=" rm -f /tmp/cache ; " - COMMAND+=" fi ; " - - local CACHE_DIR="$DOKKU_ROOT/$APP/cache" - local CACHE_HOST_DIR="$DOKKU_HOST_ROOT/$APP/cache" - [[ -d $CACHE_DIR ]] || mkdir -p "$CACHE_DIR" - - local DOCKER_ARGS=$(: | plugn trigger docker-args-deploy "$APP" "$IMAGE_TAG") - local IMAGE_SOURCE_TYPE="dockerfile" - is_image_herokuish_based "$IMAGE" "$APP" && IMAGE_SOURCE_TYPE="herokuish" - DOCKER_ARGS+=$(: | plugn trigger docker-args-process-deploy "$APP" "$IMAGE_SOURCE_TYPE" "$IMAGE_TAG") - - # strip certain args from DOCKER_ARGS - local filtered_args=("restart" "cpus" "memory" "memory-swap" "memory-reservation" "gpus") - for filtered_arg in "${filtered_args[@]}"; do - DOCKER_ARGS=$(sed -e "s/--${filtered_arg}=[[:graph:]]\+[[:blank:]]\?//g" <<<"$DOCKER_ARGS") - done - - # eval args as array to respect escapes - declare -a ARG_ARRAY - eval "ARG_ARRAY=($DOCKER_ARGS)" - - local DOKKU_APP_SHELL="/bin/bash" - DOKKU_APP_SHELL="$(config_get --global DOKKU_APP_SHELL || echo "$DOKKU_APP_SHELL")" - DOKKU_APP_SHELL="$(config_get "$APP" DOKKU_APP_SHELL || echo "$DOKKU_APP_SHELL")" - [[ -z "$DOKKU_APP_SHELL" ]] && DOKKU_APP_SHELL="/bin/bash" - - local DOKKU_CONTAINER_EXIT_CODE=0 - id=$("$DOCKER_BIN" container create "${DOCKER_RUN_LABEL_ARGS[@]}" $DOKKU_GLOBAL_RUN_ARGS --env DOKKU_TRACE="$DOKKU_TRACE" --label=dokku_phase_script="${PHASE_SCRIPT_KEY}" -v "$CACHE_HOST_DIR:/cache" "${ARG_ARRAY[@]}" "$IMAGE" "$DOKKU_APP_SHELL" -c "$COMMAND") - plugn trigger post-container-create "app" "$id" "$APP" "$PHASE_SCRIPT_KEY" - "$DOCKER_BIN" container start "$id" >/dev/null || DOKKU_CONTAINER_EXIT_CODE=$? - DOKKU_CONTAINER_EXIT_CODE="$("$DOCKER_BIN" container wait "$id" 2>/dev/null || echo "$DOKKU_CONTAINER_EXIT_CODE")" - - if [[ "$DOKKU_CONTAINER_EXIT_CODE" -ne 0 ]]; then - dokku_container_log_verbose_quiet "$id" - dokku_log_fail "execution of '$SCRIPT_CMD' failed!" - fi - - dokku_container_log_verbose_quiet "$id" - if [[ "$PHASE_SCRIPT_KEY" != "predeploy" ]]; then - return - fi - - if ! is_image_herokuish_based "$IMAGE" "$APP"; then - local DOKKU_DOCKERFILE_ENTRYPOINT=$(config_get "$APP" DOKKU_DOCKERFILE_ENTRYPOINT) - local DOKKU_DOCKERFILE_CMD=$(config_get "$APP" DOKKU_DOCKERFILE_CMD) - [[ -z "$DOKKU_DOCKERFILE_ENTRYPOINT" ]] && DOKKU_DOCKERFILE_ENTRYPOINT="$(get_entrypoint_from_image "$IMAGE")" - [[ -z "$DOKKU_DOCKERFILE_CMD" ]] && DOKKU_DOCKERFILE_CMD="$(get_cmd_from_image "$IMAGE")" - - [[ -n "$DOKKU_DOCKERFILE_ENTRYPOINT" ]] && local DOCKER_COMMIT_ENTRYPOINT_CHANGE_ARG="--change='$DOKKU_DOCKERFILE_ENTRYPOINT'" - [[ -n "$DOKKU_DOCKERFILE_CMD" ]] && local DOCKER_COMMIT_CMD_CHANGE_ARG="--change='$DOKKU_DOCKERFILE_CMD'" - - local DOCKER_COMMIT_ARGS="$DOCKER_COMMIT_ENTRYPOINT_CHANGE_ARG $DOCKER_COMMIT_CMD_CHANGE_ARG" - fi - - eval "$DOCKER_BIN" container commit "${DOCKER_COMMIT_LABEL_ARGS[@]}" "${DOCKER_COMMIT_ARGS[@]}" "$id" "$IMAGE" >/dev/null -} diff --git a/plugins/app-json/post-deploy b/plugins/app-json/post-deploy deleted file mode 100755 index 3d2eef033..000000000 --- a/plugins/app-json/post-deploy +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" -source "$PLUGIN_CORE_AVAILABLE_PATH/app-json/internal-functions" - -trigger-app-json-post-deploy() { - declare desc="app-json scripts execution" - declare trigger="post-deploy" - declare APP="$1" IMAGE_TAG="$4" - local PHASE_SCRIPT_KEY="postdeploy" - - execute_script "$APP" "$IMAGE_TAG" "$PHASE_SCRIPT_KEY" -} - -trigger-app-json-post-deploy "$@" diff --git a/plugins/app-json/pre-deploy b/plugins/app-json/pre-deploy deleted file mode 100755 index b9ac30bf0..000000000 --- a/plugins/app-json/pre-deploy +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" -source "$PLUGIN_CORE_AVAILABLE_PATH/app-json/internal-functions" - -trigger-app-json-pre-deploy() { - declare desc="app-json scripts execution" - declare trigger="pre-deploy" - declare APP="$1" IMAGE_TAG="$2" - local PHASE_SCRIPT_KEY="predeploy" - - execute_script "$APP" "$IMAGE_TAG" "$PHASE_SCRIPT_KEY" - execute_script "$APP" "$IMAGE_TAG" "release" -} - -trigger-app-json-pre-deploy "$@" diff --git a/plugins/app-json/src/triggers/triggers.go b/plugins/app-json/src/triggers/triggers.go new file mode 100644 index 000000000..200ae162e --- /dev/null +++ b/plugins/app-json/src/triggers/triggers.go @@ -0,0 +1,36 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strings" + + "github.com/dokku/dokku/plugins/app-json" + "github.com/dokku/dokku/plugins/common" +) + +// main entrypoint to all triggers +func main() { + parts := strings.Split(os.Args[0], "/") + trigger := parts[len(parts)-1] + flag.Parse() + + var err error + switch trigger { + case "post-deploy": + appName := flag.Arg(0) + imageTag := flag.Arg(3) + err = appjson.TriggerPostDeploy(appName, imageTag) + case "pre-deploy": + appName := flag.Arg(0) + imageTag := flag.Arg(1) + err = appjson.TriggerPreDeploy(appName, imageTag) + default: + common.LogFail(fmt.Sprintf("Invalid plugin trigger call: %s", trigger)) + } + + if err != nil { + common.LogFail(err.Error()) + } +} diff --git a/plugins/app-json/triggers.go b/plugins/app-json/triggers.go new file mode 100644 index 000000000..88d74c824 --- /dev/null +++ b/plugins/app-json/triggers.go @@ -0,0 +1,21 @@ +package appjson + +// TriggerPostDeploy is a trigger to execute the postdeploy deployment task +func TriggerPostDeploy(appName string, imageTag string) error { + if err := executeScript(appName, imageTag, "postdeploy"); err != nil { + return err + } + return nil +} + +// TriggerPreDeploy is a trigger to execute predeploy and release deployment tasks +func TriggerPreDeploy(appName string, imageTag string) error { + if err := executeScript(appName, imageTag, "predeploy"); err != nil { + return err + } + + if err := executeScript(appName, imageTag, "release"); err != nil { + return err + } + return nil +} diff --git a/plugins/builder-dockerfile/builder-build b/plugins/builder-dockerfile/builder-build index 2a1cf3f53..a443da29b 100755 --- a/plugins/builder-dockerfile/builder-build +++ b/plugins/builder-dockerfile/builder-build @@ -20,12 +20,6 @@ trigger-builder-dockerfile-builder-build() { # extract first port from Dockerfile local DOCKERFILE_PORTS=$(get_dockerfile_exposed_ports Dockerfile) [[ -n "$DOCKERFILE_PORTS" ]] && config_set --no-restart "$APP" DOKKU_DOCKERFILE_PORTS="$DOCKERFILE_PORTS" - - # extract ENTRYPOINT/CMD from Dockerfile - local DOCKERFILE_ENTRYPOINT=$(extract_directive_from_dockerfile Dockerfile ENTRYPOINT) - [[ -n "$DOCKERFILE_ENTRYPOINT" ]] && config_set --no-restart "$APP" DOKKU_DOCKERFILE_ENTRYPOINT="$DOCKERFILE_ENTRYPOINT" - local DOCKERFILE_CMD=$(extract_directive_from_dockerfile Dockerfile CMD) - [[ -n "$DOCKERFILE_CMD" ]] && config_set --no-restart "$APP" DOKKU_DOCKERFILE_CMD="$DOCKERFILE_CMD" plugn trigger pre-build-dockerfile "$APP" [[ "$DOKKU_DOCKERFILE_CACHE_BUILD" == "false" ]] && DOKKU_DOCKER_BUILD_OPTS="$DOKKU_DOCKER_BUILD_OPTS --no-cache" diff --git a/plugins/common/common.go b/plugins/common/common.go index b34cee5bc..dc1e282dd 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...) +} + +// NewShellCmdWithArgs 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,24 +74,28 @@ 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") return fmt.Sprintf("%v/%v", dokkuRoot, appName) } +// AppHostRoot returns the app root path +func AppHostRoot(appName string) string { + dokkuHostRoot := MustGetEnv("DOKKU_HOST_ROOT") + return fmt.Sprintf("%v/%v", dokkuHostRoot, appName) +} + // AskForDestructiveConfirmation checks for confirmation on destructive actions func AskForDestructiveConfirmation(name string, objectType string) error { LogWarn("WARNING: Potentially Destructive Action") @@ -112,6 +128,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 { @@ -331,7 +448,30 @@ func DirectoryExists(filePath string) bool { return fi.IsDir() } -// DockerInspect runs an inspect command with a given format against a container id +// 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 { @@ -424,6 +564,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)) diff --git a/plugins/common/functions b/plugins/common/functions index d377888b1..85fb0cbb6 100755 --- a/plugins/common/functions +++ b/plugins/common/functions @@ -541,7 +541,7 @@ is_app_running() { declare deprecated=true local APP="$1" - dokku_log_warn "Deprecated: ps#fn-is-app-running" + dokku_log_warn "Deprecated: ps#fn-ps-is-app-running" verify_app_name "$APP" local APP_RUNNING_CONTAINER_IDS=$(get_app_running_container_ids "$APP" 2>/dev/null) diff --git a/plugins/common/log.go b/plugins/common/log.go index 7aa77b82b..3619bbe65 100644 --- a/plugins/common/log.go +++ b/plugins/common/log.go @@ -3,6 +3,7 @@ package common import ( "fmt" "os" + "strings" ) // LogFail is the failure log formatter @@ -71,12 +72,45 @@ func LogVerboseQuiet(text string) { } } +// LogVerboseQuietContainerLogs is the verbose log formatter for container logs +func LogVerboseQuietContainerLogs(containerID string) { + sc := NewShellCmdWithArgs(DockerBin(), "container", "logs", containerID) + sc.ShowOutput = false + b, err := sc.CombinedOutput() + if err != nil { + LogExclaim(fmt.Sprintf("Failed to fetch container logs: %s", containerID)) + } + + output := strings.TrimSpace(string(b)) + if len(output) == 0 { + return + } + + for _, line := range strings.Split(output, "\n") { + if line != "" { + LogVerboseQuiet(line) + } + } +} + // LogWarn is the warning log formatter func LogWarn(text string) { fmt.Fprintln(os.Stderr, fmt.Sprintf(" ! %s", text)) } +// LogExclaim is the log exclaim formatter +func LogExclaim(text string) { + fmt.Println(fmt.Sprintf(" ! %s", text)) +} + // LogStderr is the stderr log formatter func LogStderr(text string) { fmt.Fprintln(os.Stderr, text) } + +// LogDebug is the debug log formatter +func LogDebug(text string) { + if os.Getenv("DOKKU_TRACE") == "" { + fmt.Fprintln(os.Stderr, fmt.Sprintf(" ? %s", text)) + } +} diff --git a/plugins/ps/functions b/plugins/ps/functions index 9a53b57de..e1dda2a68 100755 --- a/plugins/ps/functions +++ b/plugins/ps/functions @@ -11,8 +11,7 @@ get_cmd_from_procfile() { local COMMAND verify_app_name "$APP" - [[ ! -f $DOKKU_PROCFILE ]] && return - procfile-util show --procfile "$DOKKU_PROCFILE" --process-type "$PROC_TYPE" --default-port "$PORT" + fn-procfile-get-command "$DOKKU_PROCFILE" "$PROC_TYPE" "$PORT" } print_dokku_scale_file() { @@ -31,11 +30,18 @@ extract_procfile() { verify_app_name "$APP" remove_procfile "$APP" - copy_from_image "$IMAGE" "Procfile" "$DOKKU_PROCFILE" 2>/dev/null || true - if [[ -f "$DOKKU_PROCFILE" ]]; then + fn-procfile-extract "$IMAGE" "$DOKKU_PROCFILE" +} + +fn-procfile-extract() { + declare desc="extract procfile from a specified docker image" + declare IMAGE="$1" PROCFILE_PATH="$2" + + copy_from_image "$IMAGE" "Procfile" "$PROCFILE_PATH" 2>/dev/null || true + if [[ -f "$PROCFILE_PATH" ]]; then dokku_log_info1_quiet "App Procfile file found" # shellcheck disable=SC2069 - PROCFILE_ERRORS="$(procfile-util check --procfile "$DOKKU_PROCFILE" 2>&1 >/dev/null || true)" + PROCFILE_ERRORS="$(procfile-util check --procfile "$PROCFILE_PATH" 2>&1 >/dev/null || true)" if [[ -n "$PROCFILE_ERRORS" ]]; then dokku_log_fail "${PROCFILE_ERRORS}" fi @@ -44,6 +50,17 @@ extract_procfile() { fi } +fn-procfile-get-command() { + declare desc="Fetch a command from a Procfile" + declare PROCFILE_PATH="$1" PROCESS_TYPE="$2" PORT="$3" + + if [[ ! -f "$PROCFILE_PATH" ]]; then + return + fi + + procfile-util show --procfile "$PROCFILE_PATH" --process-type "$PROCESS_TYPE" --default-port "$PORT" 2>/dev/null +} + remove_procfile() { declare desc="removes DOKKU_PROCFILE" local APP="$1" diff --git a/plugins/ps/post-create b/plugins/ps/post-create index 8434d5444..14efa77a8 100755 --- a/plugins/ps/post-create +++ b/plugins/ps/post-create @@ -12,6 +12,9 @@ trigger-ps-post-create() { local passed_phases=(deploy) add_passed_docker_option passed_phases[@] "--restart=on-failure:10" + + mkdir -p "${DOKKU_LIB_ROOT}/data/ps/${APP}" + chown -R "${DOKKU_SYSTEM_USER}:${DOKKU_SYSTEM_GROUP}" "${DOKKU_LIB_ROOT}/data/ps/${APP}" } trigger-ps-post-create "$@" diff --git a/plugins/ps/procfile-extract b/plugins/ps/procfile-extract new file mode 100755 index 000000000..74a2ec218 --- /dev/null +++ b/plugins/ps/procfile-extract @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x +source "$PLUGIN_AVAILABLE_PATH/ps/functions" + +trigger-ps-procfile-extract() { + declare desc="ps procfile-extract plugin trigger" + declare trigger="procfile-extract" + declare desc="extract procfile from a specified docker image" + declare APP="$1" IMAGE="$2" FORCE_EXTRACT="$3" PROCFILE_PATH="$4" + + if [[ -z "$PROCFILE_PATH" ]]; then + PROCFILE_PATH="$DOKKU_LIB_ROOT/data/ps/$APP/Procfile.extracted" + mkdir -p "${DOKKU_LIB_ROOT}/data/ps/${APP}" + chown -R "${DOKKU_SYSTEM_USER}:${DOKKU_SYSTEM_GROUP}" "${DOKKU_LIB_ROOT}/data/ps/${APP}" + fi + + if [[ "$FORCE_EXTRACT" == "true" ]]; then + plugn trigger procfile-remove "$APP" "$PROCFILE_PATH" + fi + + fn-procfile-extract "$IMAGE" "$PROCFILE_PATH" +} + +trigger-ps-procfile-extract "$@" diff --git a/plugins/ps/procfile-get-command b/plugins/ps/procfile-get-command new file mode 100755 index 000000000..00c96d6a6 --- /dev/null +++ b/plugins/ps/procfile-get-command @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x +source "$PLUGIN_AVAILABLE_PATH/ps/functions" + +trigger-ps-procfile-get-command() { + declare desc="ps procfile-get-command plugin trigger" + declare trigger="procfile-get-command" + declare desc="extract command from an app procfile" + declare APP="$1" PROCESS_TYPE="$2" PORT="$3" PROCFILE_PATH="$4" + + if [[ -z "$PROCFILE_PATH" ]]; then + PROCFILE_PATH="$DOKKU_LIB_ROOT/data/ps/$APP/Procfile.extracted" + fi + + fn-procfile-get-command "$PROCFILE_PATH" "$PROCESS_TYPE" "$PORT" +} + +trigger-ps-procfile-get-command "$@" diff --git a/plugins/ps/procfile-remove b/plugins/ps/procfile-remove new file mode 100755 index 000000000..28b1ced70 --- /dev/null +++ b/plugins/ps/procfile-remove @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +trigger-ps-procfile-remove() { + declare desc="ps procfile-remove plugin trigger" + declare trigger="procfile-remove" + declare desc="remove procfile for a given app" + declare APP="$1" PROCFILE_PATH="$2" + + if [[ -z "$PROCFILE_PATH" ]]; then + PROCFILE_PATH="$DOKKU_LIB_ROOT/data/ps/$APP/Procfile.extracted" + fi + + if [[ ! -f "$PROCFILE_PATH" ]]; then + return + fi + + rm -f "$PROCFILE_PATH" 2>/dev/null +} + +trigger-ps-procfile-remove "$@" diff --git a/tests/apps/dockerfile-entrypoint/Dockerfile b/tests/apps/dockerfile-entrypoint/Dockerfile new file mode 100644 index 000000000..18d2ddaa5 --- /dev/null +++ b/tests/apps/dockerfile-entrypoint/Dockerfile @@ -0,0 +1,10 @@ +FROM ruby:2.5.1 + +RUN mkdir /app +WORKDIR /app + +ADD Procfile Procfile +ADD entrypoint entrypoint +ADD app.json app.json + +ENTRYPOINT ["./entrypoint"] diff --git a/tests/apps/dockerfile-entrypoint/Procfile b/tests/apps/dockerfile-entrypoint/Procfile new file mode 100644 index 000000000..f296d4a33 --- /dev/null +++ b/tests/apps/dockerfile-entrypoint/Procfile @@ -0,0 +1 @@ +web: sleep 1000 diff --git a/tests/apps/dockerfile-entrypoint/app.json b/tests/apps/dockerfile-entrypoint/app.json new file mode 100644 index 000000000..16b493cb2 --- /dev/null +++ b/tests/apps/dockerfile-entrypoint/app.json @@ -0,0 +1,7 @@ +{ + "scripts": { + "dokku": { + "predeploy": "touch /app/predeploy.test" + } + } +} diff --git a/tests/apps/dockerfile-entrypoint/entrypoint b/tests/apps/dockerfile-entrypoint/entrypoint new file mode 100755 index 000000000..1b4625f4f --- /dev/null +++ b/tests/apps/dockerfile-entrypoint/entrypoint @@ -0,0 +1,9 @@ +#!/bin/bash +set -o pipefail + +if [[ "$1" == touch* ]]; then + echo "entrypoint script started with arguments $@" + touch "$(echo $@ | awk '{print $2}')" +else + exec "$@" +fi diff --git a/tests/apps/dockerfile-procfile/app.json b/tests/apps/dockerfile-procfile/app.json new file mode 100644 index 000000000..0dbdea3ec --- /dev/null +++ b/tests/apps/dockerfile-procfile/app.json @@ -0,0 +1,8 @@ +{ + "scripts": { + "dokku": { + "predeploy": "touch /app/predeploy.test", + "postdeploy": "touch /app/postdeploy.test" + } + } +} diff --git a/tests/apps/dockerfile-release/Dockerfile b/tests/apps/dockerfile-release/Dockerfile new file mode 100644 index 000000000..1c53486a3 --- /dev/null +++ b/tests/apps/dockerfile-release/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.8-buster + +RUN pip install --user pipenv +ENV PATH="${PATH}:/root/.local/bin" +ENV PIPENV_VENV_IN_PROJECT=0 + +COPY . /app + +WORKDIR /app + +RUN ls -lah + +RUN rm -rf .venv + +RUN mkdir .venv + +RUN pipenv install + +CMD pipenv run gunicorn -b 0.0.0.0:8000 --workers=3 --threads=5 project.wsgi:application diff --git a/tests/apps/dockerfile-release/Pipfile b/tests/apps/dockerfile-release/Pipfile new file mode 100644 index 000000000..26df7ab95 --- /dev/null +++ b/tests/apps/dockerfile-release/Pipfile @@ -0,0 +1,17 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] + +[packages] +django = "*" +gunicorn = "*" +djangorestframework = "*" +whitenoise = "*" +psycopg2-binary = "*" +dj-database-url = "*" + +[requires] +python_version = "3.8" diff --git a/tests/apps/dockerfile-release/Pipfile.lock b/tests/apps/dockerfile-release/Pipfile.lock new file mode 100644 index 000000000..89bb16b72 --- /dev/null +++ b/tests/apps/dockerfile-release/Pipfile.lock @@ -0,0 +1,120 @@ +{ + "_meta": { + "hash": { + "sha256": "6e4ab3d777dd8ead8bc254ab4bb72ee80c93d1c09537b30ad23beb21c8bfdba9" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.8" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "asgiref": { + "hashes": [ + "sha256:7e06d934a7718bf3975acbf87780ba678957b87c7adc056f13b6215d610695a0", + "sha256:ea448f92fc35a0ef4b1508f53a04c4670255a3f33d22a81c8fc9c872036adbe5" + ], + "version": "==3.2.3" + }, + "dj-database-url": { + "hashes": [ + "sha256:4aeaeb1f573c74835b0686a2b46b85990571159ffc21aa57ecd4d1e1cb334163", + "sha256:851785365761ebe4994a921b433062309eb882fedd318e1b0fcecc607ed02da9" + ], + "index": "pypi", + "version": "==0.5.0" + }, + "django": { + "hashes": [ + "sha256:4f2c913303be4f874015993420bf0bd8fd2097a9c88e6b49c6a92f9bdd3fb13a", + "sha256:8c3575f81e11390893860d97e1e0154c47512f180ea55bd84ce8fa69ba8051ca" + ], + "index": "pypi", + "version": "==3.0.2" + }, + "djangorestframework": { + "hashes": [ + "sha256:05809fc66e1c997fd9a32ea5730d9f4ba28b109b9da71fccfa5ff241201fd0a4", + "sha256:e782087823c47a26826ee5b6fa0c542968219263fb3976ec3c31edab23a4001f" + ], + "index": "pypi", + "version": "==3.11.0" + }, + "gunicorn": { + "hashes": [ + "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626", + "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c" + ], + "index": "pypi", + "version": "==20.0.4" + }, + "psycopg2-binary": { + "hashes": [ + "sha256:040234f8a4a8dfd692662a8308d78f63f31a97e1c42d2480e5e6810c48966a29", + "sha256:086f7e89ec85a6704db51f68f0dcae432eff9300809723a6e8782c41c2f48e03", + "sha256:18ca813fdb17bc1db73fe61b196b05dd1ca2165b884dd5ec5568877cabf9b039", + "sha256:19dc39616850342a2a6db70559af55b22955f86667b5f652f40c0e99253d9881", + "sha256:2166e770cb98f02ed5ee2b0b569d40db26788e0bf2ec3ae1a0d864ea6f1d8309", + "sha256:3a2522b1d9178575acee4adf8fd9f979f9c0449b00b4164bb63c3475ea6528ed", + "sha256:3aa773580f85a28ffdf6f862e59cb5a3cc7ef6885121f2de3fca8d6ada4dbf3b", + "sha256:3b5deaa3ee7180585a296af33e14c9b18c218d148e735c7accf78130765a47e3", + "sha256:407af6d7e46593415f216c7f56ba087a9a42bd6dc2ecb86028760aa45b802bd7", + "sha256:4c3c09fb674401f630626310bcaf6cd6285daf0d5e4c26d6e55ca26a2734e39b", + "sha256:4c6717962247445b4f9e21c962ea61d2e884fc17df5ddf5e35863b016f8a1f03", + "sha256:50446fae5681fc99f87e505d4e77c9407e683ab60c555ec302f9ac9bffa61103", + "sha256:5057669b6a66aa9ca118a2a860159f0ee3acf837eda937bdd2a64f3431361a2d", + "sha256:5dd90c5438b4f935c9d01fcbad3620253da89d19c1f5fca9158646407ed7df35", + "sha256:659c815b5b8e2a55193ede2795c1e2349b8011497310bb936da7d4745652823b", + "sha256:69b13fdf12878b10dc6003acc8d0abf3ad93e79813fd5f3812497c1c9fb9be49", + "sha256:7a1cb80e35e1ccea3e11a48afe65d38744a0e0bde88795cc56a4d05b6e4f9d70", + "sha256:7e6e3c52e6732c219c07bd97fff6c088f8df4dae3b79752ee3a817e6f32e177e", + "sha256:7f42a8490c4fe854325504ce7a6e4796b207960dabb2cbafe3c3959cb00d1d7e", + "sha256:84156313f258eafff716b2961644a4483a9be44a5d43551d554844d15d4d224e", + "sha256:8578d6b8192e4c805e85f187bc530d0f52ba86c39172e61cd51f68fddd648103", + "sha256:890167d5091279a27e2505ff0e1fb273f8c48c41d35c5b92adbf4af80e6b2ed6", + "sha256:98e10634792ac0e9e7a92a76b4991b44c2325d3e7798270a808407355e7bb0a1", + "sha256:9aadff9032e967865f9778485571e93908d27dab21d0fdfdec0ca779bb6f8ad9", + "sha256:9f24f383a298a0c0f9b3113b982e21751a8ecde6615494a3f1470eb4a9d70e9e", + "sha256:a73021b44813b5c84eda4a3af5826dd72356a900bac9bd9dd1f0f81ee1c22c2f", + "sha256:afd96845e12638d2c44d213d4810a08f4dc4a563f9a98204b7428e567014b1cd", + "sha256:b73ddf033d8cd4cc9dfed6324b1ad2a89ba52c410ef6877998422fcb9c23e3a8", + "sha256:b8f490f5fad1767a1331df1259763b3bad7d7af12a75b950c2843ba319b2415f", + "sha256:dbc5cd56fff1a6152ca59445178652756f4e509f672e49ccdf3d79c1043113a4", + "sha256:eac8a3499754790187bb00574ab980df13e754777d346f85e0ff6df929bcd964", + "sha256:eaed1c65f461a959284649e37b5051224f4db6ebdc84e40b5e65f2986f101a08" + ], + "index": "pypi", + "version": "==2.8.4" + }, + "pytz": { + "hashes": [ + "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", + "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be" + ], + "version": "==2019.3" + }, + "sqlparse": { + "hashes": [ + "sha256:40afe6b8d4b1117e7dff5504d7a8ce07d9a1b15aeeade8a2d10f130a834f8177", + "sha256:7c3dca29c022744e95b547e867cee89f4fce4373f3549ccd8797d8eb52cdb873" + ], + "version": "==0.3.0" + }, + "whitenoise": { + "hashes": [ + "sha256:0f9137f74bd95fa54329ace88d8dc695fbe895369a632e35f7a136e003e41d73", + "sha256:62556265ec1011bd87113fb81b7516f52688887b7a010ee899ff1fd18fd22700" + ], + "index": "pypi", + "version": "==5.0.1" + } + }, + "develop": {} +} diff --git a/tests/apps/dockerfile-release/Procfile b/tests/apps/dockerfile-release/Procfile new file mode 100644 index 000000000..fc5b1ca69 --- /dev/null +++ b/tests/apps/dockerfile-release/Procfile @@ -0,0 +1 @@ +release: pipenv run python manage.py migrate diff --git a/tests/apps/dockerfile-release/app/__init__.py b/tests/apps/dockerfile-release/app/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/apps/dockerfile-release/app/admin.py b/tests/apps/dockerfile-release/app/admin.py new file mode 100644 index 000000000..8c38f3f3d --- /dev/null +++ b/tests/apps/dockerfile-release/app/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/tests/apps/dockerfile-release/app/apps.py b/tests/apps/dockerfile-release/app/apps.py new file mode 100644 index 000000000..4f29a567f --- /dev/null +++ b/tests/apps/dockerfile-release/app/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class AppMainConfig(AppConfig): + name = 'app' diff --git a/tests/apps/dockerfile-release/app/migrations/__init__.py b/tests/apps/dockerfile-release/app/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/apps/dockerfile-release/app/models.py b/tests/apps/dockerfile-release/app/models.py new file mode 100644 index 000000000..71a836239 --- /dev/null +++ b/tests/apps/dockerfile-release/app/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/tests/apps/dockerfile-release/app/tests.py b/tests/apps/dockerfile-release/app/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/tests/apps/dockerfile-release/app/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/tests/apps/dockerfile-release/app/views.py b/tests/apps/dockerfile-release/app/views.py new file mode 100644 index 000000000..91ea44a21 --- /dev/null +++ b/tests/apps/dockerfile-release/app/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/tests/apps/dockerfile-release/manage.py b/tests/apps/dockerfile-release/manage.py new file mode 100755 index 000000000..0c5ebba3b --- /dev/null +++ b/tests/apps/dockerfile-release/manage.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os + + +def main(): + print("SECRET_KEY: {0}".format(os.getenv('SECRET_KEY'))) + + +if __name__ == '__main__': + main() diff --git a/tests/apps/dockerfile-release/project/__init__.py b/tests/apps/dockerfile-release/project/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/apps/dockerfile-release/project/asgi.py b/tests/apps/dockerfile-release/project/asgi.py new file mode 100644 index 000000000..46249aa11 --- /dev/null +++ b/tests/apps/dockerfile-release/project/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for project project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') + +application = get_asgi_application() diff --git a/tests/apps/dockerfile-release/project/settings/__init__.py b/tests/apps/dockerfile-release/project/settings/__init__.py new file mode 100644 index 000000000..e7999ecf3 --- /dev/null +++ b/tests/apps/dockerfile-release/project/settings/__init__.py @@ -0,0 +1,12 @@ +import os + +environment = os.getenv('ENVIRONMENT') + +if environment == 'dev': + from .dev import * +elif environment == 'staging': + from .staging import * +elif environment == 'production': + from .staging import * +else: + raise Exception("Invalid environment") diff --git a/tests/apps/dockerfile-release/project/settings/defaults.py b/tests/apps/dockerfile-release/project/settings/defaults.py new file mode 100644 index 000000000..529aabcd1 --- /dev/null +++ b/tests/apps/dockerfile-release/project/settings/defaults.py @@ -0,0 +1,135 @@ +""" +Django settings for project project. + +Generated by 'django-admin startproject' using Django 3.0.2. + +For more information on this file, see +https://docs.djangoproject.com/en/3.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/3.0/ref/settings/ +""" + +import os +import dj_database_url + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = os.getenv('SECRET_KEY') + +if not SECRET_KEY: + raise Exception("Secret Key Required") + +ALLOWED_HOSTS = [ + "*", +] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'whitenoise.runserver_nostatic', + 'django.contrib.staticfiles', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'whitenoise.middleware.WhiteNoiseMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'project.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'project.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/3.0/ref/settings/#databases + +DATABASES = { + 'default': dj_database_url.config(conn_max_age=500, ssl_require=True) +} + +del DATABASES['default']['OPTIONS']['sslmode'] + +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') + + +# Password validation +# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/3.0/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.0/howto/static-files/ + +STATIC_ROOT = os.path.join(PROJECT_ROOT, 'staticfiles') +STATIC_URL = '/static/' + +# Extra places for collectstatic to find static files. +STATICFILES_DIRS = [ + os.path.join(PROJECT_ROOT, 'static'), +] + +STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' diff --git a/tests/apps/dockerfile-release/project/settings/dev.py b/tests/apps/dockerfile-release/project/settings/dev.py new file mode 100644 index 000000000..ece308f17 --- /dev/null +++ b/tests/apps/dockerfile-release/project/settings/dev.py @@ -0,0 +1,4 @@ +from .defaults import * + + +DEBUG = True \ No newline at end of file diff --git a/tests/apps/dockerfile-release/project/settings/production.py b/tests/apps/dockerfile-release/project/settings/production.py new file mode 100644 index 000000000..dbc6ab431 --- /dev/null +++ b/tests/apps/dockerfile-release/project/settings/production.py @@ -0,0 +1 @@ +from .defaults import * \ No newline at end of file diff --git a/tests/apps/dockerfile-release/project/settings/staging.py b/tests/apps/dockerfile-release/project/settings/staging.py new file mode 100644 index 000000000..dbc6ab431 --- /dev/null +++ b/tests/apps/dockerfile-release/project/settings/staging.py @@ -0,0 +1 @@ +from .defaults import * \ No newline at end of file diff --git a/tests/apps/dockerfile-release/project/urls.py b/tests/apps/dockerfile-release/project/urls.py new file mode 100644 index 000000000..5a0c0f185 --- /dev/null +++ b/tests/apps/dockerfile-release/project/urls.py @@ -0,0 +1,21 @@ +"""project URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/3.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path('admin/', admin.site.urls), +] diff --git a/tests/apps/dockerfile-release/project/wsgi.py b/tests/apps/dockerfile-release/project/wsgi.py new file mode 100644 index 000000000..eb1e0a505 --- /dev/null +++ b/tests/apps/dockerfile-release/project/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for project project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') + +application = get_wsgi_application() diff --git a/tests/unit/app-json.bats b/tests/unit/app-json.bats index d7f9a24fb..1bd0d2f03 100644 --- a/tests/unit/app-json.bats +++ b/tests/unit/app-json.bats @@ -13,7 +13,16 @@ teardown() { } @test "(app-json) app.json scripts" { - deploy_app nodejs-express + run deploy_app nodejs-express + echo "output: $output" + echo "status: $status" + assert_success + + run docker inspect "${TEST_APP}.web.1" --format "{{json .Config.Cmd}}" + echo "output: $output" + echo "status: $status" + assert_success + assert_output '["/start","web"]' run /bin/bash -c "dokku --rm run $TEST_APP ls /app/prebuild.test" echo "output: $output" @@ -28,15 +37,80 @@ teardown() { CID=$(docker ps -a -q -f "ancestor=dokku/${TEST_APP}" -f "label=dokku_phase_script=postdeploy") DOCKER_COMMIT_LABEL_ARGS=("--change" "LABEL org.label-schema.schema-version=1.0" "--change" "LABEL org.label-schema.vendor=dokku" "--change" "LABEL com.dokku.app-name=$TEST_APP") IMAGE_ID=$(docker commit "${DOCKER_COMMIT_LABEL_ARGS[@]}" $CID dokku-test/${TEST_APP}) - run /bin/bash -c "docker run -ti $IMAGE_ID ls /app/postdeploy.test" + run /bin/bash -c "docker run --rm -ti $IMAGE_ID ls /app/postdeploy.test" echo "output: $output" echo "status: $status" assert_success } - @test "(app-json) app.json scripts missing" { - deploy_app nodejs-express-noappjson + run deploy_app nodejs-express-noappjson + echo "output: $output" + echo "status: $status" + assert_success +} + +@test "(app-json) app.json dockerfile cmd" { + run deploy_app dockerfile-procfile + echo "output: $output" + echo "status: $status" + assert_success + + run docker inspect "dokku/${TEST_APP}:latest" --format "{{json .Config.Cmd}}" + echo "output: $output" + echo "status: $status" + assert_success + assert_output '["/bin/sh","-c","npm start"]' + + run docker inspect "dokku/${TEST_APP}:latest" --format "{{json .Config.Entrypoint}}" + echo "output: $output" + echo "status: $status" + assert_success + assert_output 'null' +} + +@test "(app-json) app.json dockerfile entrypoint" { + run deploy_app dockerfile-entrypoint + echo "output: $output" + echo "status: $status" + assert_success + + run docker inspect "dokku/${TEST_APP}:latest" --format "{{json .Config.Cmd}}" + echo "output: $output" + echo "status: $status" + assert_success + assert_output 'null' + + run docker inspect "dokku/${TEST_APP}:latest" --format "{{json .Config.Entrypoint}}" + echo "output: $output" + echo "status: $status" + assert_success + assert_output '["./entrypoint"]' +} + +@test "(app-json) app.json dockerfile release" { + run /bin/bash -c "dokku config:set --no-restart $TEST_APP SECRET_KEY=fjdkslafjdk ENVIRONMENT=dev" + echo "output: $output" + echo "status: $status" + assert_success + + run deploy_app dockerfile-release + echo "output: $output" + echo "status: $status" + assert_output_contains "Executing release task from Procfile" + assert_output_contains "SECRET_KEY: fjdkslafjdk" + assert_success +} + +@test "(app-json) app.json dockerfile entrypoint" { + run deploy_app dockerfile-entrypoint + echo "output: $output" + echo "status: $status" + assert_output_contains "Executing predeploy task from app.json" + assert_output_contains "entrypoint script started with arguments touch /app/predeploy.test" + assert_success + + run /bin/bash -c "dokku --rm run $TEST_APP ls /app/predeploy.test" echo "output: $output" echo "status: $status" assert_success