Merge pull request #7578 from dokku/6953-cron-maintenance

Add ability to set maintenance mode for all cron tasks running for an app
This commit is contained in:
Jose Diaz-Gonzalez
2025-06-09 01:05:08 -04:00
committed by GitHub
9 changed files with 91 additions and 39 deletions

View File

@@ -53,39 +53,39 @@ When running scheduled cron tasks, there are a few items to be aware of:
- Scheduled cron tasks are supported on a per-scheduler basis, and are currently only implemented by the `docker-local` scheduler.
- Tasks for _all_ apps managed by the `docker-local` scheduler are written to a single crontab file owned by the `dokku` user. The `dokku` user's crontab should be considered reserved for this purpose.
#### Specifying a MAILFROM value
> [!IMPORTANT]
> New as of 0.35.14
### Changing cron management settings
Users can specify a value for `MAILFROM` via the global `mailfrom` cron property by using the `cron:set` command.
The `cron` plugin provides a number of settings that can be used to managed deployments on a per-app basis. The following table outlines ones not covered elsewhere:
| Name | Description | Level | Global Default |
|-----------------------|----------------------------------------------------------------|-------------|----------------|
| `mailfrom` | Sets the `MAILFROM` variable in a cron file for cron reporting | Global-only | empty string |
| `maintenance` | Whether to have cron running for the app or not. | App-only | `false` |
| `mailto` | Sets the `MAILTO` variable in a cron file for cron reporting | Global-only | empty string |
All settings can be set via the `cron:set` command. Using `maintenance` as an example:
```shell
dokku cron:set --global mailfrom example@example.com
dokku cron:set node-js-app maintenance true
```
All output for individual cron runs will be sent from the specified email.
Cron emails can be reset to use the system email by running the `cron:set` command for the global `mailfrom` property with no value.
The default value may be set by passing an empty value for the option in question:
```shell
dokku cron:set --global mailfrom
dokku cron:set node-js-app maintenance
```
#### Specifying a MAILTO value
By default, cron tasks complete and do not perform any reporting. Users can specify a value for `MAILTO` via the global `mailto` cron property by using the `cron:set` command.
If a property can be set globally - such as `mailto`, use the `--global` flag. If not set for an app, the global value will apply if it exists.
```shell
dokku cron:set --global mailto example@example.com
dokku cron:set --global maintenance true
```
All output for individual cron runs will be sent to the specified email.
Cron emails can be disabled by running the `cron:set` command for the global `mailto` property with no value.
The global default value may be set by passing an empty value for the option.
```shell
dokku cron:set --global mailto
dokku cron:set --global maintenance
```
#### Listing Cron tasks

View File

@@ -1,5 +1,5 @@
SUBCOMMANDS = subcommands/list subcommands/report subcommands/run subcommands/set
TRIGGERS = triggers/cron-get-property
TRIGGERS = triggers/cron-get-property triggers/install triggers/post-app-clone-setup triggers/post-app-rename-setup triggers/post-delete triggers/scheduler-stop
BUILD = commands subcommands triggers
PLUGIN_NAME = cron

View File

