Files
dokku/plugins/ps/triggers.go
Jose Diaz-Gonzalez 1eb41f32dc feat: only redeploy formations that have had their values changed
Rather than redeploy every formation, only deploy formations that have had their values changed from the defaults.

This still doesn't handle the case where we touch existing processes when scaling _up_, but should decrease the number of services touched otherwise during scaling.

Also note that there are no tests for this new behavior yet (tests incoming).

Refs #7396
2025-06-23 04:42:09 -04:00

364 lines
9.7 KiB
Go

package ps
import (
"errors"
"fmt"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"github.com/dokku/dokku/plugins/common"
"github.com/dokku/dokku/plugins/config"
dockeroptions "github.com/dokku/dokku/plugins/docker-options"
)
// TriggerAppRestart restarts an app
func TriggerAppRestart(appName string) error {
return Restart(appName)
}
// TriggerCorePostDeploy sets a property to
// allow the app to be restored on boot
func TriggerCorePostDeploy(appName string) error {
existingProcfile := getProcfilePath(appName)
processSpecificProcfile := fmt.Sprintf("%s.%s", existingProcfile, os.Getenv("DOKKU_PID"))
if common.FileExists(processSpecificProcfile) {
if err := os.Rename(processSpecificProcfile, existingProcfile); err != nil {
return err
}
} else if common.FileExists(fmt.Sprintf("%s.missing", processSpecificProcfile)) {
if err := os.Remove(fmt.Sprintf("%s.missing", processSpecificProcfile)); err != nil {
return err
}
if common.FileExists(existingProcfile) {
if err := os.Remove(existingProcfile); err != nil {
return err
}
}
}
if err := common.PropertyDelete("ps", appName, "scale.old"); err != nil {
return err
}
entries := map[string]string{
"DOKKU_APP_RESTORE": "1",
}
return common.SuppressOutput(func() error {
return config.SetMany(appName, entries, false)
})
}
// TriggerCorePostExtract ensures that the main Procfile is the one specified by procfile-path
func TriggerCorePostExtract(appName string, sourceWorkDir string) error {
procfilePath := strings.Trim(reportComputedProcfilePath(appName), "/")
if procfilePath == "" {
procfilePath = "Procfile"
}
existingProcfile := getProcfilePath(appName)
files, err := filepath.Glob(fmt.Sprintf("%s.*", existingProcfile))
if err != nil {
return err
}
for _, f := range files {
if err := os.Remove(f); err != nil {
return err
}
}
processSpecificProcfile := fmt.Sprintf("%s.%s", existingProcfile, os.Getenv("DOKKU_PID"))
results, _ := common.CallPlugnTrigger(common.PlugnTriggerInput{
Trigger: "git-get-property",
Args: []string{appName, "source-image"},
})
appSourceImage := results.StdoutContents()
repoDefaultProcfilePath := path.Join(sourceWorkDir, "Procfile")
if appSourceImage == "" {
repoProcfilePath := path.Join(sourceWorkDir, procfilePath)
if !common.FileExists(repoProcfilePath) {
if procfilePath != "Procfile" && common.FileExists(repoDefaultProcfilePath) {
if err := os.Remove(repoDefaultProcfilePath); err != nil {
return err
}
}
return common.TouchFile(fmt.Sprintf("%s.missing", processSpecificProcfile))
}
if err := common.Copy(repoProcfilePath, processSpecificProcfile); err != nil {
return fmt.Errorf("Unable to extract Procfile: %v", err.Error())
}
if procfilePath != "Procfile" {
if err := common.Copy(repoProcfilePath, repoDefaultProcfilePath); err != nil {
return fmt.Errorf("Unable to move Procfile into place: %v", err.Error())
}
}
} else {
if err := common.CopyFromImage(appName, appSourceImage, procfilePath, processSpecificProcfile); err != nil {
return common.TouchFile(fmt.Sprintf("%s.missing", processSpecificProcfile))
}
}
result, err := common.CallExecCommand(common.ExecCommandInput{
Command: "procfile-util",
Args: []string{"check", "-P", processSpecificProcfile},
})
if err != nil {
return fmt.Errorf(result.StderrContents())
}
if result.ExitCode != 0 {
return fmt.Errorf("Invalid Procfile: %s", result.StderrContents())
}
return nil
}
// TriggerInstall initializes app restart policies
func TriggerInstall() error {
if err := common.PropertySetup("ps"); err != nil {
return fmt.Errorf("Unable to install the ps plugin: %s", err.Error())
}
if err := common.SetupAppData("ps"); err != nil {
return err
}
apps, err := common.UnfilteredDokkuApps()
if err != nil {
return nil
}
for _, appName := range apps {
if err := common.CreateAppDataDirectory("ps", appName); err != nil {
return err
}
}
for _, appName := range apps {
policies, err := getRestartPolicy(appName)
if err != nil {
return err
}
if len(policies) != 0 {
continue
}
if err := dockeroptions.AddDockerOptionToPhases(appName, []string{"deploy"}, "--restart=on-failure:10"); err != nil {
common.LogWarn(err.Error())
}
}
for _, appName := range apps {
dokkuScaleFile := filepath.Join(common.AppRoot(appName), "DOKKU_SCALE")
if common.FileExists(dokkuScaleFile) {
os.Remove(dokkuScaleFile)
}
dokkuScaleExtracted := filepath.Join(common.AppRoot(appName), "DOKKU_SCALE.extracted")
if common.FileExists(dokkuScaleExtracted) {
os.Remove(dokkuScaleExtracted)
}
results, _ := common.CallPlugnTrigger(common.PlugnTriggerInput{
Trigger: "config-get",
Args: []string{appName, "DOKKU_DOCKER_STOP_TIMEOUT"},
})
stopTimeout := results.StdoutContents()
if stopTimeout == "" {
continue
}
common.LogVerboseQuiet(fmt.Sprintf("Setting %s ps property 'stop-timeout-seconds' to %v", appName, stopTimeout))
if err := common.PropertyWrite("ps", appName, "stop-timeout-seconds", stopTimeout); err != nil {
return err
}
_, err := common.CallPlugnTrigger(common.PlugnTriggerInput{
Trigger: "config-unset",
Args: []string{appName, "DOKKU_DOCKER_STOP_TIMEOUT"},
})
if err != nil {
return err
}
}
return nil
}
// TriggerPostAppClone rebuilds the new app
func TriggerPostAppClone(oldAppName string, newAppName string) error {
if os.Getenv("SKIP_REBUILD") == "true" {
return nil
}
return Rebuild(newAppName)
}
// TriggerPostAppCloneSetup creates new ps files
func TriggerPostAppCloneSetup(oldAppName string, newAppName string) error {
err := common.PropertyClone("ps", oldAppName, newAppName)
if err != nil {
return err
}
return common.CloneAppData("ps", oldAppName, newAppName)
}
// TriggerPostAppRename rebuilds the renamed app
func TriggerPostAppRename(oldAppName string, newAppName string) error {
if err := common.MigrateAppDataDirectory("ps", oldAppName, newAppName); err != nil {
return err
}
if os.Getenv("SKIP_REBUILD") == "true" {
return nil
}
return Rebuild(newAppName)
}
// TriggerPostAppRenameSetup renames ps files
func TriggerPostAppRenameSetup(oldAppName string, newAppName string) error {
if err := common.PropertyClone("ps", oldAppName, newAppName); err != nil {
return err
}
if err := common.PropertyDestroy("ps", oldAppName); err != nil {
return err
}
return common.CloneAppData("ps", oldAppName, newAppName)
}
// TriggerPostCreate ensures apps have a default restart policy
// and scale value for web
func TriggerPostCreate(appName string) error {
if err := dockeroptions.AddDockerOptionToPhases(appName, []string{"deploy"}, "--restart=on-failure:10"); err != nil {
return err
}
if err := common.CreateAppDataDirectory("ps", appName); err != nil {
return err
}
formations := FormationSlice{
&Formation{
ProcessType: "web",
Quantity: 1,
},
}
return updateScale(appName, false, formations)
}
// TriggerPostDelete destroys the ps properties for a given app container
func TriggerPostDelete(appName string) error {
dataErr := common.RemoveAppDataDirectory("ps", appName)
propertyErr := common.PropertyDestroy("ps", appName)
if dataErr != nil {
return dataErr
}
return propertyErr
}
// TriggerPostStop sets the restore property to false
func TriggerPostStop(appName string) error {
entries := map[string]string{
"DOKKU_APP_RESTORE": "0",
}
return common.SuppressOutput(func() error {
return config.SetMany(appName, entries, false)
})
}
// TriggerPostReleaseBuilder ensures an app has an up to date scale parameters
func TriggerPostReleaseBuilder(builderType string, appName string, image string) error {
if err := updateScale(appName, false, FormationSlice{}); err != nil {
common.LogDebug(fmt.Sprintf("Error generating scale file: %s", err.Error()))
return err
}
return nil
}
// TriggerProcfileExists checks if a procfile exists
func TriggerProcfileExists(appName string) error {
if hasProcfile(appName) {
return nil
}
return errors.New("Procfile does not exist")
}
// TriggerProcfileGetCommand fetches a command from the procfile
func TriggerProcfileGetCommand(appName string, processType string, port int) error {
if !hasProcfile(appName) {
return nil
}
command, err := getProcfileCommand(getProcessSpecificProcfilePath(appName), processType, port)
if err != nil {
return err
}
if command != "" {
fmt.Printf("%s\n", command)
}
return nil
}
// TriggerPsCanScale sets whether or not a user can scale an app with ps:scale
func TriggerPsCanScale(appName string, canScale bool) error {
return common.PropertyWrite("ps", appName, "can-scale", strconv.FormatBool(canScale))
}
// TriggerPsCurrentScale prints out the current scale contents (process-type=quantity) delimited by newlines
func TriggerPsCurrentScale(appName string) error {
formations, err := getFormations(appName)
if err != nil {
return err
}
lines := []string{}
for _, formation := range formations {
lines = append(lines, fmt.Sprintf("%s=%d", formation.ProcessType, formation.Quantity))
}
fmt.Print(strings.Join(lines, "\n"))
return nil
}
// TriggerPsSetScale configures the scale parameters for a given app
func TriggerPsSetScale(appName string, skipDeploy bool, clearExisting bool, processTuples []string) error {
return scaleSet(scaleSetInput{
appName: appName,
skipDeploy: skipDeploy,
clearExisting: clearExisting,
processTuples: processTuples,
deployOnlyChanged: false,
})
}
func TriggerPsGetProperty(appName string, property string) error {
computedValueMap := map[string]common.ReportFunc{
"stop-timeout-seconds": reportComputedStopTimeoutSeconds,
}
fn, ok := computedValueMap[property]
if !ok {
return fmt.Errorf("Invalid network property specified: %v", property)
}
fmt.Println(fn(appName))
return nil
}