mirror of
https://github.com/dokku/dokku.git
synced 2026-08-29 10:08:53 +02:00
refactor: move host-crontab generation into cron plugin
Host-crontab generation for `app.json` cron tasks now lives in the `cron` plugin, gated by a new `scheduler-uses-host-cron` trigger that the `docker-local` scheduler answers true while self-managed schedulers such as `k3s` answer false. This lets any host-cron scheduler participate in normal `app.json` cron without coupling to `scheduler-docker-local` or duplicating the crontab writer, while the `k3s` scheduler continues to manage its own in-cluster cron jobs. Closes #8862.
This commit is contained in:
@@ -122,7 +122,7 @@ At this time, the following dokku commands are used to implement a complete sche
|
||||
- `apps:clone`: handles app cloning
|
||||
- triggers: post-app-clone-setup
|
||||
- `cron`: generates cron tasks for the app
|
||||
- triggers: scheduler-cron-write
|
||||
- triggers: scheduler-uses-host-cron, scheduler-cron-write
|
||||
- `deploy`: deploys app proceses and checks the status of a deploy
|
||||
- triggers: scheduler-app-status, scheduler-deploy, scheduler-is-deployed, scheduler-logs-failed
|
||||
- `enter`: enters a running container
|
||||
|
||||
@@ -2527,8 +2527,8 @@ DOKKU_SCHEDULER="$1"; APP="$2";
|
||||
> The scheduler plugin trigger apis are under development and may change
|
||||
> between minor releases until the 1.0 release.
|
||||
|
||||
- Description: Force triggers writing out cron tasks. Arguments are optional.
|
||||
- Invoked by: `ps:start`, `ps:stop`, `cron:set`
|
||||
- Description: Force triggers writing out cron tasks. Arguments are optional. The `cron` plugin implements this for host-crontab schedulers (regenerating the whole `dokku` user crontab when the scheduler uses host cron or when no scheduler is given); self-managed schedulers such as `k3s` implement it to update their own cron backend.
|
||||
- Invoked by: `ps:start`, `ps:stop`, `cron:set`, `apps:destroy`
|
||||
- Arguments: `$DOKKU_SCHEDULER $APP`
|
||||
- Example:
|
||||
|
||||
@@ -2989,6 +2989,30 @@ DOKKU_SCHEDULER="$1"; APP="$2"; REMOVE_CONTAINERS="$3";
|
||||
- Arguments: `$SCHEDULER $ENTRY_NAME $IMAGE [-- $cmd...]`
|
||||
- Flags: `--interactive` (stdin is open), `--tty` (stdin is a terminal), `--as-user <uid>` (override `entry.Chown`).
|
||||
|
||||
### `scheduler-uses-host-cron`
|
||||
|
||||
> [!WARNING]
|
||||
> The scheduler plugin trigger apis are under development and may change
|
||||
> between minor releases until the 1.0 release.
|
||||
|
||||
- Description: Reports whether the scheduler writes `app.json` cron tasks to the host `dokku` user crontab. Schedulers that use the host crontab (`docker-local`) echo `true`; schedulers that manage their own cron backend (`k3s`, which creates in-cluster CronJobs) echo `false`. The cron plugin reads this to decide which apps to include when regenerating the host crontab; a scheduler that does not implement the trigger is treated as `false`.
|
||||
- Invoked by: `cron`
|
||||
- Arguments: `$DOKKU_SCHEDULER`
|
||||
- Example:
|
||||
|
||||
```shell
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x
|
||||
DOKKU_SCHEDULER="$1";
|
||||
|
||||
if [[ "$DOKKU_SCHEDULER" != "custom-scheduler" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
echo "true"
|
||||
```
|
||||
|
||||
### `traefik-template-source`
|
||||
|
||||
- Description: Retrieves an alternative template for the traefik compose config
|
||||
|
||||
@@ -57,8 +57,8 @@ When running scheduled cron tasks, there are a few items to be aware of:
|
||||
- A `MAILTO` value can be set via the `cron:set` command.
|
||||
- A `MAILFROM` value can be set via the `cron:set` command.
|
||||
- Each scheduled task is executed within a one-off `run` container, and thus inherit any docker-options specified for `run` containers. Resources are never shared between scheduled tasks.
|
||||
- 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.
|
||||
- Scheduled cron tasks are supported on a per-scheduler basis. Schedulers that use the host crontab - such as `docker-local` - have their `app.json` cron tasks written to the `dokku` user crontab, while schedulers that manage their own cron backend - such as `k3s` - schedule them natively.
|
||||
- Tasks for _all_ apps managed by a host-crontab scheduler such as `docker-local` are written to a single crontab file owned by the `dokku` user. The `dokku` user's crontab should be considered reserved for this purpose.
|
||||
- The `command` is tokenized and exec'd directly inside the container. Shell features such as `;`, `&&`, `|`, and `>` are _not_ interpreted. Commands that contain a bare shell operator are rejected when `app.json` is validated at deploy time, so a malformed cron command will fail the deploy rather than silently fail to run. If shell semantics are required, wrap the command explicitly, for example `"sh -c 'do-thing > /var/log/x.log'"`.
|
||||
|
||||
|
||||
|
||||
1
plugins/cron/.gitignore
vendored
1
plugins/cron/.gitignore
vendored
@@ -7,4 +7,5 @@
|
||||
/install
|
||||
/post-*
|
||||
/report
|
||||
/scheduler-cron-write
|
||||
/scheduler-stop
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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
|
||||
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/post-deploy triggers/scheduler-cron-write triggers/scheduler-stop
|
||||
BUILD = commands subcommands triggers
|
||||
PLUGIN_NAME = cron
|
||||
|
||||
|
||||
252
plugins/cron/crontab.go
Normal file
252
plugins/cron/crontab.go
Normal file
@@ -0,0 +1,252 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/dokku/dokku/plugins/common"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
base36 "github.com/multiformats/go-base36"
|
||||
)
|
||||
|
||||
//go:embed templates/cron.tmpl
|
||||
var cronTemplate string
|
||||
|
||||
// usesHostCron reports whether the given scheduler writes its cron tasks to the
|
||||
// host crontab (as opposed to managing its own cron backend). An empty scheduler
|
||||
// or an unimplemented scheduler-uses-host-cron trigger is treated as false.
|
||||
func usesHostCron(scheduler string) bool {
|
||||
if scheduler == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
results, _ := common.CallPlugnTrigger(common.PlugnTriggerInput{
|
||||
Trigger: "scheduler-uses-host-cron",
|
||||
Args: []string{scheduler},
|
||||
})
|
||||
return results.StdoutContents() == "true"
|
||||
}
|
||||
|
||||
// hostCronSchedulers returns a map keyed by every distinct scheduler seen across
|
||||
// the given apps plus the global scheduler, with a boolean value indicating
|
||||
// whether that scheduler uses the host crontab. Deduplicating up front avoids
|
||||
// redundant scheduler-uses-host-cron dispatches and any shared-cache race across
|
||||
// concurrent task collection.
|
||||
func hostCronSchedulers(appSchedulers []string) map[string]bool {
|
||||
schedulers := map[string]bool{}
|
||||
for _, scheduler := range append(appSchedulers, common.GetGlobalScheduler()) {
|
||||
if scheduler == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := schedulers[scheduler]; ok {
|
||||
continue
|
||||
}
|
||||
schedulers[scheduler] = usesHostCron(scheduler)
|
||||
}
|
||||
return schedulers
|
||||
}
|
||||
|
||||
// injectedCronTasks parses the tasks injected via the cron-entries trigger for a
|
||||
// given scheduler. Each entry is newline delimited in the form
|
||||
// $SCHEDULE;$COMMAND[;$LOGFILE].
|
||||
func injectedCronTasks(scheduler string) ([]CronTask, error) {
|
||||
tasks := []CronTask{}
|
||||
response, _ := common.CallPlugnTrigger(common.PlugnTriggerInput{
|
||||
Trigger: "cron-entries",
|
||||
Args: []string{scheduler},
|
||||
})
|
||||
for _, line := range strings.Split(response.StdoutContents(), "\n") {
|
||||
if strings.TrimSpace(line) == "" {
|
||||
return []CronTask{}, nil
|
||||
}
|
||||
|
||||
parts := strings.Split(line, ";")
|
||||
if len(parts) != 2 && len(parts) != 3 {
|
||||
return []CronTask{}, fmt.Errorf("Invalid injected cron task: %v", line)
|
||||
}
|
||||
|
||||
id := base36.EncodeToStringLc([]byte(strings.Join(parts, ";;;")))
|
||||
task := CronTask{
|
||||
ID: id,
|
||||
Schedule: parts[0],
|
||||
AltCommand: parts[1],
|
||||
Maintenance: false,
|
||||
}
|
||||
if len(parts) == 3 {
|
||||
task.LogFile = parts[2]
|
||||
}
|
||||
tasks = append(tasks, task)
|
||||
}
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
// generateCronTasks returns all cron tasks that should be written to the host
|
||||
// crontab: the app.json cron tasks for every app whose scheduler uses the host
|
||||
// crontab, plus any tasks injected via the cron-entries trigger for each such
|
||||
// scheduler. Tasks in maintenance are omitted.
|
||||
func generateCronTasks() ([]CronTask, error) {
|
||||
apps, _ := common.UnfilteredDokkuApps()
|
||||
|
||||
appSchedulers := make([]string, len(apps))
|
||||
sg := new(errgroup.Group)
|
||||
for i, appName := range apps {
|
||||
i := i
|
||||
appName := appName
|
||||
sg.Go(func() error {
|
||||
appSchedulers[i] = common.GetAppScheduler(appName)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if err := sg.Wait(); err != nil {
|
||||
return []CronTask{}, err
|
||||
}
|
||||
|
||||
hostCron := hostCronSchedulers(appSchedulers)
|
||||
|
||||
g := new(errgroup.Group)
|
||||
results := make(chan []CronTask, len(apps)+len(hostCron))
|
||||
|
||||
for i, appName := range apps {
|
||||
if !hostCron[appSchedulers[i]] {
|
||||
continue
|
||||
}
|
||||
appName := appName
|
||||
g.Go(func() error {
|
||||
c, err := FetchCronTasks(FetchCronTasksInput{AppName: appName})
|
||||
if err != nil {
|
||||
results <- []CronTask{}
|
||||
common.LogWarn(err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
results <- c
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
for scheduler, isHostCron := range hostCron {
|
||||
if !isHostCron {
|
||||
continue
|
||||
}
|
||||
scheduler := scheduler
|
||||
g.Go(func() error {
|
||||
tasks, err := injectedCronTasks(scheduler)
|
||||
if err != nil {
|
||||
results <- []CronTask{}
|
||||
return err
|
||||
}
|
||||
|
||||
results <- tasks
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
err := g.Wait()
|
||||
close(results)
|
||||
|
||||
tasks := []CronTask{}
|
||||
if err != nil {
|
||||
return tasks, err
|
||||
}
|
||||
|
||||
for result := range results {
|
||||
for _, task := range result {
|
||||
if !task.Maintenance {
|
||||
tasks = append(tasks, task)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
// writeCronTab regenerates the dokku user crontab from every host-cron app. It
|
||||
// is always a full regeneration, so there is no clobbering when multiple
|
||||
// schedulers use the host crontab.
|
||||
func writeCronTab() error {
|
||||
tasks, err := generateCronTasks()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(tasks) == 0 {
|
||||
return deleteCrontab()
|
||||
}
|
||||
|
||||
mailfrom := common.PropertyGetDefault("cron", "--global", "mailfrom", DefaultProperties["mailfrom"])
|
||||
mailto := common.PropertyGetDefault("cron", "--global", "mailto", DefaultProperties["mailto"])
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Tasks": tasks,
|
||||
"Mailfrom": mailfrom,
|
||||
"Mailto": mailto,
|
||||
}
|
||||
|
||||
t, err := getCronTemplate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmpFile, err := os.CreateTemp(os.TempDir(), fmt.Sprintf("dokku-%s-%s", common.MustGetEnv("DOKKU_PID"), "WriteCronTab"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Cannot create temporary schedule file: %v", err)
|
||||
}
|
||||
|
||||
defer tmpFile.Close()
|
||||
defer os.Remove(tmpFile.Name())
|
||||
|
||||
if err := t.Execute(tmpFile, data); err != nil {
|
||||
return fmt.Errorf("Unable to template out schedule file: %v", err)
|
||||
}
|
||||
|
||||
result, err := common.CallExecCommand(common.ExecCommandInput{
|
||||
Command: "crontab",
|
||||
Args: []string{"-u", "dokku", tmpFile.Name()},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to update schedule file: %w", err)
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
return fmt.Errorf("Unable to update schedule file: %s", result.StderrContents())
|
||||
}
|
||||
|
||||
common.LogInfo1("Updated schedule file")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteCrontab removes the dokku user crontab
|
||||
func deleteCrontab() error {
|
||||
result, err := common.CallExecCommand(common.ExecCommandInput{
|
||||
Command: "crontab",
|
||||
Args: []string{"-l", "-u", "dokku"},
|
||||
})
|
||||
if err != nil || result.ExitCode != 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, err = common.CallExecCommand(common.ExecCommandInput{
|
||||
Command: "crontab",
|
||||
Args: []string{"-r", "-u", "dokku"},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to remove schedule file: %w", err)
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
return fmt.Errorf("Unable to remove schedule file: %s", result.StderrContents())
|
||||
}
|
||||
|
||||
common.LogInfo1("Removed")
|
||||
return nil
|
||||
}
|
||||
|
||||
// getCronTemplate parses the embedded cron template
|
||||
func getCronTemplate() (*template.Template, error) {
|
||||
t := template.New("cron")
|
||||
s := strings.TrimSpace(cronTemplate)
|
||||
return t.Parse(s)
|
||||
}
|
||||
@@ -9,6 +9,7 @@ require (
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/ryanuber/columnize v2.1.2+incompatible
|
||||
github.com/spf13/pflag v1.0.10
|
||||
golang.org/x/sync v0.22.0
|
||||
mvdan.cc/sh/v3 v3.13.1
|
||||
)
|
||||
|
||||
@@ -28,7 +29,6 @@ require (
|
||||
github.com/pkg/sftp v1.13.11 // indirect
|
||||
github.com/tailscale/hujson v0.0.0-20250605163823-992244df8c5a // indirect
|
||||
golang.org/x/crypto v0.54.0 // indirect
|
||||
golang.org/x/sync v0.22.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
k8s.io/utils v0.0.0-20240102154912-e7106e64919e // indirect
|
||||
)
|
||||
|
||||
@@ -44,9 +44,16 @@ func main() {
|
||||
case "post-delete":
|
||||
appName := flag.Arg(0)
|
||||
err = cron.TriggerPostDelete(appName)
|
||||
case "post-deploy":
|
||||
appName := flag.Arg(0)
|
||||
err = cron.TriggerPostDeploy(appName)
|
||||
case "report":
|
||||
appName := flag.Arg(0)
|
||||
err = cron.ReportSingleApp(appName, "", "")
|
||||
case "scheduler-cron-write":
|
||||
scheduler := flag.Arg(0)
|
||||
appName := flag.Arg(1)
|
||||
err = cron.TriggerSchedulerCronWrite(scheduler, appName)
|
||||
case "scheduler-stop":
|
||||
scheduler := flag.Arg(0)
|
||||
appName := flag.Arg(1)
|
||||
|
||||
@@ -98,11 +98,43 @@ func TriggerPostDelete(appName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TriggerSchedulerStop stops the scheduler for a given app container
|
||||
func TriggerSchedulerStop(scheduler string, appName string, removeContainers string) error {
|
||||
if scheduler != "docker-local" {
|
||||
// TriggerPostDeploy regenerates the cron schedule for a given app after it is
|
||||
// deployed. Dispatching scheduler-cron-write lets the cron plugin regenerate the
|
||||
// host crontab for host-cron schedulers while self-managed schedulers update
|
||||
// their own backends.
|
||||
func TriggerPostDeploy(appName string) error {
|
||||
scheduler := common.GetAppScheduler(appName)
|
||||
_, err := common.CallPlugnTrigger(common.PlugnTriggerInput{
|
||||
Trigger: "scheduler-cron-write",
|
||||
Args: []string{scheduler, appName},
|
||||
StreamStdio: true,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// TriggerSchedulerCronWrite regenerates the host crontab when no scheduler is
|
||||
// given (the letsencrypt "all apps" path) or when the given scheduler uses the
|
||||
// host crontab. Self-managed schedulers implement their own scheduler-cron-write
|
||||
// trigger and are no-ops here.
|
||||
func TriggerSchedulerCronWrite(scheduler string, appName string) error {
|
||||
if scheduler != "" && !usesHostCron(scheduler) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
return writeCronTab()
|
||||
}
|
||||
|
||||
// TriggerSchedulerStop regenerates the host crontab for a host-cron app after
|
||||
// its processes are stopped
|
||||
func TriggerSchedulerStop(scheduler string, appName string, removeContainers string) error {
|
||||
if !usesHostCron(scheduler) {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := common.CallPlugnTrigger(common.PlugnTriggerInput{
|
||||
Trigger: "scheduler-cron-write",
|
||||
Args: []string{scheduler, appName},
|
||||
StreamStdio: true,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
1
plugins/scheduler-docker-local/.gitignore
vendored
1
plugins/scheduler-docker-local/.gitignore
vendored
@@ -3,6 +3,5 @@
|
||||
/cron-*
|
||||
/report
|
||||
/report-subcommand
|
||||
/scheduler-cron-write
|
||||
/scheduler-storage-exec
|
||||
/subcommands/report
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
TRIGGERS = triggers/report triggers/scheduler-cron-write triggers/scheduler-storage-exec
|
||||
TRIGGERS = triggers/report triggers/scheduler-storage-exec
|
||||
BUILD = report-subcommand triggers
|
||||
PLUGIN_NAME = scheduler-docker-local
|
||||
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
package schedulerdockerlocal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/dokku/dokku/plugins/common"
|
||||
"github.com/dokku/dokku/plugins/cron"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
base36 "github.com/multiformats/go-base36"
|
||||
)
|
||||
|
||||
func deleteCrontab() error {
|
||||
result, err := common.CallExecCommand(common.ExecCommandInput{
|
||||
Command: "crontab",
|
||||
Args: []string{"-l", "-u", "dokku"},
|
||||
})
|
||||
if err != nil || result.ExitCode != 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, err = common.CallExecCommand(common.ExecCommandInput{
|
||||
Command: "crontab",
|
||||
Args: []string{"-r", "-u", "dokku"},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to remove schedule file: %w", err)
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
return fmt.Errorf("Unable to remove schedule file: %s", result.StderrContents())
|
||||
}
|
||||
|
||||
common.LogInfo1("Removed")
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateCronTasks() ([]cron.CronTask, error) {
|
||||
apps, _ := common.UnfilteredDokkuApps()
|
||||
|
||||
g := new(errgroup.Group)
|
||||
results := make(chan []cron.CronTask, len(apps)+1)
|
||||
for _, appName := range apps {
|
||||
appName := appName
|
||||
g.Go(func() error {
|
||||
scheduler := common.GetAppScheduler(appName)
|
||||
if scheduler != "docker-local" {
|
||||
results <- []cron.CronTask{}
|
||||
return nil
|
||||
}
|
||||
|
||||
c, err := cron.FetchCronTasks(cron.FetchCronTasksInput{AppName: appName})
|
||||
if err != nil {
|
||||
results <- []cron.CronTask{}
|
||||
common.LogWarn(err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
results <- c
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
g.Go(func() error {
|
||||
tasks := []cron.CronTask{}
|
||||
response, _ := common.CallPlugnTrigger(common.PlugnTriggerInput{
|
||||
Trigger: "cron-entries",
|
||||
Args: []string{"docker-local"},
|
||||
})
|
||||
for _, line := range strings.Split(response.StdoutContents(), "\n") {
|
||||
if strings.TrimSpace(line) == "" {
|
||||
results <- []cron.CronTask{}
|
||||
return nil
|
||||
}
|
||||
|
||||
parts := strings.Split(line, ";")
|
||||
if len(parts) != 2 && len(parts) != 3 {
|
||||
results <- []cron.CronTask{}
|
||||
return fmt.Errorf("Invalid injected cron task: %v", line)
|
||||
}
|
||||
|
||||
id := base36.EncodeToStringLc([]byte(strings.Join(parts, ";;;")))
|
||||
task := cron.CronTask{
|
||||
ID: id,
|
||||
Schedule: parts[0],
|
||||
AltCommand: parts[1],
|
||||
Maintenance: false,
|
||||
}
|
||||
if len(parts) == 3 {
|
||||
task.LogFile = parts[2]
|
||||
}
|
||||
tasks = append(tasks, task)
|
||||
}
|
||||
results <- tasks
|
||||
return nil
|
||||
})
|
||||
|
||||
err := g.Wait()
|
||||
close(results)
|
||||
|
||||
tasks := []cron.CronTask{}
|
||||
if err != nil {
|
||||
return tasks, err
|
||||
}
|
||||
|
||||
for result := range results {
|
||||
for _, task := range result {
|
||||
if !task.Maintenance {
|
||||
tasks = append(tasks, task)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
func writeCronTab(scheduler string) error {
|
||||
// allow empty scheduler, which means all apps (used by letsencrypt)
|
||||
if scheduler != "docker-local" && scheduler != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
tasks, err := generateCronTasks()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(tasks) == 0 {
|
||||
return deleteCrontab()
|
||||
}
|
||||
|
||||
resultfromResults, _ := common.CallPlugnTrigger(common.PlugnTriggerInput{
|
||||
Trigger: "cron-get-property",
|
||||
Args: []string{"--global", "mailfrom"},
|
||||
})
|
||||
mailfrom := resultfromResults.StdoutContents()
|
||||
|
||||
mailtoResults, _ := common.CallPlugnTrigger(common.PlugnTriggerInput{
|
||||
Trigger: "cron-get-property",
|
||||
Args: []string{"--global", "mailto"},
|
||||
})
|
||||
mailto := mailtoResults.StdoutContents()
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Tasks": tasks,
|
||||
"Mailfrom": mailfrom,
|
||||
"Mailto": mailto,
|
||||
}
|
||||
|
||||
t, err := getCronTemplate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmpFile, err := os.CreateTemp(os.TempDir(), fmt.Sprintf("dokku-%s-%s", common.MustGetEnv("DOKKU_PID"), "WriteCronTab"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Cannot create temporary schedule file: %v", err)
|
||||
}
|
||||
|
||||
defer tmpFile.Close()
|
||||
defer os.Remove(tmpFile.Name())
|
||||
|
||||
if err := t.Execute(tmpFile, data); err != nil {
|
||||
return fmt.Errorf("Unable to template out schedule file: %v", err)
|
||||
}
|
||||
|
||||
result, err := common.CallExecCommand(common.ExecCommandInput{
|
||||
Command: "crontab",
|
||||
Args: []string{"-u", "dokku", tmpFile.Name()},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to update schedule file: %w", err)
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
return fmt.Errorf("Unable to update schedule file: %s", result.StderrContents())
|
||||
}
|
||||
|
||||
common.LogInfo1("Updated schedule file")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getCronTemplate() (*template.Template, error) {
|
||||
t := template.New("cron")
|
||||
|
||||
templatePath := filepath.Join(common.MustGetEnv("PLUGIN_ENABLED_PATH"), "cron", "templates", "cron.tmpl")
|
||||
b, err := os.ReadFile(templatePath)
|
||||
if err != nil {
|
||||
return t, fmt.Errorf("Cannot read template file: %v", err)
|
||||
}
|
||||
|
||||
s := strings.TrimSpace(string(b))
|
||||
return t.Parse(s)
|
||||
}
|
||||
@@ -4,21 +4,16 @@ go 1.26.2
|
||||
|
||||
require (
|
||||
github.com/dokku/dokku/plugins/common v0.0.0-00010101000000-000000000000
|
||||
github.com/dokku/dokku/plugins/cron v0.0.0-00010101000000-000000000000
|
||||
github.com/dokku/dokku/plugins/storage v0.0.0-00010101000000-000000000000
|
||||
github.com/multiformats/go-base36 v0.2.0
|
||||
github.com/spf13/pflag v1.0.10
|
||||
golang.org/x/sync v0.22.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alexellis/go-execute/v2 v2.2.1 // indirect
|
||||
github.com/dokku/dokku/plugins/app-json v0.0.0-00010101000000-000000000000 // indirect
|
||||
github.com/dokku/dokku/plugins/docker-options v0.0.0-00010101000000-000000000000 // indirect
|
||||
github.com/fatih/color v1.19.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.23 // indirect
|
||||
@@ -27,21 +22,15 @@ require (
|
||||
github.com/otiai10/mint v1.6.3 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pkg/sftp v1.13.11 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/ryanuber/columnize v2.1.2+incompatible // indirect
|
||||
github.com/tailscale/hujson v0.0.0-20250605163823-992244df8c5a // indirect
|
||||
golang.org/x/crypto v0.54.0 // indirect
|
||||
golang.org/x/sync v0.22.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
k8s.io/utils v0.0.0-20240102154912-e7106e64919e // indirect
|
||||
mvdan.cc/sh/v3 v3.13.1 // indirect
|
||||
)
|
||||
|
||||
replace github.com/dokku/dokku/plugins/app-json => ../app-json
|
||||
|
||||
replace github.com/dokku/dokku/plugins/common => ../common
|
||||
|
||||
replace github.com/dokku/dokku/plugins/cron => ../cron
|
||||
|
||||
replace github.com/dokku/dokku/plugins/storage => ../storage
|
||||
|
||||
replace github.com/dokku/dokku/plugins/docker-options => ../docker-options
|
||||
|
||||
@@ -12,8 +12,6 @@ github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/U
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
@@ -26,8 +24,6 @@ github.com/mattn/go-isatty v0.0.23 h1:cYwCQTQf3HB6xUC+BtyCLZNr7IzbOmoZbmssVNzSyi
|
||||
github.com/mattn/go-isatty v0.0.23/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
||||
github.com/melbahja/goph v1.5.2 h1:2eoR45SLF3LyM6tnIhnpjakvXTjtQMJqOK/mp1PYojM=
|
||||
github.com/melbahja/goph v1.5.2/go.mod h1:T+5uoB1PDP6EeK2qXerf5gRh7b6IF8u37GK2ckEi9FU=
|
||||
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
|
||||
github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
|
||||
github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I=
|
||||
github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg=
|
||||
github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8=
|
||||
@@ -40,8 +36,6 @@ github.com/pkg/sftp v1.13.11 h1:0N92SLTB8JqASJB14ZLHHzFnBV8mG9zw4K7jghEFWuE=
|
||||
github.com/pkg/sftp v1.13.11/go.mod h1:uNkH9roSXglNJqM+glJJi+TQXQUm0fXFWqCFmT8hsN0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/ryanuber/columnize v2.1.2+incompatible h1:C89EOx/XBWwIXl8wm8OPJBd7kPF25UfsK2X7Ph/zCAk=
|
||||
@@ -50,8 +44,6 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tailscale/hujson v0.0.0-20250605163823-992244df8c5a h1:a6TNDN9CgG+cYjaeN8l2mc4kSz2iMiCDQxPEyltUV/I=
|
||||
github.com/tailscale/hujson v0.0.0-20250605163823-992244df8c5a/go.mod h1:EbW0wDK/qEUYI0A5bqq0C2kF8JTQwWONmGDBbzsxxHo=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||
@@ -68,7 +60,5 @@ golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
k8s.io/utils v0.0.0-20240102154912-e7106e64919e h1:eQ/4ljkx21sObifjzXwlPKpdGLrCfRziVtos3ofG/sQ=
|
||||
k8s.io/utils v0.0.0-20240102154912-e7106e64919e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
mvdan.cc/sh/v3 v3.13.1 h1:DP3TfgZhDkT7lerUdnp6PTGKyxxzz6T+cOlY/xEvfWk=
|
||||
mvdan.cc/sh/v3 v3.13.1/go.mod h1:lXJ8SexMvEVcHCoDvAGLZgFJ9Wsm2sulmoNEXGhYZD0=
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
|
||||
[[ $DOKKU_TRACE ]] && set -x
|
||||
|
||||
trigger-scheduler-docker-local-post-deploy() {
|
||||
declare desc="scheduler-docker-local post-deploy plugin trigger"
|
||||
declare trigger="post-deploy"
|
||||
declare APP="$1"
|
||||
|
||||
local DOKKU_SCHEDULER="$(get_app_scheduler "$APP")"
|
||||
plugn trigger scheduler-cron-write "$DOKKU_SCHEDULER" "$APP"
|
||||
}
|
||||
|
||||
trigger-scheduler-docker-local-post-deploy "$@"
|
||||
@@ -50,8 +50,6 @@ trigger-scheduler-docker-local-scheduler-stop() {
|
||||
"$DOCKER_BIN" container rm --force $DOKKU_APP_CIDS &>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
plugn trigger scheduler-cron-write "$DOKKU_SCHEDULER" "$APP"
|
||||
}
|
||||
|
||||
trigger-scheduler-docker-local-scheduler-stop "$@"
|
||||
|
||||
17
plugins/scheduler-docker-local/scheduler-uses-host-cron
Executable file
17
plugins/scheduler-docker-local/scheduler-uses-host-cron
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
[[ $DOKKU_TRACE ]] && set -x
|
||||
|
||||
trigger-scheduler-docker-local-scheduler-uses-host-cron() {
|
||||
declare desc="reports whether the scheduler writes cron tasks to the host crontab"
|
||||
declare trigger="scheduler-uses-host-cron"
|
||||
declare DOKKU_SCHEDULER="$1"
|
||||
|
||||
if [[ "$DOKKU_SCHEDULER" != "docker-local" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
echo "true"
|
||||
}
|
||||
|
||||
trigger-scheduler-docker-local-scheduler-uses-host-cron "$@"
|
||||
@@ -23,9 +23,6 @@ func main() {
|
||||
|
||||
var err error
|
||||
switch trigger {
|
||||
case "scheduler-cron-write":
|
||||
scheduler := flag.Arg(0)
|
||||
err = schedulerdockerlocal.TriggerSchedulerCronWrite(scheduler)
|
||||
case "scheduler-storage-exec":
|
||||
args := flag.Args()
|
||||
if len(args) < 3 {
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
package schedulerdockerlocal
|
||||
|
||||
// TriggerSchedulerCronWrite force updates the cron file for all apps
|
||||
func TriggerSchedulerCronWrite(scheduler string) error {
|
||||
return writeCronTab(scheduler)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
SUBCOMMANDS = subcommands/annotations:set subcommands/annotations:report subcommands/autoscaling-auth:set subcommands/autoscaling-auth:report subcommands/charts:report subcommands/charts:set subcommands/cluster:add subcommands/cluster:list subcommands/cluster:remove subcommands/ensure-charts subcommands/initialize subcommands/labels:set subcommands/labels:report subcommands/preview subcommands/profiles:add subcommands/profiles:list subcommands/profiles:remove subcommands/report subcommands/set subcommands/show-kubeconfig subcommands/uninstall
|
||||
TRIGGERS = triggers/core-post-deploy triggers/core-post-extract triggers/install triggers/post-app-clone-setup triggers/post-app-rename-setup triggers/post-certs-update triggers/post-certs-remove triggers/post-create triggers/post-delete triggers/report triggers/scheduler-app-status triggers/scheduler-deploy triggers/scheduler-enter triggers/scheduler-is-deployed triggers/scheduler-logs triggers/scheduler-proxy-config triggers/scheduler-proxy-logs triggers/scheduler-post-delete triggers/scheduler-run triggers/scheduler-run-list triggers/scheduler-stop triggers/scheduler-cron-write triggers/storage-create triggers/storage-destroy triggers/storage-status triggers/scheduler-storage-exec
|
||||
TRIGGERS = triggers/core-post-deploy triggers/core-post-extract triggers/install triggers/post-app-clone-setup triggers/post-app-rename-setup triggers/post-certs-update triggers/post-certs-remove triggers/post-create triggers/post-delete triggers/report triggers/scheduler-app-status triggers/scheduler-deploy triggers/scheduler-enter triggers/scheduler-is-deployed triggers/scheduler-logs triggers/scheduler-proxy-config triggers/scheduler-proxy-logs triggers/scheduler-post-delete triggers/scheduler-run triggers/scheduler-run-list triggers/scheduler-stop triggers/scheduler-cron-write triggers/scheduler-uses-host-cron triggers/storage-create triggers/storage-destroy triggers/storage-status triggers/scheduler-storage-exec
|
||||
BUILD = commands subcommands triggers
|
||||
PLUGIN_NAME = scheduler-k3s
|
||||
|
||||
|
||||
@@ -86,6 +86,9 @@ func main() {
|
||||
scheduler := flag.Arg(0)
|
||||
appName := flag.Arg(1)
|
||||
err = scheduler_k3s.TriggerSchedulerIsDeployed(scheduler, appName)
|
||||
case "scheduler-uses-host-cron":
|
||||
scheduler := flag.Arg(0)
|
||||
err = scheduler_k3s.TriggerSchedulerUsesHostCron(scheduler)
|
||||
case "scheduler-logs":
|
||||
var tail bool
|
||||
var quiet bool
|
||||
|
||||
@@ -637,6 +637,17 @@ func TriggerSchedulerIsDeployed(scheduler string, appName string) error {
|
||||
return fmt.Errorf("App %s is not deployed", appName)
|
||||
}
|
||||
|
||||
// TriggerSchedulerUsesHostCron reports that the k3s scheduler does not use the
|
||||
// host crontab; it manages its own in-cluster CronJobs
|
||||
func TriggerSchedulerUsesHostCron(scheduler string) error {
|
||||
if scheduler != "k3s" {
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Println("false")
|
||||
return nil
|
||||
}
|
||||
|
||||
// TriggerSchedulerEnter enters a container for a given application
|
||||
func TriggerSchedulerEnter(scheduler string, appName string, processType string, podName string, args []string) error {
|
||||
if scheduler != "k3s" {
|
||||
|
||||
17
plugins/scheduler-null/scheduler-uses-host-cron
Executable file
17
plugins/scheduler-null/scheduler-uses-host-cron
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
[[ $DOKKU_TRACE ]] && set -x
|
||||
|
||||
trigger-scheduler-null-scheduler-uses-host-cron() {
|
||||
declare desc="reports whether the scheduler writes cron tasks to the host crontab"
|
||||
declare trigger="scheduler-uses-host-cron"
|
||||
declare DOKKU_SCHEDULER="$1"
|
||||
|
||||
if [[ "$DOKKU_SCHEDULER" != "null" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
echo "false"
|
||||
}
|
||||
|
||||
trigger-scheduler-null-scheduler-uses-host-cron "$@"
|
||||
@@ -18,6 +18,10 @@ EOF
|
||||
|
||||
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
|
||||
}
|
||||
@@ -43,6 +47,32 @@ teardown() {
|
||||
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"
|
||||
@@ -406,6 +436,39 @@ teardown() {
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user