fix: retire cron containers past their active deadline

Cron containers were never reaped once they exceeded their active deadline, so a hung cron task ran indefinitely instead of being retired after 24 hours as documented. `cron:run` now also accepts a `--ttl-seconds` argument, matching the one `dokku run` already takes.
This commit is contained in:
Jose Diaz-Gonzalez
2026-08-12 13:11:34 -04:00
parent 0bfd35dada
commit 96d726c43c
18 changed files with 1170 additions and 906 deletions

View File

@@ -2903,8 +2903,8 @@ DOKKU_SCHEDULER="$1"; APP="$2"; CONTAINER="$3"; TAIL="$4"; PRETTY_PRINT="$5"; NU
> The scheduler plugin trigger apis are under development and may change
> between minor releases until the 1.0 release.
- Description: Allows you to retire containers started by the `run` command
- Invoked by: `dokku run:retire`
- Description: Allows you to retire containers started by the `run` and `cron:run` commands that have exceeded their active deadline
- Invoked by: `dokku run:retire`, `dokku ps:retire`
- Arguments: `$DOKKU_SCHEDULER $APP`
- Example:

View File

@@ -4,12 +4,12 @@
> New as of 0.23.0
```
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
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] [--ttl-seconds SECONDS] # 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
@@ -43,7 +43,7 @@ A cron task takes the following properties:
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.
Cron tasks can run for a maximum of 24 hours via the docker-local scheduler, after which they are reaped from the system.
Cron tasks can run for a maximum of 24 hours, after which they are reaped from the system. The `docker-local` scheduler reaps expired tasks via the `dokku ps:retire` pass that runs every 5 minutes, so a task may overrun its deadline by up to 5 minutes. The `k3s` scheduler enforces the deadline directly through the job's `activeDeadlineSeconds`.
See the [app.json location documentation](/docs/advanced-usage/deployment-tasks.md#changing-the-appjson-location) for more information on where to place your `app.json` file.
@@ -190,6 +190,14 @@ By default, the task is run in an attached container - as supported by the sched
dokku cron:run node-js-app cGhwPT09cGhwIHRlc3QucGhwPT09QGRhaWx5 --detach
```
An on-the-fly invocation runs for a maximum of 24 hours (86400 seconds), the same deadline scheduled invocations receive. A different deadline can be requested with the `--ttl-seconds` argument:
```shell
dokku cron:run node-js-app cGhwPT09cGhwIHRlc3QucGhwPT09QGRhaWx5 --detach --ttl-seconds 600
```
The value applies only to that invocation - tasks started by the schedule keep the 24 hour default.
All one-off cron executions have their containers terminated after invocation.
#### Displaying reports

View File

@@ -42,6 +42,23 @@ var (
const MaintenancePropertyPrefix = "maintenance."
// DefaultTTLSeconds is how long a cron task may run before it is reaped. The
// docker-local scheduler stamps this onto the container as the
// com.dokku.active-deadline-seconds label, while the k3s scheduler renders it
// as the CronJob's activeDeadlineSeconds.
const DefaultTTLSeconds int64 = 86400
// validateTTLSeconds returns an error if the requested task lifetime is not a
// positive number of seconds. A zero or negative deadline would either expire
// the task the instant it starts or never expire it at all.
func validateTTLSeconds(ttlSeconds int64) error {
if ttlSeconds <= 0 {
return fmt.Errorf("--ttl-seconds must be a positive integer")
}
return nil
}
// CronTask is a struct that represents a cron task
type CronTask struct {
// ID is a unique identifier for the cron task

View File

@@ -123,3 +123,32 @@ func TestDokkuRunCommandAppTaskIgnoresLogFile(t *testing.T) {
t.Errorf("DokkuRunCommand() interpolated a redirect into an app task line: %q", got)
}
}
// TestValidateTTLSeconds pins that a cron task deadline must be a positive
// number of seconds. A zero or negative value would either expire the task the
// instant it starts or leave it running forever, and the docker-local retire
// pass only reaps containers whose deadline has actually elapsed.
func TestValidateTTLSeconds(t *testing.T) {
cases := []struct {
name string
ttlSeconds int64
wantErr bool
}{
{name: "default", ttlSeconds: DefaultTTLSeconds},
{name: "positive override", ttlSeconds: 1},
{name: "zero", ttlSeconds: 0, wantErr: true},
{name: "negative", ttlSeconds: -1, wantErr: true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := validateTTLSeconds(tc.ttlSeconds)
if tc.wantErr && err == nil {
t.Errorf("validateTTLSeconds(%d) = nil, want an error", tc.ttlSeconds)
}
if !tc.wantErr && err != nil {
t.Errorf("validateTTLSeconds(%d) = %v, want nil", tc.ttlSeconds, err)
}
})
}
}

View File

@@ -21,7 +21,7 @@ Additional commands:`
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:run <app> <cron_id> [--detach] [--ttl-seconds SECONDS], 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`
)

View File

