Rather than attempting to always extract the Procfile, reference the one that was extracted by the last deploy. This fixes issues where 'docker container cp' may fail intermittently for lord knows what reason. Closes #4083
442 lines
11 KiB
Go
442 lines
11 KiB
Go
package ps
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/dokku/dokku/plugins/common"
|
|
dockeroptions "github.com/dokku/dokku/plugins/docker-options"
|
|
"github.com/ryanuber/columnize"
|
|
)
|
|
|
|
func canScaleApp(appName string) bool {
|
|
return !common.FileExists(getScalefileExtractedPath(appName))
|
|
}
|
|
|
|
func extractProcfile(appName, image string) error {
|
|
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)
|
|
previouslyExtracted := common.FileExists(extracted)
|
|
|
|
if err := common.CopyFromImage(appName, image, "DOKKU_SCALE", extracted); err != nil {
|
|
if previouslyExtracted {
|
|
os.Remove(destination)
|
|
}
|
|
os.Remove(extracted)
|
|
} else if err := common.CopyFile(extracted, destination); err != nil {
|
|
return err
|
|
}
|
|
|
|
if common.FileExists(destination) {
|
|
common.LogInfo1Quiet("DOKKU_SCALE file exists")
|
|
return updateScalefile(appName, make(map[string]int))
|
|
}
|
|
|
|
common.LogInfo1Quiet("DOKKU_SCALE file not found in app image. Generating one based on Procfile...")
|
|
if err := updateScalefile(appName, make(map[string]int)); err != nil {
|
|
common.LogDebug(fmt.Sprintf("Error generating scale file: %s", err.Error()))
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func generateScalefile(appName string) error {
|
|
destination := getScalefilePath(appName)
|
|
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 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
|
|
}
|
|
|
|
func getProcfileCommand(procfilePath string, processType string, port int) (string, error) {
|
|
if !common.FileExists(procfilePath) {
|
|
return "", errors.New("No procfile found")
|
|
}
|
|
|
|
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 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)
|
|
}
|
|
|
|
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 hasScaleFile(appName string) bool {
|
|
scalefilePath := getScalefilePath(appName)
|
|
return common.FileExists(scalefilePath)
|
|
}
|
|
|
|
func hasProcfile(appName string) bool {
|
|
procfilePath := getProcfilePath(appName)
|
|
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) (map[string]int, error) {
|
|
scale := make(map[string]int)
|
|
|
|
for _, processTuple := range processTuples {
|
|
s := strings.Split(processTuple, "=")
|
|
if len(s) == 1 {
|
|
return scale, fmt.Errorf("Missing count for process type %s", processTuple)
|
|
}
|
|
|
|
processType := s[0]
|
|
count, err := strconv.Atoi(s[1])
|
|
if err != nil {
|
|
return scale, fmt.Errorf("Invalid count for process type %s", s[0])
|
|
}
|
|
|
|
scale[processType] = count
|
|
}
|
|
|
|
return scale, nil
|
|
}
|
|
|
|
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 readScaleFile(appName string) (map[string]int, error) {
|
|
scale := make(map[string]int)
|
|
if !hasScaleFile(appName) {
|
|
return scale, errors.New("No DOKKU_SCALE file found")
|
|
}
|
|
|
|
scalefilePath := getScalefilePath(appName)
|
|
lines, err := common.FileToSlice(scalefilePath)
|
|
if err != nil {
|
|
return scale, err
|
|
}
|
|
|
|
for i, line := range lines {
|
|
s := strings.Split(line, "=")
|
|
if len(s) != 2 {
|
|
common.LogWarn(fmt.Sprintf("Invalid scale entry on line %d, skipping", i))
|
|
continue
|
|
}
|
|
|
|
processType := s[0]
|
|
count, err := strconv.Atoi(s[1])
|
|
if err != nil {
|
|
common.LogWarn(fmt.Sprintf("Invalid count on line %d, skipping", i))
|
|
continue
|
|
}
|
|
|
|
scale[processType] = count
|
|
}
|
|
|
|
if len(scale) == 0 {
|
|
common.LogWarn("No valid entries found in scale file, defaulting to web=1")
|
|
scale["web"] = 1
|
|
}
|
|
|
|
return scale, nil
|
|
}
|
|
|
|
func removeProcfile(appName string) error {
|
|
procfile := getProcfilePath(appName)
|
|
if !common.FileExists(procfile) {
|
|
return nil
|
|
}
|
|
|
|
return os.Remove(procfile)
|
|
}
|
|
|
|
func restorePrep() error {
|
|
apps, err := common.DokkuApps()
|
|
if err != nil {
|
|
common.LogWarn(err.Error())
|
|
return nil
|
|
}
|
|
|
|
for _, appName := range apps {
|
|
if err := common.PlugnTrigger("proxy-clear-config", []string{appName}...); err != nil {
|
|
return fmt.Errorf("Error clearing proxy config: %s", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func scaleReport(appName string) error {
|
|
scalefilePath := getScalefilePath(appName)
|
|
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]))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func scaleSet(appName string, skipDeploy bool, processTuples []string) error {
|
|
if !canScaleApp(appName) {
|
|
return fmt.Errorf("App %s contains DOKKU_SCALE file and cannot be manually scaled", appName)
|
|
}
|
|
|
|
scale, err := parseProcessTuples(processTuples)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
common.LogInfo1(fmt.Sprintf("Scaling %s processes: %s", appName, strings.Join(processTuples, " ")))
|
|
if err := updateScalefile(appName, scale); err != nil {
|
|
return err
|
|
}
|
|
|
|
if !common.IsDeployed(appName) {
|
|
return nil
|
|
}
|
|
|
|
imageTag, err := common.GetRunningImageTag(appName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if skipDeploy {
|
|
return nil
|
|
}
|
|
|
|
return common.PlugnTrigger("release-and-deploy", []string{appName, imageTag}...)
|
|
}
|
|
|
|
func updateScalefile(appName string, scaleUpdates map[string]int) error {
|
|
if !hasScaleFile(appName) {
|
|
if err := generateScalefile(appName); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
scale, err := readScaleFile(appName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
procfilePath := getProcfilePath(appName)
|
|
procfileExists := hasProcfile(appName)
|
|
validProcessTypes := make(map[string]bool)
|
|
if procfileExists {
|
|
validProcessTypes, err = processesInProcfile(procfilePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for processType, count := range scaleUpdates {
|
|
if procfileExists && !validProcessTypes[processType] && count != 0 {
|
|
return fmt.Errorf("%s is not a valid process name to scale up", processType)
|
|
}
|
|
scale[processType] = count
|
|
}
|
|
|
|
for processType := range validProcessTypes {
|
|
count, ok := scale[processType]
|
|
if !ok {
|
|
count = 0
|
|
}
|
|
scale[processType] = count
|
|
}
|
|
|
|
content := []string{}
|
|
for processType, count := range scale {
|
|
content = append(content, fmt.Sprintf("%s=%d", processType, count))
|
|
}
|
|
|
|
scalefilePath := getScalefilePath(appName)
|
|
if err := common.WriteSliceToFile(scalefilePath, content); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|