package ps import ( "fmt" "github.com/dokku/dokku/plugins/common" ) // RunInSerial is the default value for whether to run a command in parallel or not // and defaults to -1 (false) const RunInSerial = 0 var ( // DefaultProperties is a map of all valid ps properties with corresponding default property values DefaultProperties = map[string]string{ "restart-policy": "on-failure:10", "procfile-path": "", "stop-timeout-seconds": "30", } // GlobalProperties is a map of all valid global ps properties GlobalProperties = map[string]bool{ "procfile-path": true, "stop-timeout-seconds": true, } ) // 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") } // Formation contains scaling information for a given process type type Formation struct { ProcessType string `json:"process_type"` Quantity int `json:"quantity"` } // FormationSlice contains a slice of Formations that can be sorted type FormationSlice []*Formation 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 } // Rebuild rebuilds app from base image func Rebuild(appName string) error { _, err := common.CallPlugnTrigger(common.PlugnTriggerInput{ Trigger: "receive-app", Args: []string{appName}, StreamStdio: true, }) return err } // 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 } 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") _, err := common.CallPlugnTrigger(common.PlugnTriggerInput{ Trigger: "release-and-deploy", Args: []string{appName}, StreamStdio: true, }) return err } _, err = common.CallPlugnTrigger(common.PlugnTriggerInput{ Trigger: "deploy", Args: []string{appName, imageTag}, StreamStdio: true, }) return err } // 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") _, err := common.CallPlugnTrigger(common.PlugnTriggerInput{ Trigger: "release-and-deploy", Args: []string{appName}, StreamStdio: true, }) return err } _, err = common.CallPlugnTrigger(common.PlugnTriggerInput{ Trigger: "deploy", Args: []string{appName, imageTag, processName}, StreamStdio: true, }) return err } // Restore ensures an app that should be running is running on boot func Restore(appName string) error { scheduler := common.GetAppScheduler(appName) _, err := common.CallPlugnTrigger(common.PlugnTriggerInput{ Trigger: "scheduler-pre-restore", Args: []string{scheduler, appName}, StreamStdio: true, }) if err != nil { return fmt.Errorf("Error running scheduler-pre-restore: %s", err) } common.LogInfo1("Clearing potentially invalid proxy configuration") _, err = common.CallPlugnTrigger(common.PlugnTriggerInput{ Trigger: "proxy-clear-config", Args: []string{appName}, StreamStdio: true, }) if err != nil { common.LogWarn(fmt.Sprintf("Error clearing proxy config: %s", err)) } if !common.IsDeployed(appName) { common.LogWarn(fmt.Sprintf("App %s has not been deployed", appName)) return nil } results, _ := common.CallPlugnTrigger(common.PlugnTriggerInput{ Trigger: "config-get", Args: []string{appName, "DOKKU_APP_RESTORE"}, }) restore := results.StdoutContents() if restore == "0" { common.LogWarn(fmt.Sprintf("Skipping ps:restore for %s as DOKKU_APP_RESTORE=%s", appName, restore)) return nil } common.LogInfo1("Starting app") return Start(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 } _, err := common.CallPlugnTrigger(common.PlugnTriggerInput{ Trigger: "pre-start", Args: []string{appName}, StreamStdio: true, }) if 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") } else if runningState == "false" { common.LogWarn("App has been detected as not running, releasing") } if runningState != "true" { _, err := common.CallPlugnTrigger(common.PlugnTriggerInput{ Trigger: "release-and-deploy", Args: []string{appName, imageTag}, StreamStdio: true, }) if err != nil { return err } } else { common.LogWarn(fmt.Sprintf("App %s already running", appName)) } _, err = common.CallPlugnTrigger(common.PlugnTriggerInput{ Trigger: "proxy-build-config", Args: []string{appName}, StreamStdio: true, }) return err } // 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.LogInfo1Quiet(fmt.Sprintf("Stopping %s", appName)) scheduler := common.GetAppScheduler(appName) _, err := common.CallPlugnTrigger(common.PlugnTriggerInput{ Trigger: "scheduler-stop", Args: []string{scheduler, appName}, StreamStdio: true, }) if err != nil { return err } _, err = common.CallPlugnTrigger(common.PlugnTriggerInput{ Trigger: "post-stop", Args: []string{appName}, StreamStdio: true, }) return err }