@@ -12,14 +12,16 @@ import (
var (
// DefaultProperties is a map of all valid cron properties with corresponding default property values
DefaultProperties = map[string]string{
"mailfrom": "",
"mailto": "",
"mailfrom": "",
"mailto": "",
"maintenance": "false",
}
// GlobalProperties is a map of all valid global cron properties
GlobalProperties = map[string]bool{
"mailfrom": true,
"mailto": true,
"mailfrom": true,
"mailto": true,
"maintenance": true,
}
)
@@ -42,6 +44,9 @@ type TemplateCommand struct {
// LogFile is the log file to write to
LogFile string `json:"-"`
// Maintenance is whether the cron command is in maintenance mode
Maintenance bool `json:"maintenance"`
}
// CronCommand returns the command to run for a given cron command
@@ -59,6 +64,8 @@ func (t TemplateCommand) CronCommand() string {
// FetchCronEntries returns a list of cron commands for a given app
func FetchCronEntries(appName string) ([]TemplateCommand, error) {
commands := []TemplateCommand{}
isMaintenance := reportComputedMaintenance(appName) == "true"
appJSON, err := appjson.GetAppJSON(appName)
if err != nil {
return commands, fmt.Errorf("Unable to fetch app.json for app %s: %s", appName, err.Error())
@@ -76,10 +83,11 @@ func FetchCronEntries(appName string) ([]TemplateCommand, error) {
}
commands = append(commands, TemplateCommand{
App: appName,
Command: c.Command,
Schedule: c.Schedule,
ID: GenerateCommandID(appName, c),
App: appName,
Command: c.Command,
Schedule: c.Schedule,
ID: GenerateCommandID(appName, c),
Maintenance: isMaintenance,
})
}

View File

@@ -13,9 +13,12 @@ func ReportSingleApp(appName string, format string, infoFlag string) error {
}
flags := map[string]common.ReportFunc{
"--cron-mailfrom": reportMailfrom,
"--cron-mailto": reportMailto,
"--cron-task-count": reportTasks,
"--cron-mailfrom": reportMailfrom,
"--cron-mailto": reportMailto,
"--cron-task-count": reportTasks,
"--cron-global-maintenance": reportGlobalMaintenance,
"--cron-computed-maintenance": reportComputedMaintenance,
"--cron-maintenance": reportMaintenance,
}
flagKeys := []string{}
@@ -41,3 +44,20 @@ func reportTasks(appName string) string {
c, _ := FetchCronEntries(appName)
return strconv.Itoa(len(c))
}
func reportGlobalMaintenance(_ string) string {
return common.PropertyGet("cron", "--global", "maintenance")
}
func reportComputedMaintenance(appName string) string {
maintenance := common.PropertyGet("cron", appName, "maintenance")
if maintenance == "true" {
return "true"
}
return common.PropertyGetDefault("cron", "--global", "maintenance", DefaultProperties["maintenance"])
}
func reportMaintenance(appName string) string {
return common.PropertyGet("cron", appName, "maintenance")
}

View File

@@ -43,6 +43,11 @@ func main() {
case "report":
appName := flag.Arg(0)
err = cron.ReportSingleApp(appName, "", "")
case "scheduler-stop":
scheduler := flag.Arg(0)
appName := flag.Arg(1)
removeContainers := flag.Arg(2)
err = cron.TriggerSchedulerStop(scheduler, appName, removeContainers)
default:
err = fmt.Errorf("Invalid plugin trigger call: %s", trigger)
}

View File

@@ -32,9 +32,9 @@ func CommandList(appName string, format string) error {
}
if format == "stdout" {
output := []string{"ID | Schedule | Command"}
output := []string{"ID | Schedule | Maintenance | Command"}
for _, entry := range entries {
output = append(output, fmt.Sprintf("%s | %s | %s", entry.ID, entry.Schedule, entry.Command))
output = append(output, fmt.Sprintf("%s | %s | %t | %s", entry.ID, entry.Schedule, entry.Maintenance, entry.Command))
}
result := columnize.SimpleFormat(output)

View File

@@ -10,14 +10,16 @@ import (
// TriggerCronGetProperty writes the cron key to stdout for a given app container
func TriggerCronGetProperty(appName string, key string) error {
validProperties := map[string]bool{
"mailfrom": true,
"mailto": true,
"mailfrom": true,
"mailto": true,
"maintenance": true,
}
if !validProperties[key] {
return errors.New("Invalid cron property specified")
}
fmt.Println(common.PropertyGet("cron", appName, key))
value := common.PropertyGetDefault("cron", appName, key, DefaultProperties[key])
fmt.Println(value)
return nil
}
@@ -57,3 +59,12 @@ func TriggerPostAppRenameSetup(oldAppName string, newAppName string) error {
func TriggerPostDelete(appName string) error {
return common.PropertyDestroy("cron", appName)
}
// TriggerSchedulerStop stops the scheduler for a given app container
func TriggerSchedulerStop(scheduler string, appName string, removeContainers string) error {
if scheduler != "docker-local" {
return nil
}
return nil
}

View File

@@ -83,9 +83,10 @@ func generateCronEntries() ([]cron.TemplateCommand, error) {
id := base36.EncodeToStringLc([]byte(strings.Join(parts, ";;;")))
command := cron.TemplateCommand{
ID: id,
Schedule: parts[0],
AltCommand: parts[1],
ID: id,
Schedule: parts[0],
AltCommand: parts[1],
Maintenance: false,
}
if len(parts) == 3 {
command.LogFile = parts[2]
@@ -106,7 +107,7 @@ func generateCronEntries() ([]cron.TemplateCommand, error) {
for result := range results {
c := result
if len(c) > 0 {
if len(c) > 0 && !c[0].Maintenance {
commands = append(commands, c...)
}
}

View File

@@ -266,10 +266,17 @@ func TriggerSchedulerDeploy(scheduler string, appName string, imageTag string) e
workingDir := common.GetWorkingDir(appName, image)
cronEntries, err := cron.FetchCronEntries(appName)
allCronEntries, err := cron.FetchCronEntries(appName)
if err != nil {
return fmt.Errorf("Error fetching cron entries: %w", err)
}
// remove maintenance cron entries
cronEntries := []cron.TemplateCommand{}
for _, cronEntry := range allCronEntries {
if !cronEntry.Maintenance {
cronEntries = append(cronEntries, cronEntry)
}
}
domains := []string{}
if _, ok := processes["web"]; ok {