@@ -49,10 +49,11 @@ func main() {
case "run":
args := flag.NewFlagSet("cron:run", flag.ExitOnError)
detached := args.Bool("detach", false, "--detach: run the container in a detached mode")
ttlSeconds := args.Int64("ttl-seconds", cron.DefaultTTLSeconds, "--ttl-seconds: number of seconds the task may run before it is reaped")
args.Parse(os.Args[2:])
appName := args.Arg(0)
cronID := args.Arg(1)
err = cron.CommandRun(appName, cronID, *detached)
err = cron.CommandRun(appName, cronID, *detached, *ttlSeconds)
case "set":
args := flag.NewFlagSet("cron:set", flag.ExitOnError)
global := args.Bool("global", false, "--global: set a global property")

View File

@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"os"
"strconv"
"strings"
"github.com/dokku/dokku/plugins/common"
@@ -97,11 +98,15 @@ func CommandResume(appName string, cronID string) error {
}
// CommandRun executes a cron task on the fly
func CommandRun(appName string, cronID string, detached bool) error {
func CommandRun(appName string, cronID string, detached bool, ttlSeconds int64) error {
if err := common.VerifyAppName(appName); err != nil {
return err
}
if err := validateTTLSeconds(ttlSeconds); err != nil {
return err
}
tasks, err := FetchCronTasks(FetchCronTasksInput{AppName: appName})
if err != nil {
return err
@@ -139,7 +144,7 @@ func CommandRun(appName string, cronID string, detached bool) error {
os.Setenv("DOKKU_CONCURRENCY_POLICY", concurrencyPolicy)
os.Setenv("DOKKU_CRON_ID", cronID)
os.Setenv("DOKKU_RM_CONTAINER", "1")
os.Setenv("DOKKU_RUN_TTL_SECONDS", "86400")
os.Setenv("DOKKU_RUN_TTL_SECONDS", strconv.FormatInt(ttlSeconds, 10))
scheduler := common.GetAppScheduler(appName)
args := append([]string{scheduler, appName, "0", "--"}, fields...)
_, err = common.CallPlugnTrigger(common.PlugnTriggerInput{

View File

@@ -145,7 +145,7 @@ func CommandRetire(appName string) error {
return fmt.Errorf("Error retiring containers: %w", err)
}
common.LogInfo1("Retiring expired run containers")
common.LogInfo1("Retiring expired run and cron containers")
_, err = common.CallPlugnTrigger(common.PlugnTriggerInput{
Trigger: "scheduler-run-retire",
StreamStdio: true,

View File

@@ -7,7 +7,7 @@ source "$PLUGIN_AVAILABLE_PATH/scheduler-docker-local/internal-functions"
fn-scheduler-docker-local-run-retire-container() {
declare desc="stop a container"
declare CONTAINER_ID="$1"
declare CONTAINER_ID="$1" APP="$2"
local DOKKU_DOCKER_STOP_TIMEOUT="$(plugn trigger ps-get-property "$APP" stop-timeout-seconds || true)"
@@ -18,34 +18,45 @@ fn-scheduler-docker-local-run-retire-container() {
"$DOCKER_BIN" container kill "$CONTAINER_ID" &>/dev/null || true
"$DOCKER_BIN" container rm "$CONTAINER_ID" &>/dev/null || true
if "$DOCKER_BIN" container inspect "$CONTAINER_ID" &>/dev/null; then
dokku_log_warn "Unable to retire container ${CONTAINER_ID}"
return 1
local CONTAINER_STATUS
if ! CONTAINER_STATUS="$("$DOCKER_BIN" container inspect "$CONTAINER_ID" --format '{{ .State.Status }}' 2>/dev/null)"; then
return
fi
# a container started with --rm is removed by the daemon once it exits, and
# stays inspectable in the removing state until that completes
if [[ "$CONTAINER_STATUS" == "removing" ]]; then
return
fi
dokku_log_warn "Unable to retire container ${CONTAINER_ID}"
return 1
}
fn-scheduler-docker-local-run-retire() {
declare desc="stop all run containers that have exceeded their active deadline"
declare APP="$1"
declare desc="stop all containers of a given type that have exceeded their active deadline"
declare CONTAINER_TYPE="$1" APP="$2"
local containers=""
# find all run containers for the specified app
container_type_filter="label=com.dokku.container-type=run"
# docker ands repeated label filters, so each container type requires its own pass
declare -a filters=(--filter "label=com.dokku.container-type=$CONTAINER_TYPE")
if [[ -n "$APP" ]]; then
app_filter="label=com.dokku.app-name=$APP"
containers="$("$DOCKER_BIN" container ls --filter "$container_type_filter" --filter "$app_filter" --format '{{.ID}} {{.CreatedAt}}')"
else
containers="$("$DOCKER_BIN" container ls --filter "$container_type_filter" --format '{{.ID}} {{.CreatedAt}}')"
filters+=(--filter "label=com.dokku.app-name=$APP")
fi
containers="$("$DOCKER_BIN" container ls "${filters[@]}" --format '{{.ID}} {{.CreatedAt}}')"
if [[ -z "$containers" ]]; then
return
fi
# iterate over all containers, ignoring the timezone in the last two columns
echo "$containers" | awk '{NF-=2} 1' | while read -r container_id container_date container_time; do
# get the cutoff time in seconds from the container's `com.dokku.active-deadline-seconds` label
active_deadline_seconds="$("$DOCKER_BIN" container inspect "$container_id" --format '{{ index .Config.Labels "com.dokku.active-deadline-seconds" }}')"
# get the cutoff time in seconds from the container's `com.dokku.active-deadline-seconds` label,
# along with the owning app so the app's stop timeout is respected during the global pass
container_labels="$("$DOCKER_BIN" container inspect "$container_id" --format '{{ index .Config.Labels "com.dokku.active-deadline-seconds" }}|{{ index .Config.Labels "com.dokku.app-name" }}')"
active_deadline_seconds="${container_labels%%|*}"
container_app="${container_labels##*|}"
if [[ -z "$active_deadline_seconds" ]]; then
continue
fi
@@ -59,28 +70,36 @@ fn-scheduler-docker-local-run-retire() {
# if the container start time is before the cutoff time, stop the container
if [[ "$start_time" -lt "$cutoff_time" ]]; then
dokku_log_verbose_quiet "Retiring container ${container_id}"
fn-scheduler-docker-local-run-retire-container "$container_id"
fn-scheduler-docker-local-run-retire-container "$container_id" "$container_app"
fi
done
}
trigger-scheduler-docker-local-scheduler-run-retire() {
declare desc="retires all run containers for an app that have exceeded their active deadline"
declare desc="retires all run and cron containers for an app that have exceeded their active deadline"
declare trigger="scheduler-run-retire"
declare DOKKU_SCHEDULER="$1" APP="$2"
declare -a CONTAINER_TYPES=(run cron)
local exit_code=0
if [[ -z "$DOKKU_SCHEDULER" ]]; then
dokku_log_info1_quiet "Retiring all run containers"
fn-scheduler-docker-local-run-retire
return "$?"
dokku_log_info1_quiet "Retiring all run and cron containers"
for container_type in "${CONTAINER_TYPES[@]}"; do
fn-scheduler-docker-local-run-retire "$container_type" || exit_code=1
done
return "$exit_code"
fi
if [[ "$DOKKU_SCHEDULER" != "docker-local" ]]; then
return
fi
dokku_log_info1_quiet "Retiring run containers for app ${APP}"
fn-scheduler-docker-local-run-retire "$APP"
dokku_log_info1_quiet "Retiring run and cron containers for app ${APP}"
for container_type in "${CONTAINER_TYPES[@]}"; do
fn-scheduler-docker-local-run-retire "$container_type" "$APP" || exit_code=1
done
return "$exit_code"
}
trigger-scheduler-docker-local-scheduler-run-retire "$@"

View File

@@ -577,12 +577,13 @@ func BuildAppChart(ctx context.Context, appName, imageTag string, opts BuildOpti
Args: words,
Annotations: annotations,
Cron: ProcessCron{
ID: cronTask.ID,
Hash: cronIDLabelValue(cronTask.ID),
Schedule: cronTask.Schedule,
Suffix: suffix,
Suspend: cronTask.Maintenance,
ConcurrencyPolicy: ProcessCronConcurrencyPolicy(concurrencyPolicy),
ID: cronTask.ID,
Hash: cronIDLabelValue(cronTask.ID),
Schedule: cronTask.Schedule,
Suffix: suffix,
Suspend: cronTask.Maintenance,
ConcurrencyPolicy: ProcessCronConcurrencyPolicy(concurrencyPolicy),
ActiveDeadlineSeconds: cron.DefaultTTLSeconds,
},
Labels: labels,
ProcessType: ProcessType_Cron,

View File

@@ -3,6 +3,7 @@ package scheduler_k3s
import (
"errors"
"io"
"maps"
"os"
"path/filepath"
"strings"
@@ -148,6 +149,61 @@ func TestCronJobTemplateRendersLongCronID(t *testing.T) {
})
}
// TestCronJobTemplateRendersActiveDeadlineSeconds asserts that the job's
// activeDeadlineSeconds comes from the chart values rather than a literal
// baked into the template, so the deadline cannot drift from the value the
// docker-local scheduler stamps onto its cron containers. A release rendered
// from values that predate the field must still fall back to 24 hours.
func TestCronJobTemplateRendersActiveDeadlineSeconds(t *testing.T) {
baseValues := map[string]interface{}{
"id": "abcde",
"hash": "0123456789abcdef0123456789abcdef01234567",
"schedule": "5 5 5 5 5",
"suffix": "abcde",
"suspend": false,
"concurrency_policy": "Allow",
}
t.Run("from values", func(t *testing.T) {
values := maps.Clone(baseValues)
values["active_deadline_seconds"] = 600
if got := renderedActiveDeadlineSeconds(t, renderCronJobTemplate(t, values)); got != 600 {
t.Errorf("activeDeadlineSeconds = %d, want 600", got)
}
})
t.Run("defaults when absent", func(t *testing.T) {
if got := renderedActiveDeadlineSeconds(t, renderCronJobTemplate(t, maps.Clone(baseValues))); got != 86400 {
t.Errorf("activeDeadlineSeconds = %d, want 86400", got)
}
})
}
// renderedActiveDeadlineSeconds pulls jobTemplate.spec.activeDeadlineSeconds
// out of a rendered cron-job manifest.
func renderedActiveDeadlineSeconds(t *testing.T, manifest string) int {
t.Helper()
var doc map[string]interface{}
if err := yaml.NewDecoder(strings.NewReader(manifest)).Decode(&doc); err != nil {
t.Fatalf("yaml decode failed (would also fail in Kubernetes API): %v\nrendered:\n%s", err, manifest)
}
spec, _ := doc["spec"].(map[string]interface{})
jobTemplate, _ := spec["jobTemplate"].(map[string]interface{})
jobSpec, ok := jobTemplate["spec"].(map[string]interface{})
if !ok {
t.Fatalf("jobTemplate.spec missing from rendered manifest:\n%s", manifest)
}
deadline, ok := jobSpec["activeDeadlineSeconds"].(int)
if !ok {
t.Fatalf("activeDeadlineSeconds = %v (type %T); want an integer", jobSpec["activeDeadlineSeconds"], jobSpec["activeDeadlineSeconds"])
}
return deadline
}
// renderCronJobTemplate renders templates/chart/cron-job.yaml with the
// supplied cron-process values and returns the YAML manifest as a string.
// The chart is materialised in a tempdir alongside the shared _helpers.tpl

View File

@@ -306,12 +306,13 @@ const (
)
type ProcessCron struct {
ID string `yaml:"id"`
Hash string `yaml:"hash"`
Schedule string `yaml:"schedule"`
Suffix string `yaml:"suffix"`
Suspend bool `yaml:"suspend"`
ConcurrencyPolicy ProcessCronConcurrencyPolicy `yaml:"concurrency_policy"`
ID string `yaml:"id"`
Hash string `yaml:"hash"`
Schedule string `yaml:"schedule"`
Suffix string `yaml:"suffix"`
Suspend bool `yaml:"suspend"`
ConcurrencyPolicy ProcessCronConcurrencyPolicy `yaml:"concurrency_policy"`
ActiveDeadlineSeconds int64 `yaml:"active_deadline_seconds"`
}
type ProcessCronConcurrencyPolicy string

View File

@@ -52,7 +52,7 @@ spec:
backoffLimit: 0
podReplacementPolicy: Failed
ttlSecondsAfterFinished: 60
activeDeadlineSeconds: 86400
activeDeadlineSeconds: {{ $config.cron.active_deadline_seconds | default 86400 }}
template:
metadata:
annotations:

View File

@@ -181,6 +181,7 @@ go-tests:
@$(MAKE) go-test-plugin PLUGIN_NAME=certs
@$(MAKE) go-test-plugin PLUGIN_NAME=common
@$(MAKE) go-test-plugin PLUGIN_NAME=config
@$(MAKE) go-test-plugin PLUGIN_NAME=cron
@$(MAKE) go-test-plugin PLUGIN_NAME=docker-options
@$(MAKE) go-test-plugin PLUGIN_NAME=logs
@$(MAKE) go-test-plugin PLUGIN_NAME=network

263
tests/unit/cron-1.bats Normal file
View File

@@ -0,0 +1,263 @@
#!/usr/bin/env bats
load test_helper
setup() {
global_setup
create_app
mkdir -p /var/lib/dokku/plugins/available/cron-entries
mkdir -p /var/lib/dokku/plugins/enabled/cron-entries
cat >"/var/lib/dokku/plugins/available/cron-entries/plugin.toml" <<EOF
[plugin]
description = "dokku test cron-entries plugin"
version = "0.0.1"
[plugin.config]
EOF
cp /var/lib/dokku/plugins/available/cron-entries/plugin.toml /var/lib/dokku/plugins/enabled/cron-entries/plugin.toml
}
teardown() {
rm -rf /var/lib/dokku/plugins/available/cron-entries /var/lib/dokku/plugins/enabled/cron-entries
# restore the default scheduler before destroy: a k3s-scheduled app cannot be
# torn down cleanly without a cluster, and would otherwise leak into the next
# test's cleanup_apps
dokku scheduler:set "$TEST_APP" selected docker-local 2>/dev/null || true
destroy_app
global_teardown
}
@test "(cron) cron:help" {
run /bin/bash -c "dokku cron"
echo "output: $output"
echo "status: $status"
assert_output_contains "Manage scheduled cron tasks"
help_output="$output"
run /bin/bash -c "dokku cron:help"
echo "output: $output"
echo "status: $status"
assert_output_contains "Manage scheduled cron tasks"
assert_output "$help_output"
}
@test "(cron) installed" {
run /bin/bash -c "test -f /etc/sudoers.d/dokku-cron"
echo "output: $output"
echo "status: $status"
assert_failure
}
@test "(cron) scheduler-uses-host-cron" {
run /bin/bash -c "dokku plugin:trigger scheduler-uses-host-cron docker-local"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "true"
run /bin/bash -c "dokku plugin:trigger scheduler-uses-host-cron k3s"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "false"
run /bin/bash -c "dokku plugin:trigger scheduler-uses-host-cron null"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "false"
run /bin/bash -c "dokku plugin:trigger scheduler-uses-host-cron bogus-scheduler"
echo "output: $output"
echo "status: $status"
assert_success
assert_output ""
}
@test "(cron) invalid [missing-keys]" {
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP template_cron_file_invalid
echo "output: $output"
echo "status: $status"
assert_failure
}
@test "(cron) invalid [schedule]" {
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP template_cron_file_invalid_schedule
echo "output: $output"
echo "status: $status"
assert_failure
}
@test "(cron) invalid [seconds]" {
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP template_cron_file_invalid_schedule_seconds
echo "output: $output"
echo "status: $status"
assert_failure
}
@test "(cron) create [single-verbose]" {
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP template_cron_file_valid_single
echo "output: $output"
echo "status: $status"
assert_success
cron_id="$(dokku cron:list $TEST_APP --format json | jq -r '.[0].id')"
run /bin/bash -c "cat /var/spool/cron/crontabs/dokku"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "dokku cron:run $TEST_APP $cron_id"
assert_output_contains "python3 task.py schedule" 0
}
@test "(cron) create [single-short]" {
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP template_cron_file_valid_short
echo "output: $output"
echo "status: $status"
assert_success
cron_id="$(dokku cron:list $TEST_APP --format json | jq -r '.[0].id')"
run /bin/bash -c "cat /var/spool/cron/crontabs/dokku"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "dokku cron:run $TEST_APP $cron_id"
assert_output_contains "python3 task.py daily" 0
}
@test "(cron) k3s-scheduled app skips host crontab" {
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP template_cron_file_valid_single
echo "output: $output"
echo "status: $status"
assert_success
cron_id="$(dokku cron:list $TEST_APP --format json | jq -r '.[0].id')"
run /bin/bash -c "cat /var/spool/cron/crontabs/dokku"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "dokku cron:run $TEST_APP $cron_id"
# switch the app to a scheduler that manages its own cron backend
run /bin/bash -c "dokku scheduler:set $TEST_APP selected k3s"
echo "output: $output"
echo "status: $status"
assert_success
# regenerating the host crontab must now skip the k3s app's tasks, leaving
# no host-cron tasks and therefore no crontab file
run /bin/bash -c "dokku plugin:trigger scheduler-cron-write"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "cat /var/spool/cron/crontabs/dokku"
echo "output: $output"
echo "status: $status"
assert_failure
}
@test "(cron) crontab format [no raw command leakage]" {
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP template_cron_file_valid_single
echo "output: $output"
echo "status: $status"
assert_success
cron_id="$(dokku cron:list $TEST_APP --format json | jq -r '.[0].id')"
run /bin/bash -c "cat /var/spool/cron/crontabs/dokku"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "dokku cron:run $TEST_APP $cron_id"
# User command must never appear verbatim in the crontab - the crontab
# only references the cron ID, and cron:run resolves the command at run
# time and exec's it inside the container.
assert_output_contains "python3 task.py schedule" 0
}
template_cron_file_invalid() {
local APP="$1"
local APP_REPO_DIR="$2"
[[ -z "$APP" ]] && local APP="$TEST_APP"
echo "injecting invalid cron app.json -> $APP_REPO_DIR/app.json"
cat <<EOF >"$APP_REPO_DIR/app.json"
{
"cron": [
{
"key": "value"
}
]
}
EOF
}
template_cron_file_invalid_schedule() {
local APP="$1"
local APP_REPO_DIR="$2"
[[ -z "$APP" ]] && local APP="$TEST_APP"
echo "injecting invalid cron app.json -> $APP_REPO_DIR/app.json"
cat <<EOF >"$APP_REPO_DIR/app.json"
{
"cron": [
{
"command": "python3 task.py",
"schedule": "@nonstandard"
}
]
}
EOF
}
template_cron_file_invalid_schedule_seconds() {
local APP="$1"
local APP_REPO_DIR="$2"
[[ -z "$APP" ]] && local APP="$TEST_APP"
echo "injecting invalid cron app.json -> $APP_REPO_DIR/app.json"
cat <<EOF >"$APP_REPO_DIR/app.json"
{
"cron": [
{
"command": "python3 task.py",
"schedule": "0 5 * * * *"
}
]
}
EOF
}
template_cron_file_valid_single() {
local APP="$1"
local APP_REPO_DIR="$2"
[[ -z "$APP" ]] && local APP="$TEST_APP"
echo "injecting valid cron app.json -> $APP_REPO_DIR/app.json"
cat <<EOF >"$APP_REPO_DIR/app.json"
{
"cron": [
{
"command": "python3 task.py schedule",
"schedule": "5 5 5 5 5"
}
]
}
EOF
}
template_cron_file_valid_short() {
local APP="$1"
local APP_REPO_DIR="$2"
[[ -z "$APP" ]] && local APP="$TEST_APP"
echo "injecting valid cron app.json -> $APP_REPO_DIR/app.json"
cat <<EOF >"$APP_REPO_DIR/app.json"
{
"cron": [
{
"command": "python3 task.py daily",
"schedule": "@daily"
}
]
}
EOF
}

411
tests/unit/cron-2.bats Normal file
View File

@@ -0,0 +1,411 @@
#!/usr/bin/env bats
load test_helper
setup() {
global_setup
create_app
mkdir -p /var/lib/dokku/plugins/available/cron-entries
mkdir -p /var/lib/dokku/plugins/enabled/cron-entries
cat >"/var/lib/dokku/plugins/available/cron-entries/plugin.toml" <<EOF
[plugin]
description = "dokku test cron-entries plugin"
version = "0.0.1"
[plugin.config]
EOF
cp /var/lib/dokku/plugins/available/cron-entries/plugin.toml /var/lib/dokku/plugins/enabled/cron-entries/plugin.toml
}
teardown() {
rm -rf /var/lib/dokku/plugins/available/cron-entries /var/lib/dokku/plugins/enabled/cron-entries
# restore the default scheduler before destroy: a k3s-scheduled app cannot be
# torn down cleanly without a cluster, and would otherwise leak into the next
# test's cleanup_apps
dokku scheduler:set "$TEST_APP" selected docker-local 2>/dev/null || true
destroy_app
global_teardown
}
@test "(cron:report) --global --format json" {
run /bin/bash -c "dokku cron:report --global --format json | jq -e ."
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku cron:report --global --format json | jq -r 'has(\"cron-maintenance\")'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "false"
run /bin/bash -c "dokku cron:report --global --format json | jq -r 'has(\"cron-global-maintenance\")'"
assert_success
assert_output "true"
run /bin/bash -c "dokku cron:report --global --format json | jq -r 'has(\"global-maintenance\")'"
assert_success
assert_output "true"
run /bin/bash -c "dokku cron:report --global --format json | jq -r 'has(\"cron-computed-maintenance\")'"
assert_success
assert_output "true"
run /bin/bash -c "dokku cron:report --global --format json | jq -r 'has(\"computed-maintenance\")'"
assert_success
assert_output "true"
run /bin/bash -c "dokku cron:report --global --format json | jq -r 'has(\"cron-global-mailfrom\")'"
assert_success
assert_output "true"
run /bin/bash -c "dokku cron:report --global --format json | jq -r 'has(\"global-mailfrom\")'"
assert_success
assert_output "true"
run /bin/bash -c "dokku cron:report --global --format json | jq -r 'has(\"cron-computed-mailfrom\")'"
assert_success
assert_output "true"
run /bin/bash -c "dokku cron:report --global --format json | jq -r 'has(\"computed-mailfrom\")'"
assert_success
assert_output "true"
run /bin/bash -c "dokku cron:report --global --format json | jq -r 'has(\"cron-global-mailto\")'"
assert_success
assert_output "true"
run /bin/bash -c "dokku cron:report --global --format json | jq -r 'has(\"global-mailto\")'"
assert_success
assert_output "true"
run /bin/bash -c "dokku cron:report --global --format json | jq -r 'has(\"cron-computed-mailto\")'"
assert_success
assert_output "true"
run /bin/bash -c "dokku cron:report --global --format json | jq -r 'has(\"computed-mailto\")'"
assert_success
assert_output "true"
run /bin/bash -c "dokku cron:report --global"
echo "output: $output"
echo "status: $status"
assert_success
}
@test "(cron:set) --global mailto" {
run /bin/bash -c "dokku cron:set --global mailto admin@example.com"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_not_contains "unknown flag"
run /bin/bash -c "dokku cron:report --global --format json | jq -r '.\"cron-global-mailto\"'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "admin@example.com"
run /bin/bash -c "dokku cron:report --global --format json | jq -r '.\"cron-computed-mailto\"'"
assert_success
assert_output "admin@example.com"
run /bin/bash -c "dokku cron:set --global mailto"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_not_contains "unknown flag"
run /bin/bash -c "dokku cron:report --global --format json | jq -r '.\"cron-global-mailto\"'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output ""
run /bin/bash -c "dokku cron:report --global --format json | jq -r '.\"cron-computed-mailto\"'"
assert_success
assert_output ""
}
@test "(cron:set) --global mailfrom" {
run /bin/bash -c "dokku cron:set --global mailfrom dokku@example.com"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_not_contains "unknown flag"
run /bin/bash -c "dokku cron:report --global --format json | jq -r '.\"cron-global-mailfrom\"'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "dokku@example.com"
run /bin/bash -c "dokku cron:report --global --format json | jq -r '.\"cron-computed-mailfrom\"'"
assert_success
assert_output "dokku@example.com"
run /bin/bash -c "dokku cron:set --global mailfrom"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_not_contains "unknown flag"
run /bin/bash -c "dokku cron:report --global --format json | jq -r '.\"cron-global-mailfrom\"'"
assert_success
assert_output ""
run /bin/bash -c "dokku cron:report --global --format json | jq -r '.\"cron-computed-mailfrom\"'"
assert_success
assert_output ""
}
@test "(cron:set) --global maintenance" {
run /bin/bash -c "dokku cron:set --global maintenance true"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_not_contains "unknown flag"
run /bin/bash -c "dokku cron:report --global --format json | jq -r '.\"cron-global-maintenance\"'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "true"
run /bin/bash -c "dokku cron:set --global maintenance"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_not_contains "unknown flag"
}
@test "(cron:report) maintenance raw vs computed vs global" {
run /bin/bash -c "dokku cron:set --global maintenance"
assert_success
run /bin/bash -c "dokku cron:report $TEST_APP --format json | jq -r '.\"cron-maintenance\"'"
assert_success
assert_output ""
run /bin/bash -c "dokku cron:report $TEST_APP --format json | jq -r '.\"cron-global-maintenance\"'"
assert_success
assert_output ""
run /bin/bash -c "dokku cron:report $TEST_APP --format json | jq -r '.\"cron-computed-maintenance\"'"
assert_success
assert_output "false"
run /bin/bash -c "dokku cron:set --global maintenance true"
assert_success
run /bin/bash -c "dokku cron:report $TEST_APP --format json | jq -r '.\"cron-global-maintenance\"'"
assert_success
assert_output "true"
run /bin/bash -c "dokku cron:report $TEST_APP --format json | jq -r '.\"cron-computed-maintenance\"'"
assert_success
assert_output "true"
run /bin/bash -c "dokku cron:set $TEST_APP maintenance true"
assert_success
run /bin/bash -c "dokku cron:report $TEST_APP --format json | jq -r '.\"cron-maintenance\"'"
assert_success
assert_output "true"
run /bin/bash -c "dokku cron:report $TEST_APP --format json | jq -r '.\"cron-global-maintenance\"'"
assert_success
assert_output "true"
# cron computed-maintenance returns "true" if either per-app or global is
# "true" (cron's logic at plugins/cron/report.go::reportComputedMaintenance
# is OR-not-override: any "true" wins). This is intentional for maintenance
# mode - a per-app "false" does NOT override a global "true".
run /bin/bash -c "dokku cron:report $TEST_APP --format json | jq -r '.\"cron-computed-maintenance\"'"
assert_success
assert_output "true"
run /bin/bash -c "dokku cron:set $TEST_APP maintenance"
assert_success
run /bin/bash -c "dokku cron:set --global maintenance"
assert_success
}
@test "(cron:set) --global rejects task maintenance properties" {
run /bin/bash -c "dokku cron:set --global maintenance.fakeid true"
echo "output: $output"
echo "status: $status"
assert_failure
assert_output_contains "Task maintenance properties cannot be set globally"
}
@test "(cron) create [multiple]" {
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP template_cron_file_valid_multiple
echo "output: $output"
echo "status: $status"
assert_success
first_id="$(dokku cron:list $TEST_APP --format json | jq -r '.[0].id')"
second_id="$(dokku cron:list $TEST_APP --format json | jq -r '.[1].id')"
run /bin/bash -c "cat /var/spool/cron/crontabs/dokku"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "dokku cron:run $TEST_APP $first_id"
assert_output_contains "dokku cron:run $TEST_APP $second_id"
assert_output_contains "python3 task.py first" 0
assert_output_contains "python3 task.py second" 0
}
@test "(cron) cron:list --format json" {
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP template_cron_file_valid
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku cron:list $TEST_APP --format json"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_exists
cron_id="$(dokku cron:list $TEST_APP --format json | jq -r '.[0].id')"
run /bin/bash -c "echo $cron_id"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_exists
}
@test "(cron) cron:run" {
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP template_cron_file_valid
echo "output: $output"
echo "status: $status"
assert_success
cron_id="$(dokku cron:list $TEST_APP --format json | jq -r '.[0].id')"
run /bin/bash -c "echo $cron_id"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_exists
run /bin/bash -c "dokku cron:run $TEST_APP $cron_id"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "['task.py', 'schedule']"
cron_id="$(dokku cron:list $TEST_APP --format json | jq -r '.[1].id')"
run /bin/bash -c "echo $cron_id"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_exists
run /bin/bash -c "dokku cron:run $TEST_APP $cron_id"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "['task.py', 'schedule', 'now']"
}
@test "(cron) cron:suspend cron:resume" {
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP template_cron_file_valid_multiple
echo "output: $output"
echo "status: $status"
assert_success
first_id="$(dokku cron:list $TEST_APP --format json | jq -r '.[0].id')"
second_id="$(dokku cron:list $TEST_APP --format json | jq -r '.[1].id')"
run /bin/bash -c "cat /var/spool/cron/crontabs/dokku"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "dokku cron:run $TEST_APP $first_id"
assert_output_contains "dokku cron:run $TEST_APP $second_id"
run /bin/bash -c "dokku cron:report $TEST_APP --cron-maintenance-$first_id"
echo "output: $output"
echo "status: $status"
assert_failure
run /bin/bash -c "dokku cron:suspend $TEST_APP $first_id"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku cron:report $TEST_APP --cron-maintenance-$first_id"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "true"
run /bin/bash -c "cat /var/spool/cron/crontabs/dokku"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "dokku cron:run $TEST_APP $first_id" 0
assert_output_contains "dokku cron:run $TEST_APP $second_id"
run /bin/bash -c "dokku cron:resume $TEST_APP $first_id"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku cron:report $TEST_APP --cron-maintenance-$first_id"
echo "output: $output"
echo "status: $status"
assert_failure
run /bin/bash -c "cat /var/spool/cron/crontabs/dokku"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "dokku cron:run $TEST_APP $first_id"
assert_output_contains "dokku cron:run $TEST_APP $second_id"
}
template_cron_file_valid() {
local APP="$1"
local APP_REPO_DIR="$2"
[[ -z "$APP" ]] && local APP="$TEST_APP"
echo "injecting valid cron app.json -> $APP_REPO_DIR/app.json"
cat <<EOF >"$APP_REPO_DIR/app.json"
{
"cron": [
{
"command": "python3 task.py schedule",
"schedule": "5 5 5 5 5"
},
{
"command": "python3 task.py schedule now",
"schedule": "6 5 5 5 5"
}
]
}
EOF
}
template_cron_file_valid_multiple() {
local APP="$1"
local APP_REPO_DIR="$2"
[[ -z "$APP" ]] && local APP="$TEST_APP"
echo "injecting valid cron app.json -> $APP_REPO_DIR/app.json"
cat <<EOF >"$APP_REPO_DIR/app.json"
{
"cron": [
{
"command": "python3 task.py first",
"schedule": "5 5 5 5 5"
},
{
"command": "python3 task.py second",
"schedule": "@daily"
}
]
}
EOF
}

310
tests/unit/cron-3.bats Normal file
View File

@@ -0,0 +1,310 @@
#!/usr/bin/env bats
load test_helper
setup() {
global_setup
create_app
mkdir -p /var/lib/dokku/plugins/available/cron-entries
mkdir -p /var/lib/dokku/plugins/enabled/cron-entries
cat >"/var/lib/dokku/plugins/available/cron-entries/plugin.toml" <<EOF
[plugin]
description = "dokku test cron-entries plugin"
version = "0.0.1"
[plugin.config]
EOF
cp /var/lib/dokku/plugins/available/cron-entries/plugin.toml /var/lib/dokku/plugins/enabled/cron-entries/plugin.toml
}
teardown() {
rm -rf /var/lib/dokku/plugins/available/cron-entries /var/lib/dokku/plugins/enabled/cron-entries
# restore the default scheduler before destroy: a k3s-scheduled app cannot be
# torn down cleanly without a cluster, and would otherwise leak into the next
# test's cleanup_apps
dokku scheduler:set "$TEST_APP" selected docker-local 2>/dev/null || true
destroy_app
global_teardown
}
@test "(cron) create [empty]" {
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "cat /var/spool/cron/crontabs/dokku"
echo "output: $output"
echo "status: $status"
assert_failure
}
@test "(cron) injected entries" {
echo "echo '@daily;/bin/true'" >/var/lib/dokku/plugins/enabled/cron-entries/cron-entries
chmod +x /var/lib/dokku/plugins/enabled/cron-entries/cron-entries
run /bin/bash -c "dokku plugin:trigger scheduler-cron-write"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku cron:list --global --format json"
echo "output: $output"
echo "status: $status"
assert_success
assert_output '[{"id":"5cruaotm4yzzpnjlsdunblj8qyjp","command":"/bin/true","global":true,"schedule":"@daily","concurrency_policy":"","app-in-maintenance":false,"task-in-maintenance":false,"maintenance":false}]'
run /bin/bash -c "cat /var/spool/cron/crontabs/dokku"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "@daily /bin/true"
# log file
echo "echo '@daily;/bin/true;/var/log/dokku/log.log'" >/var/lib/dokku/plugins/enabled/cron-entries/cron-entries
chmod +x /var/lib/dokku/plugins/enabled/cron-entries/cron-entries
run /bin/bash -c "dokku plugin:trigger scheduler-cron-write"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "cat /var/spool/cron/crontabs/dokku"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "@daily /bin/true &>> /var/log/dokku/log.log"
# specify matching scheduler
echo "[[ \$1 == 'docker-local' ]] && echo '@daily;/bin/true'" >/var/lib/dokku/plugins/enabled/cron-entries/cron-entries
chmod +x /var/lib/dokku/plugins/enabled/cron-entries/cron-entries
run /bin/bash -c "dokku plugin:trigger scheduler-cron-write"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "cat /var/spool/cron/crontabs/dokku"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "@daily /bin/true"
# specify non-matching scheduler
echo "[[ \$1 == 'kubernetes' ]] && echo '@daily;/bin/true'" >/var/lib/dokku/plugins/enabled/cron-entries/cron-entries
chmod +x /var/lib/dokku/plugins/enabled/cron-entries/cron-entries
run /bin/bash -c "dokku plugin:trigger scheduler-cron-write"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "cat /var/spool/cron/crontabs/dokku"
echo "output: $output"
echo "status: $status"
assert_failure
}
@test "(cron) invalid [command]" {
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP template_cron_file_injection
echo "output: $output"
echo "status: $status"
assert_failure
}
@test "(cron) cron:run concurrency_policy forbid" {
run deploy_app dockerfile dokku@$DOKKU_DOMAIN:$TEST_APP template_cron_file_concurrency_forbid
echo "output: $output"
echo "status: $status"
assert_success
cron_id="$(dokku cron:list $TEST_APP --format json | jq -r '.[0].id')"
run /bin/bash -c "echo cron $cron_id"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_exists
run /bin/bash -c "dokku cron:run $TEST_APP $cron_id --detach"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "docker ps --filter "label=com.dokku.cron-id=$cron_id" -q | xargs docker inspect -f '{{ index .Config.Labels \"com.dokku.concurrency-policy\" }}'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "forbid"
run /bin/bash -c "dokku cron:run $TEST_APP $cron_id"
echo "output: $output"
echo "status: $status"
assert_output_contains "currently has a cron lock in place for $cron_id"
assert_failure
}
@test "(cron) container labels regression" {
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP template_cron_file_long_running
echo "output: $output"
echo "status: $status"
assert_success
cron_id="$(dokku cron:list $TEST_APP --format json | jq -r '.[0].id')"
run /bin/bash -c "dokku cron:run $TEST_APP $cron_id --detach"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "docker ps --filter \"label=com.dokku.cron-id=$cron_id\" -q | xargs docker inspect -f '{{ index .Config.Labels \"com.dokku.cron-id\" }}'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "$cron_id"
run /bin/bash -c "docker ps --filter \"label=com.dokku.cron-id=$cron_id\" -q | xargs docker inspect -f '{{ index .Config.Labels \"com.dokku.concurrency-policy\" }}'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "allow"
run /bin/bash -c "docker ps --filter \"label=com.dokku.cron-id=$cron_id\" -q | xargs docker inspect -f '{{ index .Config.Labels \"com.dokku.container-type\" }}'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "cron"
run /bin/bash -c "docker ps --filter \"label=com.dokku.cron-id=$cron_id\" -q | xargs docker inspect -f '{{ index .Config.Labels \"com.dokku.app-name\" }}'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "$TEST_APP"
run /bin/bash -c "docker ps --filter \"label=com.dokku.cron-id=$cron_id\" -q | xargs docker inspect -f '{{ index .Config.Labels \"com.dokku.active-deadline-seconds\" }}'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "86400"
}
@test "(cron:run) retires containers past their active deadline" {
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP template_cron_file_long_running
echo "output: $output"
echo "status: $status"
assert_success
cron_id="$(dokku cron:list $TEST_APP --format json | jq -r '.[0].id')"
run /bin/bash -c "dokku cron:run $TEST_APP $cron_id --detach --ttl-seconds 1"
echo "output: $output"
echo "status: $status"
assert_success
expiring_container="$output"
run /bin/bash -c "docker container inspect $expiring_container --format '{{ index .Config.Labels \"com.dokku.active-deadline-seconds\" }}'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "1"
run /bin/bash -c "docker container inspect $expiring_container --format '{{ index .Config.Labels \"com.dokku.container-type\" }}'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "cron"
run /bin/bash -c "dokku cron:run $TEST_APP $cron_id --detach --ttl-seconds 300"
echo "output: $output"
echo "status: $status"
assert_success
surviving_container="$output"
run /bin/bash -c "sleep 2"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku ps:retire"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "docker container ls --filter \"label=com.dokku.container-type=cron\" --format '{{ .Names }}'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_not_contains "$expiring_container"
assert_output_contains "$surviving_container"
}
@test "(cron:run) --ttl-seconds must be a positive integer" {
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP template_cron_file_long_running
echo "output: $output"
echo "status: $status"
assert_success
cron_id="$(dokku cron:list $TEST_APP --format json | jq -r '.[0].id')"
run /bin/bash -c "dokku cron:run $TEST_APP $cron_id --detach --ttl-seconds 0"
echo "output: $output"
echo "status: $status"
assert_failure
assert_output_contains "--ttl-seconds must be a positive integer"
run /bin/bash -c "dokku cron:run $TEST_APP $cron_id --detach --ttl-seconds -1"
echo "output: $output"
echo "status: $status"
assert_failure
assert_output_contains "--ttl-seconds must be a positive integer"
}
template_cron_file_injection() {
local APP="$1"
local APP_REPO_DIR="$2"
[[ -z "$APP" ]] && local APP="$TEST_APP"
echo "injecting injection-attempt cron app.json -> $APP_REPO_DIR/app.json"
cat <<EOF >"$APP_REPO_DIR/app.json"
{
"cron": [
{
"command": "echo CRON_OK; echo hi > /tmp/appjson-injection-test.txt",
"schedule": "* * * * *"
}
]
}
EOF
}
template_cron_file_concurrency_forbid() {
local APP="$1"
local APP_REPO_DIR="$2"
[[ -z "$APP" ]] && local APP="$TEST_APP"
echo "injecting valid cron app.json -> $APP_REPO_DIR/app.json"
cat <<EOF >"$APP_REPO_DIR/app.json"
{
"cron": [
{
"command": "sleep 30",
"schedule": "0 0 * * *",
"concurrency_policy": "forbid"
}
]
}
EOF
}
template_cron_file_long_running() {
local APP="$1"
local APP_REPO_DIR="$2"
[[ -z "$APP" ]] && local APP="$TEST_APP"
echo "injecting long-running cron app.json -> $APP_REPO_DIR/app.json"
cat <<EOF >"$APP_REPO_DIR/app.json"
{
"cron": [
{
"command": "sleep 30",
"schedule": "0 0 * * *"
}
]
}
EOF
}

View File

@@ -1,858 +0,0 @@
#!/usr/bin/env bats
load test_helper
setup() {
global_setup
create_app
mkdir -p /var/lib/dokku/plugins/available/cron-entries
mkdir -p /var/lib/dokku/plugins/enabled/cron-entries
cat >"/var/lib/dokku/plugins/available/cron-entries/plugin.toml" <<EOF
[plugin]
description = "dokku test cron-entries plugin"
version = "0.0.1"
[plugin.config]
EOF
cp /var/lib/dokku/plugins/available/cron-entries/plugin.toml /var/lib/dokku/plugins/enabled/cron-entries/plugin.toml
}
teardown() {
rm -rf /var/lib/dokku/plugins/available/cron-entries /var/lib/dokku/plugins/enabled/cron-entries
# restore the default scheduler before destroy: a k3s-scheduled app cannot be
# torn down cleanly without a cluster, and would otherwise leak into the next
# test's cleanup_apps
dokku scheduler:set "$TEST_APP" selected docker-local 2>/dev/null || true
destroy_app
global_teardown
}
@test "(cron) cron:help" {
run /bin/bash -c "dokku cron"
echo "output: $output"
echo "status: $status"
assert_output_contains "Manage scheduled cron tasks"
help_output="$output"
run /bin/bash -c "dokku cron:help"
echo "output: $output"
echo "status: $status"
assert_output_contains "Manage scheduled cron tasks"
assert_output "$help_output"
}
@test "(cron) installed" {
run /bin/bash -c "test -f /etc/sudoers.d/dokku-cron"
echo "output: $output"
echo "status: $status"
assert_failure
}
@test "(cron) scheduler-uses-host-cron" {
run /bin/bash -c "dokku plugin:trigger scheduler-uses-host-cron docker-local"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "true"
run /bin/bash -c "dokku plugin:trigger scheduler-uses-host-cron k3s"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "false"
run /bin/bash -c "dokku plugin:trigger scheduler-uses-host-cron null"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "false"
run /bin/bash -c "dokku plugin:trigger scheduler-uses-host-cron bogus-scheduler"
echo "output: $output"
echo "status: $status"
assert_success
assert_output ""
}
@test "(cron) invalid [missing-keys]" {
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP template_cron_file_invalid
echo "output: $output"
echo "status: $status"
assert_failure
}
@test "(cron) invalid [schedule]" {
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP template_cron_file_invalid_schedule
echo "output: $output"
echo "status: $status"
assert_failure
}
@test "(cron) invalid [seconds]" {
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP template_cron_file_invalid_schedule_seconds
echo "output: $output"
echo "status: $status"
assert_failure
}
@test "(cron) create [empty]" {
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "cat /var/spool/cron/crontabs/dokku"
echo "output: $output"
echo "status: $status"
assert_failure
}
@test "(cron) create [single-verbose]" {
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP template_cron_file_valid_single
echo "output: $output"
echo "status: $status"
assert_success
cron_id="$(dokku cron:list $TEST_APP --format json | jq -r '.[0].id')"
run /bin/bash -c "cat /var/spool/cron/crontabs/dokku"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "dokku cron:run $TEST_APP $cron_id"
assert_output_contains "python3 task.py schedule" 0
}
@test "(cron) create [single-short]" {
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP template_cron_file_valid_short
echo "output: $output"
echo "status: $status"
assert_success
cron_id="$(dokku cron:list $TEST_APP --format json | jq -r '.[0].id')"
run /bin/bash -c "cat /var/spool/cron/crontabs/dokku"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "dokku cron:run $TEST_APP $cron_id"
assert_output_contains "python3 task.py daily" 0
}
@test "(cron:report) --global --format json" {
run /bin/bash -c "dokku cron:report --global --format json | jq -e ."
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku cron:report --global --format json | jq -r 'has(\"cron-maintenance\")'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "false"
run /bin/bash -c "dokku cron:report --global --format json | jq -r 'has(\"cron-global-maintenance\")'"
assert_success
assert_output "true"
run /bin/bash -c "dokku cron:report --global --format json | jq -r 'has(\"global-maintenance\")'"
assert_success
assert_output "true"
run /bin/bash -c "dokku cron:report --global --format json | jq -r 'has(\"cron-computed-maintenance\")'"
assert_success
assert_output "true"
run /bin/bash -c "dokku cron:report --global --format json | jq -r 'has(\"computed-maintenance\")'"
assert_success
assert_output "true"
run /bin/bash -c "dokku cron:report --global --format json | jq -r 'has(\"cron-global-mailfrom\")'"
assert_success
assert_output "true"
run /bin/bash -c "dokku cron:report --global --format json | jq -r 'has(\"global-mailfrom\")'"
assert_success
assert_output "true"
run /bin/bash -c "dokku cron:report --global --format json | jq -r 'has(\"cron-computed-mailfrom\")'"
assert_success
assert_output "true"
run /bin/bash -c "dokku cron:report --global --format json | jq -r 'has(\"computed-mailfrom\")'"
assert_success
assert_output "true"
run /bin/bash -c "dokku cron:report --global --format json | jq -r 'has(\"cron-global-mailto\")'"
assert_success
assert_output "true"
run /bin/bash -c "dokku cron:report --global --format json | jq -r 'has(\"global-mailto\")'"
assert_success
assert_output "true"
run /bin/bash -c "dokku cron:report --global --format json | jq -r 'has(\"cron-computed-mailto\")'"
assert_success
assert_output "true"
run /bin/bash -c "dokku cron:report --global --format json | jq -r 'has(\"computed-mailto\")'"
assert_success
assert_output "true"
run /bin/bash -c "dokku cron:report --global"
echo "output: $output"
echo "status: $status"
assert_success
}
@test "(cron:set) --global mailto" {
run /bin/bash -c "dokku cron:set --global mailto admin@example.com"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_not_contains "unknown flag"
run /bin/bash -c "dokku cron:report --global --format json | jq -r '.\"cron-global-mailto\"'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "admin@example.com"
run /bin/bash -c "dokku cron:report --global --format json | jq -r '.\"cron-computed-mailto\"'"
assert_success
assert_output "admin@example.com"
run /bin/bash -c "dokku cron:set --global mailto"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_not_contains "unknown flag"
run /bin/bash -c "dokku cron:report --global --format json | jq -r '.\"cron-global-mailto\"'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output ""
run /bin/bash -c "dokku cron:report --global --format json | jq -r '.\"cron-computed-mailto\"'"
assert_success
assert_output ""
}
@test "(cron:set) --global mailfrom" {
run /bin/bash -c "dokku cron:set --global mailfrom dokku@example.com"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_not_contains "unknown flag"
run /bin/bash -c "dokku cron:report --global --format json | jq -r '.\"cron-global-mailfrom\"'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "dokku@example.com"
run /bin/bash -c "dokku cron:report --global --format json | jq -r '.\"cron-computed-mailfrom\"'"
assert_success
assert_output "dokku@example.com"
run /bin/bash -c "dokku cron:set --global mailfrom"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_not_contains "unknown flag"
run /bin/bash -c "dokku cron:report --global --format json | jq -r '.\"cron-global-mailfrom\"'"
assert_success
assert_output ""
run /bin/bash -c "dokku cron:report --global --format json | jq -r '.\"cron-computed-mailfrom\"'"
assert_success
assert_output ""
}
@test "(cron:set) --global maintenance" {
run /bin/bash -c "dokku cron:set --global maintenance true"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_not_contains "unknown flag"
run /bin/bash -c "dokku cron:report --global --format json | jq -r '.\"cron-global-maintenance\"'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "true"
run /bin/bash -c "dokku cron:set --global maintenance"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_not_contains "unknown flag"
}
@test "(cron:report) maintenance raw vs computed vs global" {
run /bin/bash -c "dokku cron:set --global maintenance"
assert_success
run /bin/bash -c "dokku cron:report $TEST_APP --format json | jq -r '.\"cron-maintenance\"'"
assert_success
assert_output ""
run /bin/bash -c "dokku cron:report $TEST_APP --format json | jq -r '.\"cron-global-maintenance\"'"
assert_success
assert_output ""
run /bin/bash -c "dokku cron:report $TEST_APP --format json | jq -r '.\"cron-computed-maintenance\"'"
assert_success
assert_output "false"
run /bin/bash -c "dokku cron:set --global maintenance true"
assert_success
run /bin/bash -c "dokku cron:report $TEST_APP --format json | jq -r '.\"cron-global-maintenance\"'"
assert_success
assert_output "true"
run /bin/bash -c "dokku cron:report $TEST_APP --format json | jq -r '.\"cron-computed-maintenance\"'"
assert_success
assert_output "true"
run /bin/bash -c "dokku cron:set $TEST_APP maintenance true"
assert_success
run /bin/bash -c "dokku cron:report $TEST_APP --format json | jq -r '.\"cron-maintenance\"'"
assert_success
assert_output "true"
run /bin/bash -c "dokku cron:report $TEST_APP --format json | jq -r '.\"cron-global-maintenance\"'"
assert_success
assert_output "true"
# cron computed-maintenance returns "true" if either per-app or global is
# "true" (cron's logic at plugins/cron/report.go::reportComputedMaintenance
# is OR-not-override: any "true" wins). This is intentional for maintenance
# mode - a per-app "false" does NOT override a global "true".
run /bin/bash -c "dokku cron:report $TEST_APP --format json | jq -r '.\"cron-computed-maintenance\"'"
assert_success
assert_output "true"
run /bin/bash -c "dokku cron:set $TEST_APP maintenance"
assert_success
run /bin/bash -c "dokku cron:set --global maintenance"
assert_success
}
@test "(cron:set) --global rejects task maintenance properties" {
run /bin/bash -c "dokku cron:set --global maintenance.fakeid true"
echo "output: $output"
echo "status: $status"
assert_failure
assert_output_contains "Task maintenance properties cannot be set globally"
}
@test "(cron) create [multiple]" {
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP template_cron_file_valid_multiple
echo "output: $output"
echo "status: $status"
assert_success
first_id="$(dokku cron:list $TEST_APP --format json | jq -r '.[0].id')"
second_id="$(dokku cron:list $TEST_APP --format json | jq -r '.[1].id')"
run /bin/bash -c "cat /var/spool/cron/crontabs/dokku"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "dokku cron:run $TEST_APP $first_id"
assert_output_contains "dokku cron:run $TEST_APP $second_id"
assert_output_contains "python3 task.py first" 0
assert_output_contains "python3 task.py second" 0
}
@test "(cron) injected entries" {
echo "echo '@daily;/bin/true'" >/var/lib/dokku/plugins/enabled/cron-entries/cron-entries
chmod +x /var/lib/dokku/plugins/enabled/cron-entries/cron-entries
run /bin/bash -c "dokku plugin:trigger scheduler-cron-write"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku cron:list --global --format json"
echo "output: $output"
echo "status: $status"
assert_success
assert_output '[{"id":"5cruaotm4yzzpnjlsdunblj8qyjp","command":"/bin/true","global":true,"schedule":"@daily","concurrency_policy":"","app-in-maintenance":false,"task-in-maintenance":false,"maintenance":false}]'
run /bin/bash -c "cat /var/spool/cron/crontabs/dokku"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "@daily /bin/true"
# log file
echo "echo '@daily;/bin/true;/var/log/dokku/log.log'" >/var/lib/dokku/plugins/enabled/cron-entries/cron-entries
chmod +x /var/lib/dokku/plugins/enabled/cron-entries/cron-entries
run /bin/bash -c "dokku plugin:trigger scheduler-cron-write"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "cat /var/spool/cron/crontabs/dokku"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "@daily /bin/true &>> /var/log/dokku/log.log"
# specify matching scheduler
echo "[[ \$1 == 'docker-local' ]] && echo '@daily;/bin/true'" >/var/lib/dokku/plugins/enabled/cron-entries/cron-entries
chmod +x /var/lib/dokku/plugins/enabled/cron-entries/cron-entries
run /bin/bash -c "dokku plugin:trigger scheduler-cron-write"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "cat /var/spool/cron/crontabs/dokku"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "@daily /bin/true"
# specify non-matching scheduler
echo "[[ \$1 == 'kubernetes' ]] && echo '@daily;/bin/true'" >/var/lib/dokku/plugins/enabled/cron-entries/cron-entries
chmod +x /var/lib/dokku/plugins/enabled/cron-entries/cron-entries
run /bin/bash -c "dokku plugin:trigger scheduler-cron-write"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "cat /var/spool/cron/crontabs/dokku"
echo "output: $output"
echo "status: $status"
assert_failure
}
@test "(cron) k3s-scheduled app skips host crontab" {
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP template_cron_file_valid_single
echo "output: $output"
echo "status: $status"
assert_success
cron_id="$(dokku cron:list $TEST_APP --format json | jq -r '.[0].id')"
run /bin/bash -c "cat /var/spool/cron/crontabs/dokku"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "dokku cron:run $TEST_APP $cron_id"
# switch the app to a scheduler that manages its own cron backend
run /bin/bash -c "dokku scheduler:set $TEST_APP selected k3s"
echo "output: $output"
echo "status: $status"
assert_success
# regenerating the host crontab must now skip the k3s app's tasks, leaving
# no host-cron tasks and therefore no crontab file
run /bin/bash -c "dokku plugin:trigger scheduler-cron-write"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "cat /var/spool/cron/crontabs/dokku"
echo "output: $output"
echo "status: $status"
assert_failure
}
@test "(cron) cron:list --format json" {
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP template_cron_file_valid
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku cron:list $TEST_APP --format json"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_exists
cron_id="$(dokku cron:list $TEST_APP --format json | jq -r '.[0].id')"
run /bin/bash -c "echo $cron_id"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_exists
}
@test "(cron) cron:run" {
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP template_cron_file_valid
echo "output: $output"
echo "status: $status"
assert_success
cron_id="$(dokku cron:list $TEST_APP --format json | jq -r '.[0].id')"
run /bin/bash -c "echo $cron_id"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_exists
run /bin/bash -c "dokku cron:run $TEST_APP $cron_id"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "['task.py', 'schedule']"
cron_id="$(dokku cron:list $TEST_APP --format json | jq -r '.[1].id')"
run /bin/bash -c "echo $cron_id"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_exists
run /bin/bash -c "dokku cron:run $TEST_APP $cron_id"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "['task.py', 'schedule', 'now']"
}
@test "(cron) cron:run concurrency_policy forbid" {
run deploy_app dockerfile dokku@$DOKKU_DOMAIN:$TEST_APP template_cron_file_concurrency_forbid
echo "output: $output"
echo "status: $status"
assert_success
cron_id="$(dokku cron:list $TEST_APP --format json | jq -r '.[0].id')"
run /bin/bash -c "echo cron $cron_id"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_exists
run /bin/bash -c "dokku cron:run $TEST_APP $cron_id --detach"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "docker ps --filter "label=com.dokku.cron-id=$cron_id" -q | xargs docker inspect -f '{{ index .Config.Labels \"com.dokku.concurrency-policy\" }}'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "forbid"
run /bin/bash -c "dokku cron:run $TEST_APP $cron_id"
echo "output: $output"
echo "status: $status"
assert_output_contains "currently has a cron lock in place for $cron_id"
assert_failure
}
@test "(cron) cron:suspend cron:resume" {
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP template_cron_file_valid_multiple
echo "output: $output"
echo "status: $status"
assert_success
first_id="$(dokku cron:list $TEST_APP --format json | jq -r '.[0].id')"
second_id="$(dokku cron:list $TEST_APP --format json | jq -r '.[1].id')"
run /bin/bash -c "cat /var/spool/cron/crontabs/dokku"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "dokku cron:run $TEST_APP $first_id"
assert_output_contains "dokku cron:run $TEST_APP $second_id"
run /bin/bash -c "dokku cron:report $TEST_APP --cron-maintenance-$first_id"
echo "output: $output"
echo "status: $status"
assert_failure
run /bin/bash -c "dokku cron:suspend $TEST_APP $first_id"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku cron:report $TEST_APP --cron-maintenance-$first_id"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "true"
run /bin/bash -c "cat /var/spool/cron/crontabs/dokku"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "dokku cron:run $TEST_APP $first_id" 0
assert_output_contains "dokku cron:run $TEST_APP $second_id"
run /bin/bash -c "dokku cron:resume $TEST_APP $first_id"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku cron:report $TEST_APP --cron-maintenance-$first_id"
echo "output: $output"
echo "status: $status"
assert_failure
run /bin/bash -c "cat /var/spool/cron/crontabs/dokku"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "dokku cron:run $TEST_APP $first_id"
assert_output_contains "dokku cron:run $TEST_APP $second_id"
}
@test "(cron) invalid [command]" {
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP template_cron_file_injection
echo "output: $output"
echo "status: $status"
assert_failure
}
@test "(cron) crontab format [no raw command leakage]" {
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP template_cron_file_valid_single
echo "output: $output"
echo "status: $status"
assert_success
cron_id="$(dokku cron:list $TEST_APP --format json | jq -r '.[0].id')"
run /bin/bash -c "cat /var/spool/cron/crontabs/dokku"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "dokku cron:run $TEST_APP $cron_id"
# User command must never appear verbatim in the crontab - the crontab
# only references the cron ID, and cron:run resolves the command at run
# time and exec's it inside the container.
assert_output_contains "python3 task.py schedule" 0
}
@test "(cron) container labels regression" {
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP template_cron_file_long_running
echo "output: $output"
echo "status: $status"
assert_success
cron_id="$(dokku cron:list $TEST_APP --format json | jq -r '.[0].id')"
run /bin/bash -c "dokku cron:run $TEST_APP $cron_id --detach"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "docker ps --filter \"label=com.dokku.cron-id=$cron_id\" -q | xargs docker inspect -f '{{ index .Config.Labels \"com.dokku.cron-id\" }}'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "$cron_id"
run /bin/bash -c "docker ps --filter \"label=com.dokku.cron-id=$cron_id\" -q | xargs docker inspect -f '{{ index .Config.Labels \"com.dokku.concurrency-policy\" }}'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "allow"
run /bin/bash -c "docker ps --filter \"label=com.dokku.cron-id=$cron_id\" -q | xargs docker inspect -f '{{ index .Config.Labels \"com.dokku.container-type\" }}'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "cron"
run /bin/bash -c "docker ps --filter \"label=com.dokku.cron-id=$cron_id\" -q | xargs docker inspect -f '{{ index .Config.Labels \"com.dokku.app-name\" }}'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "$TEST_APP"
run /bin/bash -c "docker ps --filter \"label=com.dokku.cron-id=$cron_id\" -q | xargs docker inspect -f '{{ index .Config.Labels \"com.dokku.active-deadline-seconds\" }}'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "86400"
}
template_cron_file_long_running() {
local APP="$1"
local APP_REPO_DIR="$2"
[[ -z "$APP" ]] && local APP="$TEST_APP"
echo "injecting long-running cron app.json -> $APP_REPO_DIR/app.json"
cat <<EOF >"$APP_REPO_DIR/app.json"
{
"cron": [
{
"command": "sleep 30",
"schedule": "0 0 * * *"
}
]
}
EOF
}
template_cron_file_injection() {
local APP="$1"
local APP_REPO_DIR="$2"
[[ -z "$APP" ]] && local APP="$TEST_APP"
echo "injecting injection-attempt cron app.json -> $APP_REPO_DIR/app.json"
cat <<EOF >"$APP_REPO_DIR/app.json"
{
"cron": [
{
"command": "echo CRON_OK; echo hi > /tmp/appjson-injection-test.txt",
"schedule": "* * * * *"
}
]
}
EOF
}
template_cron_file_invalid() {
local APP="$1"
local APP_REPO_DIR="$2"
[[ -z "$APP" ]] && local APP="$TEST_APP"
echo "injecting invalid cron app.json -> $APP_REPO_DIR/app.json"
cat <<EOF >"$APP_REPO_DIR/app.json"
{
"cron": [
{
"key": "value"
}
]
}
EOF
}
template_cron_file_invalid_schedule() {
local APP="$1"
local APP_REPO_DIR="$2"
[[ -z "$APP" ]] && local APP="$TEST_APP"
echo "injecting invalid cron app.json -> $APP_REPO_DIR/app.json"
cat <<EOF >"$APP_REPO_DIR/app.json"
{
"cron": [
{
"command": "python3 task.py",
"schedule": "@nonstandard"
}
]
}
EOF
}
template_cron_file_invalid_schedule_seconds() {
local APP="$1"
local APP_REPO_DIR="$2"
[[ -z "$APP" ]] && local APP="$TEST_APP"
echo "injecting invalid cron app.json -> $APP_REPO_DIR/app.json"
cat <<EOF >"$APP_REPO_DIR/app.json"
{
"cron": [
{
"command": "python3 task.py",
"schedule": "0 5 * * * *"
}
]
}
EOF
}
template_cron_file_valid() {
local APP="$1"
local APP_REPO_DIR="$2"
[[ -z "$APP" ]] && local APP="$TEST_APP"
echo "injecting valid cron app.json -> $APP_REPO_DIR/app.json"
cat <<EOF >"$APP_REPO_DIR/app.json"
{
"cron": [
{
"command": "python3 task.py schedule",
"schedule": "5 5 5 5 5"
},
{
"command": "python3 task.py schedule now",
"schedule": "6 5 5 5 5"
}
]
}
EOF
}
template_cron_file_valid_single() {
local APP="$1"
local APP_REPO_DIR="$2"
[[ -z "$APP" ]] && local APP="$TEST_APP"
echo "injecting valid cron app.json -> $APP_REPO_DIR/app.json"
cat <<EOF >"$APP_REPO_DIR/app.json"
{
"cron": [
{
"command": "python3 task.py schedule",
"schedule": "5 5 5 5 5"
}
]
}
EOF
}
template_cron_file_valid_short() {
local APP="$1"
local APP_REPO_DIR="$2"
[[ -z "$APP" ]] && local APP="$TEST_APP"
echo "injecting valid cron app.json -> $APP_REPO_DIR/app.json"
cat <<EOF >"$APP_REPO_DIR/app.json"
{
"cron": [
{
"command": "python3 task.py daily",
"schedule": "@daily"
}
]
}
EOF
}
template_cron_file_valid_multiple() {
local APP="$1"
local APP_REPO_DIR="$2"
[[ -z "$APP" ]] && local APP="$TEST_APP"
echo "injecting valid cron app.json -> $APP_REPO_DIR/app.json"
cat <<EOF >"$APP_REPO_DIR/app.json"
{
"cron": [
{
"command": "python3 task.py first",
"schedule": "5 5 5 5 5"
},
{
"command": "python3 task.py second",
"schedule": "@daily"
}
]
}
EOF
}
template_cron_file_concurrency_forbid() {
local APP="$1"
local APP_REPO_DIR="$2"
[[ -z "$APP" ]] && local APP="$TEST_APP"
echo "injecting valid cron app.json -> $APP_REPO_DIR/app.json"
cat <<EOF >"$APP_REPO_DIR/app.json"
{
"cron": [
{
"command": "sleep 30",
"schedule": "0 0 * * *",
"concurrency_policy": "forbid"
}
]
}
EOF
}