这是indexloc提供的服务,不要输入任何密码
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
c885205
refactor: respect the maintenance value of the cron task instead of s…
josegonzalez Sep 21, 2025
4a357e9
fix: properly suspend and resume k8s CronJob objects when executing s…
josegonzalez Sep 22, 2025
e42df29
feat: allow specifying maintenance mode in the file or not
josegonzalez Sep 22, 2025
3c7985c
feat: track task vs app maintenance separately
josegonzalez Sep 22, 2025
f99e3a6
feat: add ability to suspend and resume a cron task by ID
josegonzalez Sep 22, 2025
66a30b0
refactor: rename cron entry to cron task
josegonzalez Sep 22, 2025
426e6b7
chore: standardize on cron task vs any other naming
josegonzalez Sep 22, 2025
7461b83
fix: use correct makefile target
josegonzalez Nov 8, 2025
00ce393
fix: update the json output
josegonzalez Nov 8, 2025
2aea999
test: check if uninstalling after deleting apps will cause tests to w…
josegonzalez Nov 9, 2025
e30b579
test: fix issue where teardown was not being called
josegonzalez Nov 9, 2025
d9603af
docs: document cron task suspension
josegonzalez Nov 10, 2025
4929272
fix: calculate maintenance property correctly
josegonzalez Nov 10, 2025
3638399
fix: parse cronID correctly
josegonzalez Nov 10, 2025
d49f9b8
chore: add commands to cron:help output
josegonzalez Nov 10, 2025
379ada0
fix: properly fetch non-maintenance tasks when building crontab file
josegonzalez Nov 10, 2025
4cc2dc7
feat: add cron maintenance info to cron:report output
josegonzalez Nov 10, 2025
8ba6ce9
test: add tests for cron suspend and resume commands
josegonzalez Nov 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/appendices/file-formats/app-json.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
(list, optional) A list of cron resources. Keys are the names of the process types. The values are an object containing one or more of the following properties:

- `command`: (string, required)
- `maintenance`: (boolean, optional)
- `schedule`: (string, required)

## Formation
Expand Down
23 changes: 23 additions & 0 deletions docs/processes/scheduled-cron-tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
```
cron:list <app> [--format json|stdout] # List scheduled cron tasks for an app
cron:report [<app>] [<flag>] # Display report about an app
cron:resume <app> <cron_id> # Resume a cron task
cron:run <app> <cron_id> [--detach] # Run a cron task on the fly
cron:set [--global|<app>] <key> <value> # Set or clear a cron property for an app
cron:suspend <app> <cron_id> # Suspend a cron task
```

## Usage
Expand All @@ -34,6 +36,7 @@ The `app.json` file for a given app can define a special `cron` key that contain
A cron task takes the following properties:

