2020-11-01 15:53:53 -05:00
|
|
|
package ps
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
|
|
|
|
|
"github.com/dokku/dokku/plugins/common"
|
|
|
|
|
)
|
|
|
|
|
|
2020-11-18 13:24:43 -05:00
|
|
|
// RunInSerial is the default value for whether to run a command in parallel or not
|
|
|
|
|
// and defaults to -1 (false)
|
2020-11-21 17:31:13 -05:00
|
|
|
const RunInSerial = 0
|
2020-11-01 15:53:53 -05:00
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
// DefaultProperties is a map of all valid ps properties with corresponding default property values
|
|
|
|
|
DefaultProperties = map[string]string{
|
2025-03-09 04:26:47 -04:00
|
|
|
"restart-policy": "on-failure:10",
|
|
|
|
|
"procfile-path": "",
|
|
|
|
|
"stop-timeout-seconds": "30",
|
2020-11-01 15:53:53 -05:00
|
|
|
}
|
2021-01-04 00:50:14 -05:00
|
|
|
|
|
|
|
|
// GlobalProperties is a map of all valid global ps properties
|
2021-03-21 21:45:46 -04:00
|
|
|
GlobalProperties = map[string]bool{
|
2025-03-09 04:26:47 -04:00
|
|
|
"procfile-path": true,
|
|
|
|
|
"stop-timeout-seconds": true,
|
2021-03-21 21:45:46 -04:00
|
|
|
}
|
2020-11-01 15:53:53 -05:00
|
|
|
)
|
|
|
|
|
|
2022-11-27 20:23:13 -05:00
|
|
|
// RetireLockFailed wraps error to distinguish between a normal error
|
|
|
|
|
// and an error where the retire lock could not be fetched
|
|
|
|
|
type RetireLockFailed struct {
|
|
|
|
|
Err *error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ExitCode returns an exit code to use in case this error bubbles
|
|
|
|
|
// up into an os.Exit() call
|
|
|
|
|
func (err *RetireLockFailed) ExitCode() int {
|
|
|
|
|
return 137
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Error returns a standard non-existent app error
|
|
|
|
|
func (err *RetireLockFailed) Error() string {
|
|
|
|
|
if err.Err != nil {
|
|
|
|
|
e := *err.Err
|
|
|
|
|
return fmt.Sprintf("Failed to acquire ps:retire lock: %s", e.Error())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return fmt.Sprintf("Failed to acquire ps:retire lock")
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-01 15:47:55 -04:00
|
|
|
// Formation contains scaling information for a given process type
|
2021-08-01 01:21:46 -04:00
|
|
|
type Formation struct {
|
|
|
|
|
ProcessType string `json:"process_type"`
|
|
|
|
|
Quantity int `json:"quantity"`
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-01 15:47:55 -04:00
|
|
|
// FormationSlice contains a slice of Formations that can be sorted
|
|
|
|
|
type FormationSlice []*Formation
|
|
|
|
|
|
2021-08-01 01:21:46 -04:00
|
|
|
func (d FormationSlice) Len() int {
|
|
|
|
|
return len(d)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (d FormationSlice) Swap(i, j int) {
|
|
|
|
|
d[i], d[j] = d[j], d[i]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (d FormationSlice) Less(i, j int) bool {
|
|
|
|
|
return d[i].ProcessType < d[j].ProcessType
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-01 15:53:53 -05:00
|
|
|
// Rebuild rebuilds app from base image
|
|
|
|
|
func Rebuild(appName string) error {
|
2024-03-14 02:19:03 -04:00
|
|
|
_, err := common.CallPlugnTrigger(common.PlugnTriggerInput{
|
|
|
|
|
Trigger: "receive-app",
|
|
|
|
|
Args: []string{appName},
|
|
|
|
|
StreamStdio: true,
|
|
|
|
|
})
|
|
|
|
|
return err
|
2020-11-01 15:53:53 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-05 00:02:46 -04:00
|
|
|
imageTag, err := common.GetRunningImageTag(appName, "")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if imageTag == "" {
|
|
|
|
|
common.LogWarn("No deployed-image-tag property saved, falling back to full release-and-deploy")
|
2024-03-14 02:19:03 -04:00
|
|
|
_, err := common.CallPlugnTrigger(common.PlugnTriggerInput{
|
|
|
|
|
Trigger: "release-and-deploy",
|
|
|
|
|
Args: []string{appName},
|
|
|
|
|
StreamStdio: true,
|
|
|
|
|
})
|
|
|
|
|
return err
|
2021-09-05 00:02:46 -04:00
|
|
|
}
|
|
|
|
|
|
2024-03-14 02:19:03 -04:00
|
|
|
_, err = common.CallPlugnTrigger(common.PlugnTriggerInput{
|
|
|
|
|
Trigger: "deploy",
|
|
|
|
|
Args: []string{appName, imageTag},
|
|
|
|
|
StreamStdio: true,
|
|
|
|
|
})
|
|
|
|
|
return err
|
2021-09-05 00:02:46 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RestartProcess restarts a process type within an app
|
|
|
|
|
func RestartProcess(appName string, processName string) error {
|
|
|
|
|
if !common.IsDeployed(appName) {
|
|
|
|
|
common.LogWarn(fmt.Sprintf("App %s has not been deployed", appName))
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
imageTag, err := common.GetRunningImageTag(appName, "")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if imageTag == "" {
|
|
|
|
|
common.LogWarn("No deployed-image-tag property saved, falling back to full release-and-deploy")
|
2024-03-14 02:19:03 -04:00
|
|
|
_, err := common.CallPlugnTrigger(common.PlugnTriggerInput{
|
|
|
|
|
Trigger: "release-and-deploy",
|
|
|
|
|
Args: []string{appName},
|
|
|
|
|
StreamStdio: true,
|
|
|
|
|
})
|
|
|
|
|
return err
|
2021-09-05 00:02:46 -04:00
|
|
|
}
|
|
|
|
|
|
2024-03-14 02:19:03 -04:00
|
|
|
_, err = common.CallPlugnTrigger(common.PlugnTriggerInput{
|
|
|
|
|
Trigger: "deploy",
|
|
|
|
|
Args: []string{appName, imageTag, processName},
|
|
|
|
|
StreamStdio: true,
|
|
|
|
|
})
|
|
|
|
|
return err
|
2020-11-01 15:53:53 -05:00
|
|
|
}
|
|
|
|
|
|
2020-11-21 20:37:54 -05:00
|
|
|
// Restore ensures an app that should be running is running on boot
|
|
|
|
|
func Restore(appName string) error {
|
|
|
|
|
scheduler := common.GetAppScheduler(appName)
|
2024-03-14 02:19:03 -04:00
|
|
|
_, err := common.CallPlugnTrigger(common.PlugnTriggerInput{
|
|
|
|
|
Trigger: "scheduler-pre-restore",
|
|
|
|
|
Args: []string{scheduler, appName},
|
|
|
|
|
StreamStdio: true,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
2022-11-22 23:18:45 -05:00
|
|
|
return fmt.Errorf("Error running scheduler-pre-restore: %s", err)
|
2020-11-21 20:37:54 -05:00
|
|
|
}
|
|
|
|
|
|
2020-12-19 18:19:07 -05:00
|
|
|
common.LogInfo1("Clearing potentially invalid proxy configuration")
|
2024-03-14 02:19:03 -04:00
|
|
|
_, err = common.CallPlugnTrigger(common.PlugnTriggerInput{
|
|
|
|
|
Trigger: "proxy-clear-config",
|
|
|
|
|
Args: []string{appName},
|
|
|
|
|
StreamStdio: true,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
2022-01-28 20:28:43 -05:00
|
|
|
common.LogWarn(fmt.Sprintf("Error clearing proxy config: %s", err))
|
2020-11-21 20:37:54 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !common.IsDeployed(appName) {
|
|
|
|
|
common.LogWarn(fmt.Sprintf("App %s has not been deployed", appName))
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-14 01:18:28 -04:00
|
|
|
results, _ := common.CallPlugnTrigger(common.PlugnTriggerInput{
|
|
|
|
|
Trigger: "config-get",
|
|
|
|
|
Args: []string{appName, "DOKKU_APP_RESTORE"},
|
|
|
|
|
})
|
|
|
|
|
restore := results.StdoutContents()
|
2020-11-21 20:37:54 -05:00
|
|
|
if restore == "0" {
|
2020-12-19 18:19:07 -05:00
|
|
|
common.LogWarn(fmt.Sprintf("Skipping ps:restore for %s as DOKKU_APP_RESTORE=%s", appName, restore))
|
2020-11-21 20:37:54 -05:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-19 18:19:07 -05:00
|
|
|
common.LogInfo1("Starting app")
|
2020-11-21 20:37:54 -05:00
|
|
|
return Start(appName)
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-01 15:53:53 -05:00
|
|
|
// Start starts the app
|
|
|
|
|
func Start(appName string) error {
|
2021-09-05 00:02:46 -04:00
|
|
|
imageTag, _ := common.GetRunningImageTag(appName, "")
|
2020-11-01 15:53:53 -05:00
|
|
|
|
|
|
|
|
if !common.IsDeployed(appName) {
|
|
|
|
|
common.LogWarn(fmt.Sprintf("App %s has not been deployed", appName))
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-14 02:19:03 -04:00
|
|
|
_, err := common.CallPlugnTrigger(common.PlugnTriggerInput{
|
|
|
|
|
Trigger: "pre-start",
|
|
|
|
|
Args: []string{appName},
|
|
|
|
|
StreamStdio: true,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
2020-11-01 15:53:53 -05:00
|
|
|
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")
|
2021-12-02 11:53:13 -05:00
|
|
|
} else if runningState == "false" {
|
|
|
|
|
common.LogWarn("App has been detected as not running, releasing")
|
2020-11-01 15:53:53 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if runningState != "true" {
|
2024-03-14 02:19:03 -04:00
|
|
|
_, err := common.CallPlugnTrigger(common.PlugnTriggerInput{
|
|
|
|
|
Trigger: "release-and-deploy",
|
|
|
|
|
Args: []string{appName, imageTag},
|
|
|
|
|
StreamStdio: true,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
2020-11-01 15:53:53 -05:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
common.LogWarn(fmt.Sprintf("App %s already running", appName))
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-14 02:19:03 -04:00
|
|
|
_, err = common.CallPlugnTrigger(common.PlugnTriggerInput{
|
|
|
|
|
Trigger: "proxy-build-config",
|
|
|
|
|
Args: []string{appName},
|
|
|
|
|
StreamStdio: true,
|
|
|
|
|
})
|
|
|
|
|
return err
|
2020-11-01 15:53:53 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-18 22:31:41 -05:00
|
|
|
common.LogInfo1Quiet(fmt.Sprintf("Stopping %s", appName))
|
2020-11-01 15:53:53 -05:00
|
|
|
scheduler := common.GetAppScheduler(appName)
|
|
|
|
|
|
2024-03-14 02:19:03 -04:00
|
|
|
_, err := common.CallPlugnTrigger(common.PlugnTriggerInput{
|
|
|
|
|
Trigger: "scheduler-stop",
|
|
|
|
|
Args: []string{scheduler, appName},
|
|
|
|
|
StreamStdio: true,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
2020-11-01 15:53:53 -05:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-14 02:19:03 -04:00
|
|
|
_, err = common.CallPlugnTrigger(common.PlugnTriggerInput{
|
|
|
|
|
Trigger: "post-stop",
|
|
|
|
|
Args: []string{appName},
|
|
|
|
|
StreamStdio: true,
|
|
|
|
|
})
|
|
|
|
|
return err
|
2020-11-01 15:53:53 -05:00
|
|
|
}
|