diff --git a/docs/appendices/0.22.0-migration-guide.md b/docs/appendices/0.22.0-migration-guide.md index 55f04a508..ca5b96cbe 100644 --- a/docs/appendices/0.22.0-migration-guide.md +++ b/docs/appendices/0.22.0-migration-guide.md @@ -5,3 +5,15 @@ - Underscores are no longer valid characters in app names. Please rename applications before upgrading. - Process type names specified in Procfile may no longer use characters not valid in DNS Label Names ([RFC 1123](https://tools.ietf.org/html/rfc1123)). - The minimum Docker version is now 17.05.0. + +## Removals + +The `ps` command has had a few removals as a result of a rewrite to Golang: + +- The `ps:set-restart-policy` command has been removed in favor the `ps:set` command. +- The `ps:restart-policy` command has been removed in favor of the `ps:report` command. +- The `ps:rebuildall` command has been removed in favor of calling the `ps:rebuild` with the `--all` flag instead of an app name. +- The `ps:restartall` command has been removed in favor of calling the `ps:restart` with the `--all` flag instead of an app name. +- The `ps:stopall` command has been removed in favor of calling the `ps:stop` with the `--all` flag instead of an app name. +- The `ps:startall` command has been removed in favor of calling the `ps:start` with the `--all` flag instead of an app name. +- The `DOKKU_PROCFILE` file is no longer located in the git directory for a given app on the server. It's location is currently not exposed. diff --git a/docs/deployment/process-management.md b/docs/deployment/process-management.md index f44b3351e..3478b608f 100644 --- a/docs/deployment/process-management.md +++ b/docs/deployment/process-management.md @@ -8,10 +8,9 @@ ps:rebuild # Rebuild an app from source ps:rebuildall # Rebuild all apps from source ps:report [] [] # Displays a process report for one or more apps ps:restart # Restart app container(s) -ps:restart-policy # Shows the restart-policy for an app ps:restartall # Restart all deployed app containers ps:scale = [=] # Get/Set how many instances of a given process to run -ps:set-restart-policy # Sets app restart-policy +ps:set # Set or clear a ps property for an app ps:start # Start app container(s) ps:startall # Start all deployed app containers ps:stop # Stop app container(s) @@ -133,68 +132,29 @@ dokku ps:startall > New as of 0.7.0 -By default, Dokku will automatically restart containers that exit with a non-zero status up to 10 times via the [on-failure Docker restart policy](https://docs.docker.com/engine/reference/run/#restart-policies-restart). - -### Showing the current restart policy - -The `ps:restart-policy` command will show the currently configured restart policy for an application. The default policy is `on-failure:10` - -```shell -dokku ps:restart-policy node-js-app -``` - -``` -=====> node-js-app restart-policy: -on-failure:10 -``` +By default, Dokku will automatically restart containers that exit with a non-zero status up to 10 times via the [on-failure Docker restart policy](https://docs.docker.com/engine/reference/run/#restart-policies---restart). ### Setting the restart policy -You can configure this via the `ps:set-restart-policy` command: +> A change in the restart policy must be followed by a `ps:rebuild` call. + +You can configure this via the `ps:set` command: ```shell # always restart an exited container -dokku ps:set-restart-policy node-js-app always -``` +dokku ps:set node-js-app restart-policy always -``` ------> Setting restart policy: always -``` - -```shell # never restart an exited container -dokku ps:set-restart-policy node-js-app no -``` +dokku ps:set node-js-app restart-policy no -``` ------> Setting restart policy: no -``` - -```shell # only restart it on Docker restart if it was not manually stopped -dokku ps:set-restart-policy node-js-app unless-stopped -``` +dokku ps:set node-js-app restart-policy unless-stopped -``` ------> Setting restart policy: unless-stopped -``` - -```shell # restart only on non-zero exit status -dokku ps:set-restart-policy node-js-app on-failure -``` +dokku ps:set node-js-app restart-policy on-failure -``` ------> Setting restart policy: on-failure -``` - -```shell # restart only on non-zero exit status up to 20 times -dokku ps:set-restart-policy node-js-app on-failure:20 -``` - -``` ------> Setting restart policy: on-failure:20 +dokku ps:set node-js-app restart-policy on-failure:20 ``` Restart policies have no bearing on server reboot, and Dokku will always attempt to restart your applications at that point unless they were manually stopped. diff --git a/plugins/common/common.go b/plugins/common/common.go index 50085d29f..be763e55a 100644 --- a/plugins/common/common.go +++ b/plugins/common/common.go @@ -9,6 +9,7 @@ import ( "reflect" "regexp" "sort" + "strconv" "strings" "unicode" @@ -379,6 +380,16 @@ func ToBool(s string) bool { return s == "true" } +// ToInt returns an int value for a given string +func ToInt(s string, defaultValue int) int { + i, err := strconv.Atoi(s) + if err != nil { + return defaultValue + } + + return i +} + // UcFirst uppercases the first character in a string func UcFirst(str string) string { for i, v := range str { diff --git a/plugins/docker-options/dockeroptions.go b/plugins/docker-options/dockeroptions.go index a1d44e6b3..1a30faa25 100644 --- a/plugins/docker-options/dockeroptions.go +++ b/plugins/docker-options/dockeroptions.go @@ -4,13 +4,39 @@ import ( "bufio" "fmt" "os" - "path/filepath" "sort" "strings" - - "github.com/dokku/dokku/plugins/common" ) +func SetDockerOptionForPhases(appName string, phases []string, name string, value string) error { + for _, phase := range phases { + if err := touchPhaseFile(appName, phase); err != nil { + return err + } + + options, err := GetDockerOptionsForPhase(appName, phase) + if err != nil { + return err + } + + newOptions := []string{} + for _, option := range options { + if strings.HasPrefix(option, fmt.Sprintf("--%s=", name)) { + continue + } + + newOptions = append(newOptions, option) + } + + newOptions = append(newOptions, fmt.Sprintf("--%s=%s", name, value)) + sort.Strings(newOptions) + if err = writeDockerOptionsForPhase(appName, phase, newOptions); err != nil { + return err + } + } + return nil +} + // AddDockerOptionToPhases adds docker option to specified phases func AddDockerOptionToPhases(appName string, phases []string, option string) error { for _, phase := range phases { @@ -58,29 +84,3 @@ func GetDockerOptionsForPhase(appName string, phase string) ([]string, error) { return options, nil } - -func getPhaseFilePath(appName string, phase string) string { - return filepath.Join(common.MustGetEnv("DOKKU_ROOT"), appName, "DOCKER_OPTIONS_"+strings.ToUpper(phase)) -} - -func touchPhaseFile(appName string, phase string) error { - phaseFilePath := getPhaseFilePath(appName, phase) - - _, err := os.Stat(phaseFilePath) - if !os.IsNotExist(err) { - return nil - } - - file, err := os.Create(phaseFilePath) - if err != nil { - return fmt.Errorf("Unable to create docker options phase file %s.%s: %s", appName, phase, err.Error()) - } - defer file.Close() - - return nil -} - -func writeDockerOptionsForPhase(appName string, phase string, options []string) error { - phaseFilePath := getPhaseFilePath(appName, phase) - return common.WriteSliceToFile(phaseFilePath, options) -} diff --git a/plugins/docker-options/functions.go b/plugins/docker-options/functions.go new file mode 100644 index 000000000..d7f1919e6 --- /dev/null +++ b/plugins/docker-options/functions.go @@ -0,0 +1,36 @@ +package dockeroptions + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/dokku/dokku/plugins/common" +) + +func getPhaseFilePath(appName string, phase string) string { + return filepath.Join(common.AppRoot(appName), "DOCKER_OPTIONS_"+strings.ToUpper(phase)) +} + +func touchPhaseFile(appName string, phase string) error { + phaseFilePath := getPhaseFilePath(appName, phase) + + _, err := os.Stat(phaseFilePath) + if !os.IsNotExist(err) { + return nil + } + + file, err := os.Create(phaseFilePath) + if err != nil { + return fmt.Errorf("Unable to create docker options phase file %s.%s: %s", appName, phase, err.Error()) + } + defer file.Close() + + return nil +} + +func writeDockerOptionsForPhase(appName string, phase string, options []string) error { + phaseFilePath := getPhaseFilePath(appName, phase) + return common.WriteSliceToFile(phaseFilePath, options) +} diff --git a/plugins/ps/.gitignore b/plugins/ps/.gitignore new file mode 100644 index 000000000..3922c71ac --- /dev/null +++ b/plugins/ps/.gitignore @@ -0,0 +1,11 @@ +/commands +/subcommands/* +/triggers/* +/triggers +/proxy-* +/install +/post-* +/report +/pre-* +/procfile-* +/app-restart diff --git a/plugins/ps/Makefile b/plugins/ps/Makefile new file mode 100644 index 000000000..004322380 --- /dev/null +++ b/plugins/ps/Makefile @@ -0,0 +1,6 @@ +SUBCOMMANDS = subcommands/inspect subcommands/rebuild subcommands/rebuildall subcommands/report subcommands/restart subcommands/restart-policy subcommands/restartall subcommands/restore subcommands/retire subcommands/scale subcommands/set subcommands/start subcommands/startall subcommands/stop subcommands/stopall +TRIGGERS = triggers/app-restart triggers/install triggers/post-app-clone triggers/post-app-rename triggers/post-create triggers/post-delete triggers/post-stop triggers/pre-deploy triggers/procfile-extract triggers/procfile-get-command triggers/procfile-remove triggers/report +BUILD = commands subcommands triggers +PLUGIN_NAME = ps + +include ../../common.mk diff --git a/plugins/ps/app-restart b/plugins/ps/app-restart deleted file mode 100755 index 064bfd845..000000000 --- a/plugins/ps/app-restart +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" -source "$PLUGIN_AVAILABLE_PATH/ps/functions" - -trigger-ps-app-restart() { - declare desc="ps app-restart plugin trigger" - declare trigger="app-restart" - declare APP="$1" - - ps_restart "$APP" -} - -trigger-ps-app-restart "$@" diff --git a/plugins/ps/commands b/plugins/ps/commands deleted file mode 100755 index 2f2be37cc..000000000 --- a/plugins/ps/commands +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash -[[ " help ps:help " == *" $1 "* ]] || exit "$DOKKU_NOT_IMPLEMENTED_EXIT" -source "$PLUGIN_AVAILABLE_PATH/ps/help-functions" -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x - -case "$1" in - help | ps:help) - cmd-ps-help "$@" - ;; - - *) - exit "$DOKKU_NOT_IMPLEMENTED_EXIT" - ;; - -esac diff --git a/plugins/ps/core-post-deploy b/plugins/ps/core-post-deploy deleted file mode 100755 index b789ddc20..000000000 --- a/plugins/ps/core-post-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_AVAILABLE_PATH/config/functions" -source "$PLUGIN_AVAILABLE_PATH/ps/functions" - -trigger-ps-core-post-deploy() { - declare desc="ps core-post-deploy plugin trigger" - declare trigger="core-post-deploy" - declare APP="$1" - - remove_procfile "$APP" - DOKKU_QUIET_OUTPUT=1 config_set --no-restart "$APP" DOKKU_APP_RESTORE=1 -} - -trigger-ps-core-post-deploy "$@" diff --git a/plugins/ps/functions.go b/plugins/ps/functions.go new file mode 100644 index 000000000..45fcfa151 --- /dev/null +++ b/plugins/ps/functions.go @@ -0,0 +1,287 @@ +package ps + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/dokku/dokku/plugins/common" + dockeroptions "github.com/dokku/dokku/plugins/docker-options" +) + +func canScaleApp(appName string) bool { + return !common.FileExists(getScalefilePath(appName)) +} + +func extractProcfile(appName, image string, procfilePath string) error { + if err := removeProcfile(appName); err != nil { + return err + } + + destination := getProcfilePath(appName) + common.CopyFromImage(appName, image, "Procfile", destination) + if !common.FileExists(destination) { + common.LogInfo1Quiet("No Procfile found in app image") + return nil + } + + common.LogInfo1Quiet("App Procfile file found") + checkCmd := common.NewShellCmd(strings.Join([]string{ + "procfile-util", + "check", + "--procfile", + destination, + }, " ")) + var stderr bytes.Buffer + checkCmd.ShowOutput = false + checkCmd.Command.Stderr = &stderr + _, err := checkCmd.Output() + + if err != nil { + return fmt.Errorf(strings.TrimSpace(stderr.String())) + } + + return nil +} + + +func extractOrGenerateScalefile(appName string, image string) error { + destination := getScalefilePath(appName) + extracted := getScalefileExtractedPath(appName) + if err := common.CopyFromImage(appName, image, "DOKKU_SCALE", destination); err != nil { + os.Remove(extracted) + os.Remove(destination) + } else if err := common.CopyFile(destination, extracted); err != nil { + return err + } + + if common.FileExists(destination) { + return nil + } + + common.LogInfo1Quiet("DOKKU_SCALE file not found in app image. Generating one based on Procfile...") + if err := generateScalefile(appName, destination); err != nil { + common.LogDebug(fmt.Sprintf("Error: %s", err.Error())) + return err + } + return nil +} + +func generateScalefile(appName string, destination string) error { + procfilePath := getProcfilePath(appName) + content := []string{"web=1"} + if !common.FileExists(procfilePath) { + return common.WriteSliceToFile(destination, content) + } + + lines, err := common.FileToSlice(procfilePath) + if err != nil { + return common.WriteSliceToFile(destination, content) + } + + content = []string{} + for _, line := range lines { + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + parts := strings.Split(line, ":") + count := 0 + if parts[0] == "web" { + count = 1 + } + + content = append(content, fmt.Sprintf("%s=%d", parts[0], count)) + } + + return common.WriteSliceToFile(destination, content) +} + +func getProcfileCommand(procfilePath string, processType string, port int) (string, error) { + shellCmd := common.NewShellCmd(strings.Join([]string{ + "procfile-util", + "show", + "--procfile", + procfilePath, + "--process-type", + processType, + "--default-port", + strconv.Itoa(port), + }, " ")) + var stderr bytes.Buffer + shellCmd.ShowOutput = false + shellCmd.Command.Stderr = &stderr + b, err := shellCmd.Output() + + if err != nil { + return "", fmt.Errorf(strings.TrimSpace(stderr.String())) + } + + return strings.TrimSpace(string(b[:])), nil +} + +func getProcfilePath(appName string) string { + directory := filepath.Join(common.MustGetEnv("DOKKU_LIB_ROOT"), "data", "ps", appName) + return filepath.Join(directory, "Procfile") +} + +func getRestartPolicy(appName string) (string, error) { + options, err := dockeroptions.GetDockerOptionsForPhase(appName, "deploy") + if err != nil { + return "", err + } + + for _, option := range options { + if strings.HasPrefix(option, "--restart=") { + return strings.TrimPrefix(option, "--restart="), nil + } + } + + return "", nil +} + +func getRunningState(appName string) string { + scheduler := common.GetAppScheduler(appName) + b, _ := common.PlugnTriggerOutput("scheduler-app-status", []string{scheduler, appName}...) + return strings.Split(strings.TrimSpace(string(b[:])), " ")[1] +} + +func getScalefilePath(appName string) string { + return filepath.Join(common.AppRoot(appName), "DOKKU_SCALE") +} + +func getScalefileExtractedPath(appName string) string { + return filepath.Join(common.AppRoot(appName), "DOKKU_SCALE.extracted") +} + +func isValidRestartPolicy(policy string) bool { + if policy == "" { + return false + } + + validRestartPolicies := map[string]bool{ + "no": true, + "always": true, + "unless-stopped": true, + "on-failure": true, + } + + if _, ok := validRestartPolicies[policy]; ok { + return true + } + + return strings.HasPrefix(policy, "on-failure:") +} + +func processesInProcfile(procfilePath string) (map[string]bool, error) { + processes := map[string]bool{} + + shellCmd := common.NewShellCmd(strings.Join([]string{ + "procfile-util", + "list", + "--procfile", + procfilePath, + }, " ")) + var stderr bytes.Buffer + shellCmd.ShowOutput = false + shellCmd.Command.Stderr = &stderr + b, err := shellCmd.Output() + + if err != nil { + return processes, fmt.Errorf(strings.TrimSpace(stderr.String())) + } + + for _, s := range strings.Split(strings.TrimSpace(string(b[:])), "\n") { + processes[s] = true + } + + return processes, nil +} + +func removeProcfile(appName string) error { + procfile := getProcfilePath(appName) + if !common.FileExists(procfile) { + return nil + } + + return os.Remove(procfile) +} + +func updateScalefile(appName string, processTuples []string) error { + procfilePath := getProcfilePath(appName) + scalefilePath := getScalefilePath(appName) + lines, err := common.FileToSlice(scalefilePath) + if err != nil { + return err + } + + processTypes, err := processesInProcfile(procfilePath) + if err != nil { + return err + } + + newLines := []string{} + for processType := range processTypes { + found := false + for _, line := range lines { + if strings.HasPrefix(line, fmt.Sprintf("%s=", processType)) { + newLines = append(newLines, line) + found = true + break + } + } + + if !found { + newLines = append(newLines, fmt.Sprintf("%s=0", processType)) + } + } + + scale := []string{} + for _, processTuple := range processTuples { + s := strings.Split(processTuple, "=") + if len(s) == 1 { + return fmt.Errorf("Missing count for process type %s", processTuple) + } + + processType := s[0] + count, err := strconv.Atoi(s[1]) + if err != nil { + return fmt.Errorf("Invalid count for process type %s", s[0]) + } + + if _, ok := processTypes[processType]; !ok { + if count != 0 && len(processTypes) == 0 { + return fmt.Errorf("%s is not a valid process name to scale up", processType) + } + } + + scale = append(scale, fmt.Sprintf("%s=%d", processType, count)) + } + + for _, line := range newLines { + s := strings.Split(line, "=") + processType := s[0] + + found := false + for _, s := range scale { + if strings.HasPrefix(s, fmt.Sprintf("%s=", processType)) { + found = true + break + } + } + + if !found { + scale = append(scale, line) + } + + } + + if err := common.WriteSliceToFile(scalefilePath, scale); err != nil { + return err + } + + return err +} diff --git a/plugins/ps/go.mod b/plugins/ps/go.mod new file mode 100644 index 000000000..8cb14fd8a --- /dev/null +++ b/plugins/ps/go.mod @@ -0,0 +1,15 @@ +module github.com/dokku/dokku/plugins/ps + +go 1.14 + +require ( + github.com/codeskyblue/go-sh v0.0.0-20190412065543-76bd3d59ff27 + github.com/dokku/dokku/plugins/common v0.0.0-00010101000000-000000000000 + github.com/dokku/dokku/plugins/config v0.0.0-00010101000000-000000000000 + github.com/dokku/dokku/plugins/docker-options v0.0.0-00010101000000-000000000000 + github.com/ryanuber/columnize v1.1.2-0.20190319233515-9e6335e58db3 +) + +replace github.com/dokku/dokku/plugins/common => ../common +replace github.com/dokku/dokku/plugins/config => ../config +replace github.com/dokku/dokku/plugins/docker-options => ../docker-options diff --git a/plugins/ps/go.sum b/plugins/ps/go.sum new file mode 100644 index 000000000..8b973f334 --- /dev/null +++ b/plugins/ps/go.sum @@ -0,0 +1,10 @@ +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/dokku/dokku v0.21.4 h1:WG0hyPzNqKqoQXuI7H/N+cWX9B7mAxTW6Yop40BjCQk= +github.com/joho/godotenv v1.2.0 h1:vGTvz69FzUFp+X4/bAkb0j5BoLC+9bpqTWY8mjhA9pc= +github.com/joho/godotenv v1.2.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +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/ps/help-functions b/plugins/ps/help-functions deleted file mode 100755 index df2913503..000000000 --- a/plugins/ps/help-functions +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x - -cmd-ps-help() { - declare desc="help command" - declare CMD="$1" - local plugin_name="ps" - local plugin_description="Manage container processes" - - if [[ "$CMD" == "$plugin_name:help" ]]; then - echo -e "Usage: dokku $plugin_name[:COMMAND]" - echo '' - echo "$plugin_description" - echo '' - echo 'Additional commands:' - fn-help-content | sort | column -c2 -t -s, - elif [[ $(ps -o command= $PPID) == *"--all"* ]]; then - fn-help-content - else - cat <, Displays a sanitized version of docker inspect for an app - ps:scale = [=...], Get/Set how many instances of a given process to run - ps:start , Start app container(s) - ps:startall, Starts all apps via command line - ps:stop , Stop app container(s) - ps:stopall, Stop all app container(s) - ps:rebuild , Rebuild an app from source - ps:rebuildall, Rebuild all apps from source - ps:report [] [], Displays a process report for one or more apps - ps:restart , Restart app container(s) - ps:restartall, Restart all deployed app containers - ps:restore, Start previously running apps e.g. after reboot - ps:restart-policy , Shows the restart-policy for an app - ps:set-restart-policy , Sets app restart-policy -help_content -} diff --git a/plugins/ps/install b/plugins/ps/install deleted file mode 100755 index 4d57b90be..000000000 --- a/plugins/ps/install +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" -source "$PLUGIN_AVAILABLE_PATH/docker-options/functions" -source "$PLUGIN_AVAILABLE_PATH/ps/functions" - -trigger-ps-install() { - declare desc="set the default restart policy for all applications if there is not one already set" - declare trigger="install" - local APPS="$(dokku_apps)" - local APP - - mkdir -p "${DOKKU_LIB_ROOT}/data/ps" - chown -R "${DOKKU_SYSTEM_USER}:${DOKKU_SYSTEM_GROUP}" "${DOKKU_LIB_ROOT}/data/ps" - - for APP in $APPS; do - local RESTART_POLICIES=$(get_restart_policies "$(get_phase_file_path "deploy")") - if [[ -z "$RESTART_POLICIES" ]]; then - local passed_phases=(deploy) - add_passed_docker_option passed_phases[@] "--restart=on-failure:10" - fi - done -} - -trigger-ps-install "$@" diff --git a/plugins/ps/post-app-clone b/plugins/ps/post-app-clone deleted file mode 100755 index 92fdefd7b..000000000 --- a/plugins/ps/post-app-clone +++ /dev/null @@ -1,18 +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" - -trigger-ps-post-app-clone() { - declare desc="ps post-app-clone plugin trigger" - declare trigger="post-app-clone" - declare OLD_APP="$1" NEW_APP="$2" - - if [[ "$SKIP_REBUILD" != "true" ]]; then - ps_rebuild "$NEW_APP" - fi -} - -trigger-ps-post-app-clone "$@" diff --git a/plugins/ps/post-app-rename b/plugins/ps/post-app-rename deleted file mode 100755 index 7175b517b..000000000 --- a/plugins/ps/post-app-rename +++ /dev/null @@ -1,18 +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" - -trigger-ps-post-app-rename() { - declare desc="ps post-app-rename plugin trigger" - declare trigger="post-app-rename" - declare OLD_APP="$1" NEW_APP="$2" - - if [[ "$SKIP_REBUILD" != "true" ]]; then - ps_rebuild "$NEW_APP" - fi -} - -trigger-ps-post-app-rename "$@" diff --git a/plugins/ps/post-create b/plugins/ps/post-create deleted file mode 100755 index 14efa77a8..000000000 --- a/plugins/ps/post-create +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" -source "$PLUGIN_AVAILABLE_PATH/docker-options/functions" -source "$PLUGIN_AVAILABLE_PATH/ps/functions" - -trigger-ps-post-create() { - declare desc="ps post-create plugin trigger" - declare trigger="post-create" - local APP="$1" - - 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/post-extract b/plugins/ps/post-extract deleted file mode 100755 index ac329ccfd..000000000 --- a/plugins/ps/post-extract +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" - -trigger-ps-post-extract() { - declare desc="ps post-extract plugin trigger" - declare trigger="post-extract" - declare APP="$1" TMP_WORK_DIR="$2" REV="$3" - - if [[ ! -f "$TMP_WORK_DIR/Procfile" ]]; then - return - fi - - pushd "$TMP_WORK_DIR" >/dev/null - for process_type in $(procfile-util list); do - if echo "$process_type" | grep -vqE '^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$'; then - dokku_log_fail "Invalid process type name ($process_type)" - fi - done - popd >/dev/null -} - -trigger-ps-post-extract "$@" diff --git a/plugins/ps/post-stop b/plugins/ps/post-stop deleted file mode 100755 index 4d091c180..000000000 --- a/plugins/ps/post-stop +++ /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_AVAILABLE_PATH/config/functions" -source "$PLUGIN_AVAILABLE_PATH/ps/functions" - -trigger-ps-post-stop() { - declare desc="ps post-stop plugin trigger" - declare trigger="post-stop" - declare APP="$1" - - DOKKU_QUIET_OUTPUT=1 config_set --no-restart "$APP" DOKKU_APP_RESTORE=0 -} - -trigger-ps-post-stop "$@" diff --git a/plugins/ps/pre-deploy b/plugins/ps/pre-deploy deleted file mode 100755 index 4e4e85db8..000000000 --- a/plugins/ps/pre-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_AVAILABLE_PATH/ps/functions" - -trigger-ps-pre-deploy() { - declare desc="ps pre-deploy plugin trigger" - declare trigger="pre-deploy" - declare APP="$1" IMAGE_TAG="$2" - - extract_procfile "$APP" "$IMAGE_TAG" - generate_scale_file "$APP" "$IMAGE_TAG" -} - -trigger-ps-pre-deploy "$@" diff --git a/plugins/ps/procfile-extract b/plugins/ps/procfile-extract deleted file mode 100755 index 74a2ec218..000000000 --- a/plugins/ps/procfile-extract +++ /dev/null @@ -1,25 +0,0 @@ -#!/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 deleted file mode 100755 index 00c96d6a6..000000000 --- a/plugins/ps/procfile-get-command +++ /dev/null @@ -1,19 +0,0 @@ -#!/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 deleted file mode 100755 index 28b1ced70..000000000 --- a/plugins/ps/procfile-remove +++ /dev/null @@ -1,22 +0,0 @@ -#!/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/plugins/ps/ps.go b/plugins/ps/ps.go new file mode 100644 index 000000000..a4e7b1db9 --- /dev/null +++ b/plugins/ps/ps.go @@ -0,0 +1,148 @@ +package ps + +import ( + "fmt" + + "github.com/dokku/dokku/plugins/common" +) + +const RUN_IN_SERIAL = -1 + +type ParallelCommand func(string) error + +var ( + // DefaultProperties is a map of all valid ps properties with corresponding default property values + DefaultProperties = map[string]string{ + "restart-policy": "on-failure:10", + } +) + +func RunCommandAgainstAllApps(command ParallelCommand, commandName string, runInSerial bool, parallelCount int) error { + if runInSerial && parallelCount != RUN_IN_SERIAL { + common.LogWarn("Ignoring --parallel value and running in serial mode") + } + + if runInSerial { + return RunCommandAgainstAllAppsSerially(command, commandName) + } + + return RunCommandAgainstAllAppsInParallel(command, commandName, parallelCount) +} + +// TODO: implement me +func RunCommandAgainstAllAppsInParallel(command ParallelCommand, commandName string, parallelCount int) error { + return nil +} + +func RunCommandAgainstAllAppsSerially(command ParallelCommand, commandName string) error { + apps, err := common.DokkuApps() + if err != nil { + common.LogWarn(err.Error()) + return nil + } + + errorCount := 0 + for _, appName := range apps { + if err = command(appName); err != nil { + errorCount += 1 + } + } + + if errorCount > 0 { + return fmt.Errorf("%s command returned %d errors", commandName, errorCount) + } + + return nil +} + +// Rebuild rebuilds app from base image +func Rebuild(appName string) error { + return common.PlugnTrigger("receive-app", []string{appName}...) +} + +// ReportSingleApp is an internal function that displays the ps report for one or more apps +// TODO: implement me +func ReportSingleApp(appName, infoFlag string) error { + if err := common.VerifyAppName(appName); err != nil { + return err + } + + + policy, _ := getRestartPolicy(appName) + if policy == "" { + policy = DefaultProperties["restart-policy"] + } + + infoFlags := map[string]string{ + "--ps-restart-policy": policy, + } + + trimPrefix := false + uppercaseFirstCharacter := true + return common.ReportSingleApp("ps", appName, infoFlag, infoFlags, trimPrefix, uppercaseFirstCharacter) +} + +// Restart restarts the app +func Restart(appName string) error { + if !common.IsDeployed(appName) { + common.LogWarn(fmt.Sprintf("App %s has not been deployed", appName)) + return nil + } + + return common.PlugnTrigger("release-and-deploy", []string{appName}...) +} + +// Start starts the app +func Start(appName string) error { + imageTag, _ := common.GetRunningImageTag(appName) + + if !common.IsDeployed(appName) { + common.LogWarn(fmt.Sprintf("App %s has not been deployed", appName)) + return nil + } + + if err := common.PlugnTrigger("pre-start", []string{appName}...); err != nil { + return fmt.Errorf("Failure in pre-start hook: %s", err) + } + + runningState := getRunningState(appName) + + if runningState == "mixed" { + common.LogWarn("App is running in mixed mode, releasing") + } + + if runningState != "true" { + if err := common.PlugnTrigger("release-and-deploy", []string{appName, imageTag}...); err != nil { + return err + } + } else { + common.LogWarn(fmt.Sprintf("App %s already running", appName)) + } + + if err := common.PlugnTrigger("proxy-build-config", []string{appName}...); err != nil { + return err + } + + return nil +} + +// Stop stops the app +func Stop(appName string) error { + if !common.IsDeployed(appName) { + common.LogWarn(fmt.Sprintf("App %s has not been deployed", appName)) + return nil + } + + common.LogQuiet(fmt.Sprintf("Stopping %s", appName)) + scheduler := common.GetAppScheduler(appName) + + if err := common.PlugnTrigger("scheduler-stop", []string{scheduler, appName}...); err != nil { + return err + } + + if err := common.PlugnTrigger("post-stop", []string{appName}...); err != nil { + return err + } + + return nil +} diff --git a/plugins/ps/report b/plugins/ps/report deleted file mode 100755 index 273d4431c..000000000 --- a/plugins/ps/report +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash -source "$PLUGIN_AVAILABLE_PATH/ps/internal-functions" -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x - -cmd-ps-report-single "$@" diff --git a/plugins/ps/src/commands/commands.go b/plugins/ps/src/commands/commands.go new file mode 100644 index 000000000..259073d65 --- /dev/null +++ b/plugins/ps/src/commands/commands.go @@ -0,0 +1,62 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strconv" + "strings" + + "github.com/dokku/dokku/plugins/common" +) + +const ( + helpHeader = `Usage: dokku ps[:COMMAND] + +Manage app processes + +Additional commands:` + + helpContent = ` + ps:inspect , Displays a sanitized version of docker inspect for an app + ps:scale = [=...], Get/Set how many instances of a given process to run + ps:start [--parallel count] [--serial] [--all|], Start an app + ps:stop [--parallel count] [--serial] [--all|], Stop an app + ps:rebuild [--parallel count] [--serial] [--all|], Rebuilds an app from source + ps:report [] [], Displays a process report for one or more apps + ps:restart [--parallel count] [--serial] [--all|], Restart an app + ps:restore, Start previously running apps e.g. after reboot +` +) + +func main() { + flag.Usage = usage + flag.Parse() + + cmd := flag.Arg(0) + switch cmd { + case "ps", "ps:help": + usage() + case "help": + command := common.NewShellCmd(fmt.Sprintf("ps -o command= %d", os.Getppid())) + command.ShowOutput = false + output, err := command.Output() + + if err == nil && strings.Contains(string(output), "--all") { + fmt.Println(helpContent) + } else { + fmt.Print("\n ps, Manage app processes\n") + } + default: + dokkuNotImplementExitCode, err := strconv.Atoi(os.Getenv("DOKKU_NOT_IMPLEMENTED_EXIT")) + if err != nil { + fmt.Println("failed to retrieve DOKKU_NOT_IMPLEMENTED_EXIT environment variable") + dokkuNotImplementExitCode = 10 + } + os.Exit(dokkuNotImplementExitCode) + } +} + +func usage() { + common.CommandUsage(helpHeader, helpContent) +} diff --git a/plugins/ps/src/subcommands/subcommands.go b/plugins/ps/src/subcommands/subcommands.go new file mode 100644 index 000000000..cecfc6e75 --- /dev/null +++ b/plugins/ps/src/subcommands/subcommands.go @@ -0,0 +1,92 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strings" + + "github.com/dokku/dokku/plugins/common" + "github.com/dokku/dokku/plugins/ps" +) + +// main entrypoint to all subcommands +func main() { + parts := strings.Split(os.Args[0], "/") + subcommand := parts[len(parts)-1] + + var err error + switch subcommand { + case "inspect": + args := flag.NewFlagSet("ps:inspect", flag.ExitOnError) + args.Parse(os.Args[2:]) + appName := args.Arg(0) + err = ps.CommandInspect(appName) + case "rebuild": + args := flag.NewFlagSet("ps:rebuild", flag.ExitOnError) + allApps := args.Bool("all", false, "--all: restart all apps") + parallelCount := args.Int("parallel", ps.RUN_IN_SERIAL, "--parallel: number of apps to restart in parallel, -1 to match cpu count") + runInSerial := args.Bool("serial", false, "--serial: restart apps serially") + args.Parse(os.Args[2:]) + appName := args.Arg(0) + err = ps.CommandRebuild(appName, *allApps, *runInSerial, *parallelCount) + case "report": + args := flag.NewFlagSet("ps:report", flag.ExitOnError) + args.Parse(os.Args[2:]) + appName := args.Arg(0) + infoFlag := args.Arg(1) + err = ps.CommandReport(appName, infoFlag) + case "restart": + args := flag.NewFlagSet("ps:restart", flag.ExitOnError) + allApps := args.Bool("all", false, "--all: restart all apps") + parallelCount := args.Int("parallel", ps.RUN_IN_SERIAL, "--parallel: number of apps to restart in parallel, -1 to match cpu count") + runInSerial := args.Bool("serial", false, "--serial: restart apps serially") + args.Parse(os.Args[2:]) + appName := args.Arg(0) + err = ps.CommandRestart(appName, *allApps, *runInSerial, *parallelCount) + case "restore": + args := flag.NewFlagSet("ps:restore", flag.ExitOnError) + args.Parse(os.Args[2:]) + appName := args.Arg(0) + err = ps.CommandRestore(appName) + case "retire": + args := flag.NewFlagSet("ps:retire", flag.ExitOnError) + args.Parse(os.Args[2:]) + err = ps.CommandRetire() + case "scale": + args := flag.NewFlagSet("ps:scale", flag.ExitOnError) + args.Parse(os.Args[2:]) + appName := args.Arg(0) + _, processTuples := common.ShiftString(args.Args()) + err = ps.CommandScale(appName, processTuples) + case "set": + args := flag.NewFlagSet("ps:set", flag.ExitOnError) + args.Parse(os.Args[2:]) + appName := args.Arg(0) + property := args.Arg(1) + value := args.Arg(2) + err = ps.CommandSet(appName, property, value) + case "start": + args := flag.NewFlagSet("ps:start", flag.ExitOnError) + allApps := args.Bool("all", false, "--all: restart all apps") + parallelCount := args.Int("parallel", ps.RUN_IN_SERIAL, "--parallel: number of apps to restart in parallel, -1 to match cpu count") + runInSerial := args.Bool("serial", false, "--serial: restart apps serially") + args.Parse(os.Args[2:]) + appName := args.Arg(0) + err = ps.CommandStart(appName, *allApps, *runInSerial, *parallelCount) + case "stop": + args := flag.NewFlagSet("ps:stop", flag.ExitOnError) + allApps := args.Bool("all", false, "--all: restart all apps") + parallelCount := args.Int("parallel", ps.RUN_IN_SERIAL, "--parallel: number of apps to restart in parallel, -1 to match cpu count") + runInSerial := args.Bool("serial", false, "--serial: restart apps serially") + args.Parse(os.Args[2:]) + appName := args.Arg(0) + err = ps.CommandStop(appName, *allApps, *runInSerial, *parallelCount) + default: + common.LogFail(fmt.Sprintf("Invalid plugin subcommand call: %s", subcommand)) + } + + if err != nil { + common.LogFail(err.Error()) + } +} diff --git a/plugins/ps/src/triggers/triggers.go b/plugins/ps/src/triggers/triggers.go new file mode 100644 index 000000000..3ded508cb --- /dev/null +++ b/plugins/ps/src/triggers/triggers.go @@ -0,0 +1,80 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strings" + + "github.com/dokku/dokku/plugins/common" + "github.com/dokku/dokku/plugins/ps" +) + +// 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 "app-restart": + appName := flag.Arg(0) + err = ps.TriggerAppRestart(appName) + case "core-post-deploy": + appName := flag.Arg(0) + err = ps.TriggerCorePostDeploy(appName) + case "install": + err = ps.TriggerInstall() + case "post-app-clone": + oldAppName := flag.Arg(0) + newAppName := flag.Arg(1) + err = ps.TriggerPostAppClone(oldAppName, newAppName) + case "post-app-rename": + oldAppName := flag.Arg(0) + newAppName := flag.Arg(1) + err = ps.TriggerPostAppRename(oldAppName, newAppName) + case "post-create": + appName := flag.Arg(0) + err = ps.TriggerPostCreate(appName) + case "post-delete": + appName := flag.Arg(0) + err = ps.TriggerPostDelete(appName) + case "post-extract": + appName := flag.Arg(0) + tmpWorkDir := flag.Arg(1) + err = ps.TriggerPostExtract(appName, tmpWorkDir) + case "post-stop": + appName := flag.Arg(0) + err = ps.TriggerPostStop(appName) + case "pre-deploy": + appName := flag.Arg(0) + imageTag := flag.Arg(1) + err = ps.TriggerPreDeploy(appName, imageTag) + case "procfile-extract": + appName := flag.Arg(0) + image := flag.Arg(1) + forceExtract := common.ToBool(flag.Arg(2)) + procfilePath := flag.Arg(3) + err = ps.TriggerProcfileExtract(appName, image, forceExtract, procfilePath) + case "procfile-get-command": + appName := flag.Arg(0) + processType := flag.Arg(1) + port := common.ToInt(flag.Arg(2), 5000) + procfilePath := flag.Arg(3) + err = ps.TriggerProcfileGetCommand(appName, processType, port, procfilePath) + case "procfile-remove": + appName := flag.Arg(0) + procfilePath := flag.Arg(1) + err = ps.TriggerProcfileRemove(appName, procfilePath) + case "report": + appName := flag.Arg(0) + err = ps.ReportSingleApp(appName, "") + default: + common.LogFail(fmt.Sprintf("Invalid plugin trigger call: %s", trigger)) + } + + if err != nil { + common.LogFail(err.Error()) + } +} diff --git a/plugins/ps/subcommands.go b/plugins/ps/subcommands.go new file mode 100644 index 000000000..21a27739a --- /dev/null +++ b/plugins/ps/subcommands.go @@ -0,0 +1,215 @@ +package ps + +import ( + "errors" + "fmt" + "os" + "sort" + "strings" + + dockeroptions "github.com/dokku/dokku/plugins/docker-options" + + "github.com/dokku/dokku/plugins/common" + "github.com/ryanuber/columnize" +) + +func CommandInspect(appName string) error { + if appName == "" { + return errors.New("Please specify an app to run the command on") + } + + if err := common.VerifyAppName(appName); err != nil { + return err + } + + scheduler := common.GetAppScheduler(appName) + return common.PlugnTrigger("scheduler-inspect", []string{scheduler, appName}...) +} + +func CommandRebuild(appName string, allApps bool, runInSerial bool, parallelCount int) error { + if allApps { + return RunCommandAgainstAllApps(Rebuild, "rebuild", runInSerial, parallelCount) + } + + if appName == "" { + return errors.New("Please specify an app to run the command on") + } + + if err := common.VerifyAppName(appName); err != nil { + return err + } + + return Rebuild(appName) +} + +// CommandReport displays a ps report for one or more apps +func CommandReport(appName string, infoFlag string) error { + if strings.HasPrefix(appName, "--") { + infoFlag = appName + appName = "" + } + + if len(appName) == 0 { + apps, err := common.DokkuApps() + if err != nil { + return err + } + for _, appName := range apps { + if err := ReportSingleApp(appName, infoFlag); err != nil { + return err + } + } + return nil + } + + return ReportSingleApp(appName, infoFlag) +} + +func CommandRestart(appName string, allApps bool, runInSerial bool, parallelCount int) error { + if allApps { + return RunCommandAgainstAllApps(Restart, "restart", runInSerial, parallelCount) + } + + if appName == "" { + return errors.New("Please specify an app to run the command on") + } + + if err := common.VerifyAppName(appName); err != nil { + return err + } + + return Restart(appName) +} + +// TODO: implement me +func CommandRestore(appName string) error { + return nil +} + +// TODO: implement me +func CommandRetire() error { + return nil +} + +// TODO: implement me +func CommandScale(appName string, processTuples []string) error { + if appName == "" { + return errors.New("Please specify an app to run the command on") + } + + if err := common.VerifyAppName(appName); err != nil { + return err + } + + procfilePath := getProcfilePath(appName) + scalefilePath := getScalefilePath(appName) + if !common.FileExists(procfilePath) { + image := common.GetAppImageRepo(appName) + extractProcfile(appName, image, procfilePath) + } + + if !common.FileExists(scalefilePath) { + err := common.SuppressOutput(func() error { + return generateScalefile(appName, scalefilePath) + }) + if err != nil { + return err + } + } else if common.FileExists(procfilePath) { + if err := updateScalefile(appName, []string{}); err != nil { + return err + } + } + + if len(processTuples) == 0 { + lines, err := common.FileToSlice(scalefilePath) + if err != nil { + return err + } + + common.LogInfo1Quiet(fmt.Sprintf("Scaling for %s", appName)) + config := columnize.DefaultConfig() + config.Delim = "=" + config.Glue = ": " + config.Prefix = " " + config.Empty = "" + + content := []string{} + if os.Getenv("DOKKU_QUIET_OUTPUT") == "" { + content = append(content, "proctype=qty","--------=---") + } + + sort.Strings(lines) + for _, line := range lines { + content = append(content, line) + } + + for _, line := range content { + s := strings.Split(line, "=") + common.Log(fmt.Sprintf("%s %s", common.RightPad(fmt.Sprintf("%s:", s[0]), 5, " "), s[1])) + } + } else { + if !canScaleApp(appName) { + return fmt.Errorf("App %s contains DOKKU_SCALE file and cannot be manually scaled", appName) + } + + if err := updateScalefile(appName, processTuples); err != nil { + return err + } + + imageTag, err := common.GetRunningImageTag(appName) + if err != nil { + return err + } + return common.PlugnTrigger("release-and-deploy", []string{appName, imageTag}...) + } + + return nil +} + +// TODO: implement me +func CommandSet(appName string, property string, value string) error { + if property == "restart-policy" { + if !isValidRestartPolicy(value) { + return errors.New("Invalid restart-policy specified") + } + + common.LogInfo2Quiet(fmt.Sprintf("Setting %s to %s", property, value)) + return dockeroptions.SetDockerOptionForPhases(appName, []string{"deploy"}, "restart", value) + } + + common.CommandPropertySet("ps", appName, property, value, DefaultProperties) + return nil +} + +func CommandStart(appName string, allApps bool, runInSerial bool, parallelCount int) error { + if allApps { + return RunCommandAgainstAllApps(Start, "start", runInSerial, parallelCount) + } + + if appName == "" { + return errors.New("Please specify an app to run the command on") + } + + if err := common.VerifyAppName(appName); err != nil { + return err + } + + return Start(appName) +} + +func CommandStop(appName string, allApps bool, runInSerial bool, parallelCount int) error { + if allApps { + return RunCommandAgainstAllApps(Stop, "stop", runInSerial, parallelCount) + } + + if appName == "" { + return errors.New("Please specify an app to run the command on") + } + + if err := common.VerifyAppName(appName); err != nil { + return err + } + + return Stop(appName) +} diff --git a/plugins/ps/subcommands/default b/plugins/ps/subcommands/default deleted file mode 100755 index 55742f338..000000000 --- a/plugins/ps/subcommands/default +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_AVAILABLE_PATH/ps/help-functions" - -cmd-ps-help "ps:help" diff --git a/plugins/ps/subcommands/inspect b/plugins/ps/subcommands/inspect deleted file mode 100755 index acea3e2b9..000000000 --- a/plugins/ps/subcommands/inspect +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash -source "$PLUGIN_AVAILABLE_PATH/ps/internal-functions" -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x - -cmd-ps-inspect "$@" diff --git a/plugins/ps/subcommands/rebuild b/plugins/ps/subcommands/rebuild deleted file mode 100755 index 22d1ba2dd..000000000 --- a/plugins/ps/subcommands/rebuild +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" -source "$PLUGIN_AVAILABLE_PATH/ps/functions" - -cmd-ps-rebuild() { - declare desc="rebuilds an app from source" - declare cmd="ps:rebuild" - [[ "$1" == "$cmd" ]] && shift 1 - declare APP="$1" - - [[ -z "$APP" ]] && dokku_log_fail "Please specify an app to run the command on" - - if (is_deployed "$APP"); then - ps_rebuild "$APP" - else - dokku_log_warn "App $APP has not been deployed" - fi -} - -cmd-ps-rebuild "$@" diff --git a/plugins/ps/subcommands/rebuildall b/plugins/ps/subcommands/rebuildall deleted file mode 100755 index 98fe32095..000000000 --- a/plugins/ps/subcommands/rebuildall +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" -source "$PLUGIN_AVAILABLE_PATH/ps/internal-functions" - -cmd-ps-rebuildall() { - declare desc="rebuilds all apps from source" - declare cmd="ps:rebuildall" - [[ "$1" == "$cmd" ]] && shift 1 - - fn-ps-parallel-cmd "rebuild" -} - -cmd-ps-rebuildall "$@" diff --git a/plugins/ps/subcommands/report b/plugins/ps/subcommands/report deleted file mode 100755 index 0a967fd0d..000000000 --- a/plugins/ps/subcommands/report +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash -source "$PLUGIN_AVAILABLE_PATH/ps/internal-functions" -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x - -cmd-ps-report "$@" diff --git a/plugins/ps/subcommands/restart b/plugins/ps/subcommands/restart deleted file mode 100755 index 582fe82ca..000000000 --- a/plugins/ps/subcommands/restart +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" - -cmd-ps-restart() { - declare desc="restarts app via command line" - declare cmd="ps:restart" - [[ "$1" == "$cmd" ]] && shift 1 - declare APP="$1" - - [[ -z "$APP" ]] && dokku_log_fail "Please specify an app to run the command on" - plugn trigger app-restart "$APP" -} - -cmd-ps-restart "$@" diff --git a/plugins/ps/subcommands/restart-policy b/plugins/ps/subcommands/restart-policy deleted file mode 100755 index fe30684c5..000000000 --- a/plugins/ps/subcommands/restart-policy +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" -source "$PLUGIN_AVAILABLE_PATH/docker-options/functions" -source "$PLUGIN_AVAILABLE_PATH/ps/functions" - -cmd-ps-restart-policy() { - declare desc="shows the restart-policy for an app" - declare cmd="ps:restart-policy" - [[ "$1" == "$cmd" ]] && shift 1 - declare APP="$1" - local passed_phases="deploy" - - [[ -z "$APP" ]] && dokku_log_fail "Please specify an app to run the command on" - verify_app_name "$APP" - - dokku_log_info2_quiet "$APP restart-policy:" - get_restart_policies "$(get_phase_file_path "$passed_phases")" -} - -cmd-ps-restart-policy "$@" diff --git a/plugins/ps/subcommands/restartall b/plugins/ps/subcommands/restartall deleted file mode 100755 index cb9200e10..000000000 --- a/plugins/ps/subcommands/restartall +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" -source "$PLUGIN_AVAILABLE_PATH/ps/internal-functions" - -cmd-ps-restartall() { - declare desc="restarts all apps via command line" - declare cmd="ps:restartall" - [[ "$1" == "$cmd" ]] && shift 1 - - fn-ps-parallel-cmd "restart" -} - -cmd-ps-restartall "$@" diff --git a/plugins/ps/subcommands/restore b/plugins/ps/subcommands/restore deleted file mode 100755 index d3c25df41..000000000 --- a/plugins/ps/subcommands/restore +++ /dev/null @@ -1,40 +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" -source "$PLUGIN_AVAILABLE_PATH/ps/internal-functions" - -cmd-ps-restore() { - declare desc="starts all apps with DOKKU_APP_RESTORE not set to 0 via command line" - declare cmd="ps:restore" - [[ "$1" == "$cmd" ]] && shift 1 - declare APP="$1" - local DOKKU_SCHEDULER - - DOKKU_SCHEDULER=$(get_app_scheduler "$APP") - plugn trigger pre-restore "$DOKKU_SCHEDULER" "$APP" - - if [[ -n "$APP" ]]; then - plugn trigger proxy-clear-config "$APP" - - if ! (is_deployed "$APP"); then - dokku_log_warn "App $APP has not been deployed" - return - fi - - DOKKU_APP_RESTORE="$(config_get "$APP" DOKKU_APP_RESTORE || true)" - if [[ $DOKKU_APP_RESTORE != 0 ]]; then - ps_restore "$APP" || dokku_log_warn "dokku ps:restore ${APP} failed" - fi - else - for APP in $(dokku_apps); do - plugn trigger proxy-clear-config "$APP" - done - - fn-ps-parallel-cmd "restore" - fi -} - -cmd-ps-restore "$@" diff --git a/plugins/ps/subcommands/retire b/plugins/ps/subcommands/retire deleted file mode 100755 index d32e6b8af..000000000 --- a/plugins/ps/subcommands/retire +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_AVAILABLE_PATH/config/functions" -source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" - -cmd-ps-retire() { - declare desc="ensures old containers are retired" - declare cmd="ps:retire" - [[ "$1" == "$cmd" ]] && shift 1 - - local LOCK_FILE="${DOKKU_LIB_ROOT}/data/ps/retire" - local DOKKU_SCHEDULER=$(config_get "--global" DOKKU_SCHEDULER || echo "docker-local") - - acquire_advisory_lock "$LOCK_FILE" "exclusive" "" "Failed to acquire ps:retire lock" - plugn trigger scheduler-retire "$DOKKU_SCHEDULER" - release_advisory_lock "$LOCK_FILE" -} - -cmd-ps-retire "$@" diff --git a/plugins/ps/subcommands/scale b/plugins/ps/subcommands/scale deleted file mode 100755 index 5a5a6e4bc..000000000 --- a/plugins/ps/subcommands/scale +++ /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_AVAILABLE_PATH/ps/functions" - -cmd-ps-scale() { - declare desc="sets app scaling config via command line" - declare cmd="ps:scale" - [[ "$1" == "$cmd" ]] && shift 1 - declare APP="$1" - - [[ -z "$APP" ]] && dokku_log_fail "Please specify an app to run the command on" - ps_scale "$@" -} - -cmd-ps-scale "$@" diff --git a/plugins/ps/subcommands/set-restart-policy b/plugins/ps/subcommands/set-restart-policy deleted file mode 100755 index b18495e2e..000000000 --- a/plugins/ps/subcommands/set-restart-policy +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" -source "$PLUGIN_AVAILABLE_PATH/docker-options/functions" -source "$PLUGIN_AVAILABLE_PATH/ps/functions" - -cmd-ps-set-restart-policy() { - declare desc="sets app restart-policy" - declare cmd="ps:set-restart-policy" - [[ "$1" == "$cmd" ]] && shift 1 - declare APP="$1" RESTART_POLICY="$2" - - [[ -z "$APP" ]] && dokku_log_fail "Please specify an app to run the command on" - verify_app_name "$APP" - local RE_RESTART_POLICY="^(no|unless-stopped|always|on-failure(:[1-9][0-9]*)?)$" - - if [[ ! "$RESTART_POLICY" =~ $RE_RESTART_POLICY ]]; then - dokku_log_fail "Please specify an valid restart policy matching the following regex: ${RE_RESTART_POLICY}" - fi - - local RESTART_POLICIES=$(get_raw_restart_policies "$(get_phase_file_path "deploy")") - local passed_phases=(deploy) - - for policy in $RESTART_POLICIES; do - remove_passed_docker_option passed_phases[@] "$policy" - done - - dokku_log_info1_quiet "Setting restart policy: ${RESTART_POLICY}" - local passed_phases=(deploy) - add_passed_docker_option passed_phases[@] "--restart=${RESTART_POLICY}" -} - -cmd-ps-set-restart-policy "$@" diff --git a/plugins/ps/subcommands/start b/plugins/ps/subcommands/start deleted file mode 100755 index 8be6cae51..000000000 --- a/plugins/ps/subcommands/start +++ /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_AVAILABLE_PATH/ps/functions" - -cmd-ps-start() { - declare desc="starts app via command line" - declare cmd="ps:start" - [[ "$1" == "$cmd" ]] && shift 1 - declare APP="$1" - - [[ -z "$APP" ]] && dokku_log_fail "Please specify an app to run the command on" - ps_start "$APP" -} - -cmd-ps-start "$@" diff --git a/plugins/ps/subcommands/startall b/plugins/ps/subcommands/startall deleted file mode 100755 index 69726033c..000000000 --- a/plugins/ps/subcommands/startall +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" -source "$PLUGIN_AVAILABLE_PATH/ps/internal-functions" - -cmd-ps-startall() { - declare desc="starts all apps via command line" - declare cmd="ps:startall" - [[ "$1" == "$cmd" ]] && shift 1 - - fn-ps-parallel-cmd "start" -} - -cmd-ps-startall "$@" diff --git a/plugins/ps/subcommands/stop b/plugins/ps/subcommands/stop deleted file mode 100755 index 37c0fec66..000000000 --- a/plugins/ps/subcommands/stop +++ /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_AVAILABLE_PATH/ps/functions" - -cmd-ps-stop() { - declare desc="stops app via command line" - declare cmd="ps:stop" - [[ "$1" == "$cmd" ]] && shift 1 - declare APP="$1" - - [[ -z "$APP" ]] && dokku_log_fail "Please specify an app to run the command on" - ps_stop "$APP" -} - -cmd-ps-stop "$@" diff --git a/plugins/ps/subcommands/stopall b/plugins/ps/subcommands/stopall deleted file mode 100755 index f05be6a45..000000000 --- a/plugins/ps/subcommands/stopall +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" -source "$PLUGIN_AVAILABLE_PATH/ps/internal-functions" - -cmd-ps-stopall() { - declare desc="stops all apps via command line" - declare cmd="ps:stopall" - [[ "$1" == "$cmd" ]] && shift 1 - - fn-ps-parallel-cmd "stop" -} - -cmd-ps-stopall "$@" diff --git a/plugins/ps/triggers.go b/plugins/ps/triggers.go new file mode 100644 index 000000000..201ea22ef --- /dev/null +++ b/plugins/ps/triggers.go @@ -0,0 +1,193 @@ +package ps + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/dokku/dokku/plugins/common" + "github.com/dokku/dokku/plugins/config" + dockeroptions "github.com/dokku/dokku/plugins/docker-options" + sh "github.com/codeskyblue/go-sh" +) + +func TriggerAppRestart(appName string) error { + return Restart(appName) +} + +func TriggerCorePostDeploy(appName string) error { + if err := removeProcfile(appName); err != nil { + return err + } + + entries := map[string]string{ + "DOKKU_APP_RESTORE": "1", + } + + return common.SuppressOutput(func() error { + return config.SetMany(appName, entries, false) + }) +} + +func TriggerInstall() error { + directory := filepath.Join(common.MustGetEnv("DOKKU_LIB_ROOT"), "data", "ps") + if err := os.MkdirAll(directory, 0755); err != nil { + return err + } + + if err := common.SetPermissions(directory, 0755); err != nil { + return err + } + + apps, err := common.DokkuApps() + if err != nil { + return nil + } + + for _, appName := range apps { + policies, err := getRestartPolicy(appName) + if err != nil { + return err + } + + if len(policies) != 0 { + continue + } + + if err := dockeroptions.AddDockerOptionToPhases(appName, []string{"deploy"}, "--restart=on-failure:10"); err != nil { + common.LogWarn(err.Error()) + } + } + + return nil +} + +func TriggerPostAppClone(oldAppName string, newAppName string) error { + if os.Getenv("SKIP_REBUILD") == "true" { + return nil + } + + return Rebuild(newAppName) +} + +func TriggerPostAppRename(oldAppName string, newAppName string) error { + if os.Getenv("SKIP_REBUILD") == "true" { + return nil + } + + return Rebuild(newAppName) +} + +func TriggerPostCreate(appName string) error { + if err := dockeroptions.AddDockerOptionToPhases(appName, []string{"deploy"}, "--restart=on-failure:10"); err != nil { + return err + } + + directory := filepath.Join(common.MustGetEnv("DOKKU_LIB_ROOT"), "data", "ps", appName) + if err := os.MkdirAll(directory, 0755); err != nil { + return err + } + + if err := common.SetPermissions(directory, 0755); err != nil { + return err + } + + return nil +} + +// TriggerPostDelete destroys the ps properties for a given app container +func TriggerPostDelete(appName string) error { + return common.PropertyDestroy("ps", appName) +} + +func TriggerPostExtract(appName string, tempWorkDir string) error { + procfile := filepath.Join(tempWorkDir, "Procfile") + if !common.FileExists(procfile) { + return nil + } + + b, err := sh.Command("procfile-util", "check", "-P", procfile).CombinedOutput() + if err != nil { + return fmt.Errorf(strings.TrimSpace(string(b[:]))) + } + return nil +} + +func TriggerPostStop(appName string) error { + entries := map[string]string{ + "DOKKU_APP_RESTORE": "0", + } + + return common.SuppressOutput(func() error { + return config.SetMany(appName, entries, false) + }) +} + +func TriggerPreDeploy(appName string, imageTag string) error { + image := common.GetAppImageRepo(appName) + removeProcfile(appName) + + procfilePath := getProcfilePath(appName) + if err := extractProcfile(appName, image, procfilePath); err != nil { + return err + } + if err := extractOrGenerateScalefile(appName, imageTag); err != nil { + return err + } + + return nil +} + +func TriggerProcfileExtract(appName string, image string, forceExtract bool, procfilePath string) error { + if procfilePath == "" { + directory := filepath.Join(common.MustGetEnv("DOKKU_LIB_ROOT"), "data", "ps", appName) + if err := os.MkdirAll(directory, 0755); err != nil { + return err + } + + if err := common.SetPermissions(directory, 0755); err != nil { + return err + } + + procfilePath = getProcfilePath(appName) + } + + if forceExtract { + if err := common.PlugnTrigger("procfile-remove", []string{appName, procfilePath}...); err != nil { + return err + } + } + + return extractProcfile(appName, image, procfilePath) +} + +func TriggerProcfileGetCommand(appName string, processType string, port int, procfilePath string) error { + if procfilePath == "" { + procfilePath = getProcfilePath(appName) + } + + command, err := getProcfileCommand(procfilePath, processType, port) + if err != nil { + return err + } + + if command != "" { + fmt.Printf("%s\n", command) + } + + return nil +} + +func TriggerProcfileRemove(appName string, procfilePath string) error { + if procfilePath == "" { + procfilePath = getProcfilePath(appName) + } + + if !common.FileExists(procfilePath) { + return nil + } + + os.Remove(procfilePath) + return nil +} diff --git a/tests/unit/ps-general.bats b/tests/unit/ps-general.bats index 4b2fc7220..99bc2bea6 100644 --- a/tests/unit/ps-general.bats +++ b/tests/unit/ps-general.bats @@ -16,13 +16,13 @@ teardown() { run /bin/bash -c "dokku ps" echo "output: $output" echo "status: $status" - assert_output_contains "Manage container processes" + assert_output_contains "Manage app processes" help_output="$output" run /bin/bash -c "dokku ps:help" echo "output: $output" echo "status: $status" - assert_output_contains "Manage container processes" + assert_output_contains "Manage app processes" assert_output "$help_output" } @@ -64,7 +64,7 @@ EOF run /bin/bash -c "dokku --quiet ps:scale $TEST_APP" output=$(echo "$output" | tr -s " ") echo "output: ($output)" - assert_output $'cron: 0 \ncustom: 0 \nrelease: 0 \nweb: 1 \nworker: 0 ' + assert_output $'cron: 0\ncustom: 0\nrelease: 0\nweb: 1\nworker: 0' pushd $TMP echo scaletest: sleep infinity >> Procfile @@ -74,28 +74,27 @@ EOF run /bin/bash -c "dokku --quiet ps:scale $TEST_APP" output=$(echo "$output" | tr -s " ") echo "output: ($output)" - assert_output $'cron: 0 \ncustom: 0 \nrelease: 0 \nweb: 1 \nworker: 0 \nscaletest: 0 ' + assert_output $'cron: 0\ncustom: 0\nrelease: 0\nscaletest: 0\nweb: 1\nworker: 0' popd rm -rf "$TMP" } @test "(ps:restart-policy) default policy" { - run /bin/bash -c "dokku --quiet ps:restart-policy $TEST_APP" + run /bin/bash -c "dokku --quiet ps:report $TEST_APP --ps-restart-policy" echo "output: $output" echo "status: $status" assert_output "on-failure:10" } - -@test "(ps:restart-policy) ps:set-restart-policy, ps:restart-policy" { +@test "(ps:restart-policy) ps:set restart-policy" { for policy in no unless-stopped always on-failure on-failure:20; do - run /bin/bash -c "dokku ps:set-restart-policy $TEST_APP $policy" + run /bin/bash -c "dokku ps:set $TEST_APP restart-policy $policy" echo "output: $output" echo "status: $status" assert_success - run /bin/bash -c "dokku --quiet ps:restart-policy $TEST_APP" + run /bin/bash -c "dokku --quiet ps:report $TEST_APP --ps-restart-policy" echo "output: $output" echo "status: $status" assert_output "$policy" @@ -104,12 +103,12 @@ EOF @test "(ps:restart-policy) deployed policy" { test_restart_policy="on-failure:20" - run /bin/bash -c "dokku ps:set-restart-policy $TEST_APP $test_restart_policy" + run /bin/bash -c "dokku ps:set $TEST_APP restart-policy $test_restart_policy" echo "output: $output" echo "status: $status" assert_success - run /bin/bash -c "dokku --quiet ps:restart-policy $TEST_APP" + run /bin/bash -c "dokku --quiet ps:report $TEST_APP --ps-restart-policy" echo "output: $output" echo "status: $status" assert_output "$test_restart_policy"