- `command`: A command to be run within the built app image. Specified commands can also be `Procfile` entries.
- `maintenance`: A boolean value that decides whether the cron task is in maintenance and therefore executable or not.
- `schedule`: A [cron-compatible](https://en.wikipedia.org/wiki/Cron#Overview) scheduling definition upon which to run the command. Seconds are generally not supported.

Zero or more cron tasks can be specified per app. Cron tasks are validated after the build artifact is created but before the app is deployed, and the cron schedule is updated during the post-deploy phase.
Expand Down Expand Up @@ -123,6 +126,26 @@ ID Schedule Command
5cruaotm4yzzpnjlsdunblj8qyjp @daily /bin/true
```

#### Suspending and resuming a specific cron task

Cron tasks can be suspended to temporarily prevent them from running, and later resumed to re-enable them. This is useful for maintenance or debugging purposes.

To suspend a specific cron task, use the `cron:suspend` command with the app name and cron ID:

```shell
dokku cron:suspend node-js-app cGhwPT09cGhwIHRlc3QucGhwPT09QGRhaWx5
```

A suspended task will not execute according to its schedule. You can verify a task is suspended by checking the `Maintenance` column in the `cron:list` output, which will show `true (task)` for suspended tasks.

To resume a suspended cron task, use the `cron:resume` command:

```shell
dokku cron:resume node-js-app cGhwPT09cGhwIHRlc3QucGhwPT09QGRhaWx5
```

Once resumed, the task will execute according to its schedule again. The cron ID can be retrieved from the `cron:list` output.

#### Executing a cron task on the fly

Cron tasks can be invoked via the `cron:run` command. This command takes an `app` argument and a `cron id` (retrievable from `cron:list` output).
Expand Down
3 changes: 3 additions & 0 deletions plugins/app-json/appjson.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ type CronTask struct {
// Command is the command to execute
Command string `json:"command"`

// Maintenance is whether or not the cron task is in maintenance mode
Maintenance bool `json:"maintenance"`

// Schedule is the cron schedule to execute the command on
Schedule string `json:"schedule"`
}
Expand Down
14 changes: 7 additions & 7 deletions plugins/common/properties.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,27 @@ import (
)

// CommandPropertySet is a generic function that will set a property for a given plugin/app combination
func CommandPropertySet(pluginName, appName, property, value string, properties map[string]string, globalProperties map[string]bool) {
func CommandPropertySet(pluginName, appName, property, value string, validProperties map[string]string, validGlobalProperties map[string]bool) {
if appName != "--global" {
if err := VerifyAppName(appName); err != nil {
LogFailWithError(err)
}
}
if appName == "--global" && !globalProperties[property] {
if appName == "--global" && !validGlobalProperties[property] {
LogFail("Property cannot be specified globally")
}
if property == "" {
LogFail("No property specified")
}

for k := range globalProperties {
if _, ok := properties[k]; !ok {
properties[k] = ""
for k := range validGlobalProperties {
if _, ok := validProperties[k]; !ok {
validProperties[k] = ""
}
}

if _, ok := properties[property]; !ok {
properties := reflect.ValueOf(properties).MapKeys()
if _, ok := validProperties[property]; !ok {
properties := reflect.ValueOf(validProperties).MapKeys()
validPropertyList := make([]string, len(properties))
for i := 0; i < len(properties); i++ {
validPropertyList[i] = properties[i].String()
Expand Down
2 changes: 1 addition & 1 deletion plugins/cron/Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
SUBCOMMANDS = subcommands/list subcommands/report subcommands/run subcommands/set
SUBCOMMANDS = subcommands/list subcommands/report subcommands/resume subcommands/run subcommands/set subcommands/suspend
TRIGGERS = triggers/app-json-is-valid 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
Expand Down
56 changes: 44 additions & 12 deletions plugins/cron/cron.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cron

import (
"fmt"
"strconv"
"strings"

appjson "github.com/dokku/dokku/plugins/app-json"
Expand All @@ -27,6 +28,8 @@ var (
}
)

const MaintenancePropertyPrefix = "maintenance."

// CronTask is a struct that represents a cron task
type CronTask struct {
// ID is a unique identifier for the cron task
Expand All @@ -50,6 +53,12 @@ type CronTask struct {
// LogFile is the log file to write to
LogFile string `json:"-"`

// AppInMaintenance is whether the app's cron is in maintenance mode
AppInMaintenance bool `json:"app-in-maintenance"`

// Maintenance is whether the cron task is in maintenance mode
TaskInMaintenance bool `json:"task-in-maintenance"`

// Maintenance is whether the cron task is in maintenance mode
Maintenance bool `json:"maintenance"`
}
Expand Down Expand Up @@ -77,7 +86,7 @@ type FetchCronTasksInput struct {
func FetchCronTasks(input FetchCronTasksInput) ([]CronTask, error) {
appName := input.AppName
tasks := []CronTask{}
isMaintenance := reportComputedMaintenance(appName) == "true"
isAppCronInMaintenance := reportComputedMaintenance(appName) == "true"

if input.AppJSON == nil && input.AppName == "" {
return tasks, fmt.Errorf("Missing app name or app.json")
Expand All @@ -96,6 +105,11 @@ func FetchCronTasks(input FetchCronTasksInput) ([]CronTask, error) {
return tasks, nil
}

properties, err := common.PropertyGetAllByPrefix("cron", appName, MaintenancePropertyPrefix)
if err != nil {
return tasks, fmt.Errorf("Error getting maintenance properties: %w", err)
}

for i, c := range input.AppJSON.Cron {
if c.Command == "" {
if input.WarnToFailure {
Expand All @@ -121,12 +135,28 @@ func FetchCronTasks(input FetchCronTasksInput) ([]CronTask, error) {
return tasks, fmt.Errorf("Invalid cron schedule for app %s (schedule %s): %s", appName, c.Schedule, err.Error())
}

cronID := GenerateCommandID(appName, c)
maintenance := c.Maintenance
if value, ok := properties[MaintenancePropertyPrefix+cronID]; ok {
boolValue, err := strconv.ParseBool(value)
if err != nil {
return tasks, fmt.Errorf("Invalid maintenance property for app %s (schedule %s): %s", appName, c.Schedule, err.Error())
}

// only override the maintenance value if the property is set to true
if boolValue {
maintenance = boolValue
}
}

tasks = append(tasks, CronTask{
App: appName,
Command: c.Command,
Schedule: c.Schedule,
ID: GenerateCommandID(appName, c),
Maintenance: isMaintenance,
App: appName,
Command: c.Command,
Schedule: c.Schedule,
ID: cronID,
Maintenance: isAppCronInMaintenance || maintenance,
AppInMaintenance: isAppCronInMaintenance,
TaskInMaintenance: maintenance,
})
}

Expand Down Expand Up @@ -155,12 +185,14 @@ func FetchGlobalCronTasks() ([]CronTask, error) {

id := base36.EncodeToStringLc([]byte(strings.Join(parts, ";;;")))
task := CronTask{
ID: id,
Schedule: parts[0],
Command: parts[1],
AltCommand: parts[1],
Maintenance: false,
Global: true,
ID: id,
Schedule: parts[0],
Command: parts[1],
AltCommand: parts[1],
Global: true,
Maintenance: false,
TaskInMaintenance: false,
AppInMaintenance: false,
}
if len(parts) == 3 {
task.LogFile = parts[2]
Expand Down
25 changes: 25 additions & 0 deletions plugins/cron/report.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package cron

import (
"fmt"
"strconv"
"strings"

"github.com/dokku/dokku/plugins/common"
)
Expand All @@ -21,6 +23,11 @@ func ReportSingleApp(appName string, format string, infoFlag string) error {
"--cron-maintenance": reportMaintenance,
}

extraFlags := addCronMaintenanceFlags(appName, infoFlag)
for flag, fn := range extraFlags {
flags[flag] = fn
}

flagKeys := []string{}
for flagKey := range flags {
flagKeys = append(flagKeys, flagKey)
Expand All @@ -32,6 +39,24 @@ func ReportSingleApp(appName string, format string, infoFlag string) error {
return common.ReportSingleApp("cron", appName, infoFlag, infoFlags, flagKeys, format, trimPrefix, uppercaseFirstCharacter)
}

func addCronMaintenanceFlags(appName string, infoFlag string) map[string]common.ReportFunc {
flags := map[string]common.ReportFunc{}

properties, err := common.PropertyGetAllByPrefix("cron", appName, MaintenancePropertyPrefix)
if err != nil {
return flags
}

for property, value := range properties {
key := strings.Replace(property, MaintenancePropertyPrefix, "", 1)
flags[fmt.Sprintf("--cron-maintenance-%s", key)] = func(appName string) string {
return value
}
}

return flags
}

func reportMailfrom(_ string) string {
return common.PropertyGet("cron", "--global", "mailfrom")
}
Expand Down
4 changes: 3 additions & 1 deletion plugins/cron/src/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ Additional commands:`
helpContent = `
cron:list <app> [--format json|stdout], List scheduled cron tasks for an app
cron:report [<app>] [<flag>], Display report about an app
cron:resume <app> <cron_id>, Resume a cron task
cron:run <app> <cron_id> [--detach], Run a cron task on the fly
cron:set [--global|<app>] <key> <value>, Set or clear a cron property for an app`
cron:set [--global|<app>] <key> <value>, Set or clear a cron property for an app
cron:suspend <app> <cron_id>, Suspend a cron task`
)

func main() {
Expand Down
12 changes: 12 additions & 0 deletions plugins/cron/src/subcommands/subcommands.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ func main() {
appName := args.Arg(0)
err = cron.CommandReport(appName, *format, infoFlag)
}
case "resume":
args := flag.NewFlagSet("cron:resume", flag.ExitOnError)
args.Parse(os.Args[2:])
appName := args.Arg(0)
cronID := args.Arg(1)
err = cron.CommandResume(appName, cronID)
case "run":
args := flag.NewFlagSet("cron:run", flag.ExitOnError)
detached := args.Bool("detach", false, "--detach: run the container in a detached mode")
Expand All @@ -57,6 +63,12 @@ func main() {
value = args.Arg(1)
}
err = cron.CommandSet(appName, property, value)
case "suspend":
args := flag.NewFlagSet("cron:suspend", flag.ExitOnError)
args.Parse(os.Args[2:])
appName := args.Arg(0)
cronID := args.Arg(1)
err = cron.CommandSuspend(appName, cronID)
default:
err = fmt.Errorf("Invalid plugin subcommand call: %s", subcommand)
}
Expand Down
53 changes: 51 additions & 2 deletions plugins/cron/subcommands.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"os"
"strings"

"github.com/dokku/dokku/plugins/common"

Expand Down Expand Up @@ -43,7 +44,15 @@ func CommandList(appName string, format string) error {
if format == "stdout" {
output := []string{"ID | Schedule | Maintenance | Command"}
for _, task := range tasks {
output = append(output, fmt.Sprintf("%s | %s | %t | %s", task.ID, task.Schedule, task.Maintenance, task.Command))
maintenance := "false"
if task.Maintenance {
if task.TaskInMaintenance {
maintenance = "true (task)"
} else if task.AppInMaintenance {
maintenance = "true (app)"
}
}
output = append(output, fmt.Sprintf("%s | %s | %s | %s", task.ID, task.Schedule, maintenance, task.Command))
}

result := columnize.SimpleFormat(output)
Expand Down Expand Up @@ -82,6 +91,11 @@ func CommandReport(appName string, format string, infoFlag string) error {
return ReportSingleApp(appName, format, infoFlag)
}

// CommandResume resumes a cron task
func CommandResume(appName string, cronID string) error {
return CommandSet(appName, fmt.Sprintf("%s%s", MaintenancePropertyPrefix, cronID), "")
}

// CommandRun executes a cron task on the fly
func CommandRun(appName string, cronID string, detached bool) error {
if err := common.VerifyAppName(appName); err != nil {
Expand Down Expand Up @@ -138,7 +152,37 @@ func CommandSet(appName string, property string, value string) error {
return err
}

common.CommandPropertySet("cron", appName, property, value, DefaultProperties, GlobalProperties)
validProperties := DefaultProperties
globalProperties := GlobalProperties
if strings.HasPrefix(property, MaintenancePropertyPrefix) {
if appName == "--global" {
return fmt.Errorf("Task maintenance properties cannot be set globally")
}

cronTaskID := strings.TrimPrefix(property, MaintenancePropertyPrefix)
if cronTaskID == "" {
return fmt.Errorf("Invalid task maintenance property, missing ID")
}

tasks, err := FetchCronTasks(FetchCronTasksInput{AppName: appName})
if err != nil {
return err
}

for _, task := range tasks {
if task.ID == cronTaskID {
validProperties[property] = ""
globalProperties[property] = false
break
}
}

if _, ok := validProperties[property]; !ok {
return fmt.Errorf("Invalid task maintenance property, no matching task ID found: %s", property)
}
}

common.CommandPropertySet("cron", appName, property, value, validProperties, globalProperties)
scheduler := common.GetAppScheduler(appName)
_, err := common.CallPlugnTrigger(common.PlugnTriggerInput{
Trigger: "scheduler-cron-write",
Expand All @@ -147,3 +191,8 @@ func CommandSet(appName string, property string, value string) error {
})
return err
}

// CommandSuspend suspends a cron task
func CommandSuspend(appName string, cronID string) error {
return CommandSet(appName, fmt.Sprintf("%s%s", MaintenancePropertyPrefix, cronID), "true")
}
Loading
Loading