2020-11-01 15:53:53 -05:00
|
|
|
package ps
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
2020-11-01 17:42:32 -05:00
|
|
|
"errors"
|
2020-11-01 15:53:53 -05:00
|
|
|
"fmt"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
2020-11-18 00:33:01 -05:00
|
|
|
"sort"
|
2020-11-01 15:53:53 -05:00
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"github.com/dokku/dokku/plugins/common"
|
|
|
|
|
dockeroptions "github.com/dokku/dokku/plugins/docker-options"
|
2020-11-18 00:33:01 -05:00
|
|
|
"github.com/ryanuber/columnize"
|
2020-11-01 15:53:53 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func canScaleApp(appName string) bool {
|
2021-08-01 16:27:13 -04:00
|
|
|
canScale := common.PropertyGetDefault("ps", appName, "can-scale", "true")
|
|
|
|
|
return common.ToBool(canScale)
|
2020-11-01 15:53:53 -05:00
|
|
|
}
|
|
|
|
|
|
2020-11-18 02:18:25 -05:00
|
|
|
func getProcessStatus(appName string) map[string]string {
|
|
|
|
|
statuses := make(map[string]string)
|
|
|
|
|
containerFiles := common.ListFilesWithPrefix(common.AppRoot(appName), "CONTAINER.")
|
|
|
|
|
for _, filename := range containerFiles {
|
|
|
|
|
containerID := common.ReadFirstLine(filename)
|
|
|
|
|
containerStatus, _ := common.DockerInspect(containerID, "{{ .State.Status }}")
|
|
|
|
|
process := strings.TrimPrefix(filename, fmt.Sprintf("%s/CONTAINER.", common.AppRoot(appName)))
|
|
|
|
|
|
|
|
|
|
if containerStatus == "" {
|
|
|
|
|
containerStatus = "missing"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
statuses[process] = fmt.Sprintf("%s (CID: %s)", containerStatus, containerID[0:11])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return statuses
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-01 15:53:53 -05:00
|
|
|
func getProcfileCommand(procfilePath string, processType string, port int) (string, error) {
|
2020-11-01 17:42:32 -05:00
|
|
|
if !common.FileExists(procfilePath) {
|
|
|
|
|
return "", errors.New("No procfile found")
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-01 15:53:53 -05:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-17 03:28:26 -05:00
|
|
|
func getProcessCount(appName string) (int, error) {
|
|
|
|
|
scheduler := common.GetAppScheduler(appName)
|
|
|
|
|
b, _ := common.PlugnTriggerOutput("scheduler-app-status", []string{scheduler, appName}...)
|
|
|
|
|
count := strings.Split(strings.TrimSpace(string(b[:])), " ")[0]
|
|
|
|
|
return strconv.Atoi(count)
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-01 15:53:53 -05:00
|
|
|
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]
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-18 01:23:36 -05:00
|
|
|
func hasProcfile(appName string) bool {
|
|
|
|
|
procfilePath := getProcfilePath(appName)
|
2022-11-25 00:41:55 -05:00
|
|
|
if common.FileExists(fmt.Sprintf("%s.%s.missing", procfilePath, os.Getenv("DOKKU_PID"))) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if common.FileExists(fmt.Sprintf("%s.%s", procfilePath, os.Getenv("DOKKU_PID"))) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-18 01:23:36 -05:00
|
|
|
return common.FileExists(procfilePath)
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-01 15:53:53 -05:00
|
|
|
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:")
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-01 17:47:04 -04:00
|
|
|
func parseProcessTuples(processTuples []string) (FormationSlice, error) {
|
|
|
|
|
formations := FormationSlice{}
|
2020-11-18 02:18:25 -05:00
|
|
|
|
|
|
|
|
for _, processTuple := range processTuples {
|
|
|
|
|
s := strings.Split(processTuple, "=")
|
|
|
|
|
if len(s) == 1 {
|
2021-08-01 17:47:04 -04:00
|
|
|
return formations, fmt.Errorf("Missing count for process type %s", processTuple)
|
2020-11-18 02:18:25 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
processType := s[0]
|
2021-08-01 17:47:04 -04:00
|
|
|
quantity, err := strconv.Atoi(s[1])
|
2020-11-18 02:18:25 -05:00
|
|
|
if err != nil {
|
2021-08-01 17:47:04 -04:00
|
|
|
return formations, fmt.Errorf("Invalid count for process type %s", s[0])
|
2020-11-18 02:18:25 -05:00
|
|
|
}
|
|
|
|
|
|
2021-08-01 17:47:04 -04:00
|
|
|
formations = append(formations, &Formation{
|
|
|
|
|
ProcessType: processType,
|
|
|
|
|
Quantity: quantity,
|
|
|
|
|
})
|
2020-11-18 02:18:25 -05:00
|
|
|
}
|
|
|
|
|
|
2021-08-01 17:47:04 -04:00
|
|
|
return formations, nil
|
2020-11-18 02:18:25 -05:00
|
|
|
}
|
|
|
|
|
|
2020-11-01 15:53:53 -05:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-01 01:21:46 -04:00
|
|
|
func getFormations(appName string) (FormationSlice, error) {
|
2021-08-01 17:47:04 -04:00
|
|
|
formations := FormationSlice{}
|
|
|
|
|
processTuples, err := common.PropertyListGet("ps", appName, "scale")
|
2021-08-01 01:21:46 -04:00
|
|
|
if err != nil {
|
2021-08-01 17:47:04 -04:00
|
|
|
return formations, err
|
2021-08-01 01:21:46 -04:00
|
|
|
}
|
|
|
|
|
|
2021-08-01 17:47:04 -04:00
|
|
|
return parseProcessTuples(processTuples)
|
2021-08-01 01:21:46 -04:00
|
|
|
}
|
|
|
|
|
|
2020-11-21 21:07:20 -05:00
|
|
|
func restorePrep() error {
|
2022-01-28 20:18:36 -05:00
|
|
|
if err := common.PlugnTrigger("proxy-clear-config", []string{"--all"}...); err != nil {
|
|
|
|
|
return fmt.Errorf("Error clearing proxy config: %s", err)
|
2020-11-21 21:07:20 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-18 00:33:01 -05:00
|
|
|
func scaleReport(appName string) error {
|
2021-08-01 01:21:46 -04:00
|
|
|
formations, err := getFormations(appName)
|
2020-11-18 00:33:01 -05:00
|
|
|
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", "--------=---")
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-01 01:21:46 -04:00
|
|
|
sort.Sort(formations)
|
|
|
|
|
for _, formation := range formations {
|
|
|
|
|
content = append(content, fmt.Sprintf("%s=%d", formation.ProcessType, formation.Quantity))
|
2020-11-18 00:33:01 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, line := range content {
|
|
|
|
|
s := strings.Split(line, "=")
|
|
|
|
|
common.Log(fmt.Sprintf("%s %s", common.RightPad(fmt.Sprintf("%s:", s[0]), 5, " "), s[1]))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-01 16:20:19 -04:00
|
|
|
func scaleSet(appName string, skipDeploy bool, clearExisting bool, processTuples []string) error {
|
2021-08-01 17:47:04 -04:00
|
|
|
formations, err := parseProcessTuples(processTuples)
|
2020-11-18 01:23:36 -05:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-01 17:47:04 -04:00
|
|
|
if err := updateScale(appName, clearExisting, formations); err != nil {
|
2020-11-18 00:33:01 -05:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-01 16:07:26 -04:00
|
|
|
if skipDeploy {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-18 00:33:01 -05:00
|
|
|
if !common.IsDeployed(appName) {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-05 00:02:46 -04:00
|
|
|
imageTag, err := common.GetRunningImageTag(appName, "")
|
2020-11-18 00:33:01 -05:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2020-11-18 01:23:36 -05:00
|
|
|
|
2021-09-05 00:02:46 -04:00
|
|
|
for _, formation := range formations {
|
|
|
|
|
if err := common.PlugnTrigger("deploy", []string{appName, imageTag, formation.ProcessType}...); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
2020-11-18 01:23:36 -05:00
|
|
|
}
|
2020-11-01 15:53:53 -05:00
|
|
|
|
2022-11-25 00:41:55 -05:00
|
|
|
func getProcessSpecificProcfile(appName string) string {
|
|
|
|
|
existingProcfile := getProcfilePath(appName)
|
|
|
|
|
processSpecificProcfile := fmt.Sprintf("%s.%s", existingProcfile, os.Getenv("DOKKU_PID"))
|
|
|
|
|
if common.FileExists(processSpecificProcfile) {
|
|
|
|
|
return processSpecificProcfile
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return existingProcfile
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-01 17:47:04 -04:00
|
|
|
func updateScale(appName string, clearExisting bool, formationUpdates FormationSlice) error {
|
|
|
|
|
formations := FormationSlice{}
|
|
|
|
|
if !clearExisting {
|
|
|
|
|
processTuples, err := common.PropertyListGet("ps", appName, "scale")
|
|
|
|
|
if err != nil {
|
2020-11-18 01:23:36 -05:00
|
|
|
return err
|
2020-11-01 15:53:53 -05:00
|
|
|
}
|
|
|
|
|
|
2021-08-01 17:47:04 -04:00
|
|
|
formations, err = parseProcessTuples(processTuples)
|
2021-08-01 16:20:19 -04:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2020-11-18 01:23:36 -05:00
|
|
|
}
|
2020-11-01 15:53:53 -05:00
|
|
|
|
2020-11-18 01:23:36 -05:00
|
|
|
validProcessTypes := make(map[string]bool)
|
2022-11-25 00:41:55 -05:00
|
|
|
if hasProcfile(appName) {
|
2021-08-01 16:20:19 -04:00
|
|
|
var err error
|
2022-11-25 00:41:55 -05:00
|
|
|
validProcessTypes, err = processesInProcfile(getProcessSpecificProcfile(appName))
|
2020-11-18 01:23:36 -05:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
2020-11-01 15:53:53 -05:00
|
|
|
}
|
2020-11-18 01:23:36 -05:00
|
|
|
}
|
2020-11-01 15:53:53 -05:00
|
|
|
|
2022-11-25 00:41:55 -05:00
|
|
|
if common.FileExists(getProcessSpecificProcfile(appName)) {
|
|
|
|
|
common.CatFile(getProcessSpecificProcfile(appName))
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-01 17:47:04 -04:00
|
|
|
foundProcessTypes := map[string]bool{}
|
|
|
|
|
updatedFormation := FormationSlice{}
|
|
|
|
|
for _, formation := range formationUpdates {
|
2022-11-25 00:41:55 -05:00
|
|
|
if hasProcfile(appName) && !validProcessTypes[formation.ProcessType] && formation.Quantity != 0 {
|
2021-08-01 17:47:04 -04:00
|
|
|
return fmt.Errorf("%s is not a valid process name to scale up", formation.ProcessType)
|
2020-11-01 15:53:53 -05:00
|
|
|
}
|
2021-08-01 17:47:04 -04:00
|
|
|
|
|
|
|
|
foundProcessTypes[formation.ProcessType] = true
|
|
|
|
|
updatedFormation = append(updatedFormation, &Formation{
|
|
|
|
|
ProcessType: formation.ProcessType,
|
|
|
|
|
Quantity: formation.Quantity,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, formation := range formations {
|
2021-08-01 19:06:33 -04:00
|
|
|
if foundProcessTypes[formation.ProcessType] {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-01 17:47:04 -04:00
|
|
|
foundProcessTypes[formation.ProcessType] = true
|
|
|
|
|
updatedFormation = append(updatedFormation, &Formation{
|
|
|
|
|
ProcessType: formation.ProcessType,
|
|
|
|
|
Quantity: formation.Quantity,
|
|
|
|
|
})
|
2020-11-18 01:23:36 -05:00
|
|
|
}
|
2020-11-01 15:53:53 -05:00
|
|
|
|
2020-11-18 12:27:56 -05:00
|
|
|
for processType := range validProcessTypes {
|
2021-08-01 17:47:04 -04:00
|
|
|
if foundProcessTypes[processType] {
|
|
|
|
|
continue
|
2020-11-18 12:27:56 -05:00
|
|
|
}
|
|
|
|
|
|
2021-08-01 17:47:04 -04:00
|
|
|
updatedFormation = append(updatedFormation, &Formation{
|
|
|
|
|
ProcessType: processType,
|
|
|
|
|
Quantity: 0,
|
|
|
|
|
})
|
2020-11-01 15:53:53 -05:00
|
|
|
}
|
|
|
|
|
|
2021-08-01 17:47:04 -04:00
|
|
|
values := []string{}
|
2021-08-01 19:06:33 -04:00
|
|
|
for _, formation := range updatedFormation {
|
2022-11-25 00:41:55 -05:00
|
|
|
if !validProcessTypes[formation.ProcessType] && formation.Quantity == 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-01 17:47:04 -04:00
|
|
|
values = append(values, fmt.Sprintf("%s=%d", formation.ProcessType, formation.Quantity))
|
2020-11-01 15:53:53 -05:00
|
|
|
}
|
|
|
|
|
|
2021-08-01 17:47:04 -04:00
|
|
|
return common.PropertyListWrite("ps", appName, "scale", values)
|
2020-11-01 15:53:53 -05:00
|
|
|
}
|