Files
dokku/plugins/ps/functions.go

429 lines
10 KiB
Go
Raw Normal View History

package ps
import (
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"github.com/dokku/dokku/plugins/common"
dockeroptions "github.com/dokku/dokku/plugins/docker-options"
)
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)
}
func getProcfileCommand(appName string, procfilePath string, processType string, port int) (string, error) {
if !common.FileExists(procfilePath) {
return "", errors.New("No procfile found")
}
configResult, err := common.CallPlugnTrigger(common.PlugnTriggerInput{
Trigger: "config-export",
Args: []string{appName, "false", "true", "envfile"},
})
if err != nil {
return "", err
}
// write the envfile to a temporary file
tempFile, err := os.CreateTemp("", "envfile-*.env")
if err != nil {
return "", err
}
defer os.Remove(tempFile.Name())
if _, err := tempFile.Write(configResult.StdoutBytes()); err != nil {
return "", err
}
if err := tempFile.Close(); err != nil {
return "", err
}
result, err := common.CallExecCommand(common.ExecCommandInput{
Command: "procfile-util",
Args: []string{"show", "--procfile", procfilePath, "--process-type", processType, "--default-port", strconv.Itoa(port), "--env-file", tempFile.Name()},
})
if err != nil {
return "", fmt.Errorf("Error running procfile-util: %s", err)
}
if result.ExitCode != 0 {
return "", fmt.Errorf("Error running procfile-util: %s", result.StderrContents())
}
return result.StdoutContents(), nil
}
func getProcfilePath(appName string) string {
directory := common.GetAppDataDirectory("ps", appName)
return filepath.Join(directory, "Procfile")
}
2022-12-29 01:21:35 -05:00
func getProcessSpecificProcfilePath(appName string) string {
existingProcfile := getProcfilePath(appName)
processSpecificProcfile := fmt.Sprintf("%s.%s", existingProcfile, os.Getenv("DOKKU_PID"))
if common.FileExists(processSpecificProcfile) {
return processSpecificProcfile
}
return existingProcfile
}
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)
results, _ := common.CallPlugnTrigger(common.PlugnTriggerInput{
Trigger: "scheduler-app-status",
Args: []string{scheduler, appName},
})
count := strings.Split(results.StdoutContents(), " ")[0]
2020-11-17 03:28:26 -05:00
return strconv.Atoi(count)
}
func getRunningState(appName string) string {
scheduler := common.GetAppScheduler(appName)
results, _ := common.CallPlugnTrigger(common.PlugnTriggerInput{
Trigger: "scheduler-app-status",
Args: []string{scheduler, appName},
})
return strings.Split(results.StdoutContents(), " ")[1]
}
func hasProcfile(appName string) bool {
procfilePath := getProcfilePath(appName)
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
}
return common.FileExists(procfilePath)
}
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 parseProcessTuples(processTuples []string) (FormationSlice, error) {
formations := FormationSlice{}
foundFormations := map[string]bool{}
for _, processTuple := range processTuples {
s := strings.SplitN(processTuple, "=", 2)
if len(s) == 1 {
return formations, fmt.Errorf("Missing count for process type %s", processTuple)
}
processType := strings.TrimSpace(s[0])
quantity, err := strconv.Atoi(strings.TrimSpace(s[1]))
if err != nil {
return formations, fmt.Errorf("Invalid count for process type %s", s[0])
}
if foundFormations[processType] {
continue
}
foundFormations[processType] = true
formations = append(formations, &Formation{
ProcessType: processType,
Quantity: quantity,
})
}
return formations, nil
}
func processesInProcfile(procfilePath string) (map[string]bool, error) {
processes := map[string]bool{}
result, err := common.CallExecCommand(common.ExecCommandInput{
Command: "procfile-util",
Args: []string{"list", "--procfile", procfilePath},
})
if err != nil {
return processes, fmt.Errorf("Error listing processes: %s", err)
}
if result.ExitCode != 0 {
return processes, fmt.Errorf("Error listing processes: %s", result.StderrContents())
}
for _, s := range strings.Split(result.StdoutContents(), "\n") {
processes[s] = true
}
return processes, nil
}
func getFormations(appName string) (FormationSlice, error) {
formations := FormationSlice{}
processTuples, err := common.PropertyListGet("ps", appName, "scale")
if err != nil {
return formations, err
}
oldProcessTuples, err := common.PropertyListGet("ps", appName, "scale.old")
if err != nil {
return formations, err
}
formations, err = parseProcessTuples(processTuples)
if err != nil {
return formations, err
}
oldFormations, err := parseProcessTuples(oldProcessTuples)
if err != nil {
return formations, err
}
foundProcessTypes := map[string]bool{}
for _, formation := range formations {
foundProcessTypes[formation.ProcessType] = true
}
for _, formation := range oldFormations {
if foundProcessTypes[formation.ProcessType] {
continue
}
foundProcessTypes[formation.ProcessType] = true
formations = append(formations, formation)
}
2024-02-11 21:12:25 -05:00
sort.Sort(formations)
return formations, nil
}
2020-11-21 21:07:20 -05:00
func restorePrep() error {
_, err := common.CallPlugnTrigger(common.PlugnTriggerInput{
Trigger: "proxy-clear-config",
Args: []string{"--all"},
StreamStdio: true,
})
if err != nil {
return fmt.Errorf("Error clearing proxy config: %s", err)
2020-11-21 21:07:20 -05:00
}
return nil
}
func scaleReport(appName string) error {
formations, err := getFormations(appName)
if err != nil {
return err
}
common.LogInfo1Quiet(fmt.Sprintf("Scaling for %s", appName))
content := []string{}
if os.Getenv("DOKKU_QUIET_OUTPUT") == "" {
content = append(content, "proctype=qty", "--------=---")
}
for _, formation := range formations {
content = append(content, fmt.Sprintf("%s=%d", formation.ProcessType, formation.Quantity))
}
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
}
// scaleSetInput is the input for the scaleSet function
type scaleSetInput struct {
// appName is the name of the app to scale
appName string
// skipDeploy is a flag to skip the deploy phase
skipDeploy bool
// clearExisting is a flag to clear the existing scale
clearExisting bool
// processTuples is a list of process tuples to scale
processTuples []string
// deployOnlyChanged is a flag to deploy only the changed formations
deployOnlyChanged bool
}
func scaleSet(input scaleSetInput) error {
existingFormations, err := getFormations(input.appName)
if err != nil {
return err
}
formations, err := parseProcessTuples(input.processTuples)
if err != nil {
return err
}
if err := updateScale(input.appName, input.clearExisting, formations); err != nil {
return err
}
if input.skipDeploy {
return nil
}
if !common.IsDeployed(input.appName) {
return nil
}
imageTag, err := common.GetRunningImageTag(input.appName, "")
if err != nil {
return err
}
changedFormations := FormationSlice{}
if input.deployOnlyChanged {
for _, formation := range formations {
isChanged := true
for _, existingFormation := range existingFormations {
if existingFormation.ProcessType == formation.ProcessType {
if existingFormation.Quantity == formation.Quantity {
isChanged = false
break
}
}
}
if isChanged {
changedFormations = append(changedFormations, formation)
}
}
} else {
changedFormations = formations
}
for _, formation := range changedFormations {
_, err := common.CallPlugnTrigger(common.PlugnTriggerInput{
Trigger: "deploy",
Args: []string{input.appName, imageTag, formation.ProcessType},
StreamStdio: true,
})
if err != nil {
return err
}
}
return nil
}
func updateScale(appName string, clearExisting bool, formationUpdates FormationSlice) error {
formations := FormationSlice{}
if !clearExisting {
processTuples, err := common.PropertyListGet("ps", appName, "scale")
if err != nil {
return err
}
formations, err = parseProcessTuples(processTuples)
if err != nil {
return err
}
}
validProcessTypes := make(map[string]bool)
if hasProcfile(appName) {
var err error
2022-12-29 01:13:00 -05:00
validProcessTypes, err = processesInProcfile(getProcessSpecificProcfilePath(appName))
if err != nil {
return err
}
}
foundProcessTypes := map[string]bool{}
updatedFormation := FormationSlice{}
for _, formation := range formationUpdates {
if hasProcfile(appName) && !validProcessTypes[formation.ProcessType] && formation.Quantity != 0 {
return fmt.Errorf("%s is not a valid process name to scale up", formation.ProcessType)
}
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
}
foundProcessTypes[formation.ProcessType] = true
updatedFormation = append(updatedFormation, &Formation{
ProcessType: formation.ProcessType,
Quantity: formation.Quantity,
})
}
for processType := range validProcessTypes {
if foundProcessTypes[processType] {
continue
}
updatedFormation = append(updatedFormation, &Formation{
ProcessType: processType,
Quantity: 0,
})
}
values := []string{}
oldValues := []string{}
2021-08-01 19:06:33 -04:00
for _, formation := range updatedFormation {
if !validProcessTypes[formation.ProcessType] && formation.Quantity == 0 {
oldValues = append(oldValues, fmt.Sprintf("%s=%d", formation.ProcessType, formation.Quantity))
continue
}
values = append(values, fmt.Sprintf("%s=%d", formation.ProcessType, formation.Quantity))
}
if err := common.PropertyListWrite("ps", appName, "scale.old", oldValues); err != nil {
return err
}
return common.PropertyListWrite("ps", appName, "scale", values)
}