From 7b0d3b14bcf6a7b5ec7364394281634822476ccc Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 9 Mar 2025 05:12:31 -0400 Subject: [PATCH 01/16] fix: stop all app containers when calling ps:stop Closes #6872 --- plugins/scheduler-docker-local/scheduler-stop | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/plugins/scheduler-docker-local/scheduler-stop b/plugins/scheduler-docker-local/scheduler-stop index 8e336e95b..aaea1299e 100755 --- a/plugins/scheduler-docker-local/scheduler-stop +++ b/plugins/scheduler-docker-local/scheduler-stop @@ -4,6 +4,12 @@ set -eo pipefail source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" source "$PLUGIN_AVAILABLE_PATH/config/functions" +fn-stop-container() { + declare APP="$1" CID="$2" + "$DOCKER_BIN" container update --restart=no "$CID" &>/dev/null || true + "$DOCKER_BIN" container stop $DOCKER_STOP_TIME_ARG "$CID" &>/dev/null || true +} + trigger-scheduler-docker-local-scheduler-stop() { declare desc="scheduler-docker-local scheduler-stop plugin trigger" declare trigger="scheduler-stop" @@ -19,14 +25,22 @@ trigger-scheduler-docker-local-scheduler-stop() { [[ -n "$DOKKU_DOCKER_STOP_TIMEOUT" ]] && DOCKER_STOP_TIME_ARG="--time=${DOKKU_DOCKER_STOP_TIMEOUT}" if [[ -n "$DOKKU_APP_RUNNING_CONTAINER_IDS" ]]; then - # Disable the container restart policy - "$DOCKER_BIN" container update --restart=no $DOKKU_APP_RUNNING_CONTAINER_IDS &>/dev/null || true + for CID in $DOKKU_APP_RUNNING_CONTAINER_IDS; do + fn-stop-container "$APP" "$CID" + done + fi - "$DOCKER_BIN" container stop $DOCKER_STOP_TIME_ARG $DOKKU_APP_RUNNING_CONTAINER_IDS >/dev/null || true + # Stop all other containers related to the app + # fetch all containers with the com.dokku.app-name label + local DOKKU_APP_CIDS="$(docker container ls -q -f label=com.dokku.app-name="$APP")" + if [[ -n "$DOKKU_APP_CIDS" ]]; then + for CID in $DOKKU_APP_CIDS; do + fn-stop-container "$APP" "$CID" + done fi if [[ "$REMOVE_CONTAINERS" == "true" ]]; then - local DOKKU_APP_CIDS=$(get_app_container_ids "$APP") + local DOKKU_APP_CIDS="$(docker ps -q -f label=com.dokku.app-name="$APP")" if [[ -n "$DOKKU_APP_CIDS" ]]; then for CID in $DOKKU_APP_CIDS; do From 2dea993804e364329856543063d2bb1239d9952b Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 9 Mar 2025 05:23:02 -0400 Subject: [PATCH 02/16] fix: also stop k8s cron tasks when calling ps:stop --- plugins/scheduler-k3s/k8s.go | 29 +++++++++++++++++++++++++++++ plugins/scheduler-k3s/triggers.go | 9 +++++++++ 2 files changed, 38 insertions(+) diff --git a/plugins/scheduler-k3s/k8s.go b/plugins/scheduler-k3s/k8s.go index 2cb7a8095..494d5ab9a 100644 --- a/plugins/scheduler-k3s/k8s.go +++ b/plugins/scheduler-k3s/k8s.go @@ -940,3 +940,32 @@ func streamLogsFromRequest(ctx context.Context, request rest.ResponseWrapper, ou } } } + +// SuspendCronJobsInput contains all the information needed to suspend a Kubernetes cron job +type SuspendCronJobsInput struct { + // LabelSelector is the Kubernetes label selector + LabelSelector string + + // Namespace is the Kubernetes namespace + Namespace string +} + +// SuspendCronJobs suspends a Kubernetes cron job +func (k KubernetesClient) SuspendCronJobs(ctx context.Context, input SuspendCronJobsInput) error { + cronJobs, err := k.Client.BatchV1().CronJobs(input.Namespace).List(ctx, metav1.ListOptions{ + LabelSelector: input.LabelSelector, + }) + if err != nil { + return err + } + + for _, cronJob := range cronJobs.Items { + cronJob.Spec.Suspend = ptr.To(true) + _, err := k.Client.BatchV1().CronJobs(input.Namespace).Update(ctx, &cronJob, metav1.UpdateOptions{}) + if err != nil { + return err + } + } + + return nil +} diff --git a/plugins/scheduler-k3s/triggers.go b/plugins/scheduler-k3s/triggers.go index 5fa5e8b62..22fec8b5a 100644 --- a/plugins/scheduler-k3s/triggers.go +++ b/plugins/scheduler-k3s/triggers.go @@ -1499,5 +1499,14 @@ func TriggerSchedulerStop(scheduler string, appName string) error { } } + // get all cronjobs for the app + err = clientset.SuspendCronJobs(ctx, SuspendCronJobsInput{ + Namespace: namespace, + LabelSelector: fmt.Sprintf("app.kubernetes.io/part-of=%s", appName), + }) + if err != nil { + return fmt.Errorf("Error suspending cron jobs: %w", err) + } + return nil } From 398a78b2f6d92b0fb54f4c7bc63812f617fd12c9 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 9 Mar 2025 05:39:55 -0400 Subject: [PATCH 03/16] refactor: move cron-write to scheduler-cron-write --- .../schedulers/scheduler-management.md | 6 ++-- docs/development/plugin-triggers.md | 36 ++++++++++--------- plugins/cron/subcommands.go | 4 ++- plugins/scheduler-docker-local/Makefile | 2 +- plugins/scheduler-docker-local/post-delete | 2 +- plugins/scheduler-docker-local/post-deploy | 2 +- .../src/triggers/triggers.go | 2 +- tests/unit/cron.bats | 8 ++--- 8 files changed, 35 insertions(+), 27 deletions(-) diff --git a/docs/deployment/schedulers/scheduler-management.md b/docs/deployment/schedulers/scheduler-management.md index 0189092bf..4fac3ce0f 100644 --- a/docs/deployment/schedulers/scheduler-management.md +++ b/docs/deployment/schedulers/scheduler-management.md @@ -98,7 +98,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: cron-write + - triggers: 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 @@ -112,7 +112,9 @@ At this time, the following dokku commands are used to implement a complete sche - `run:logs`: fetches running container logs - triggers: scheduler-run-logs - `ps:stop`: stops app processes - - triggers: scheduler-stop + - triggers: scheduler-stop, scheduler-cron-write +- `ps:start`: starts app processes + - triggers: scheduler-deploy, scheduler-cron-write - `ps:inspect`: outputs inspect output for processes in an app - triggers: scheduler-inspect diff --git a/docs/development/plugin-triggers.md b/docs/development/plugin-triggers.md index a8551d54e..e37e22c90 100644 --- a/docs/development/plugin-triggers.md +++ b/docs/development/plugin-triggers.md @@ -583,22 +583,6 @@ DOKKU_SCHEDULER="$1" # TODO ``` -### `cron-write` - -- Description: Force triggers writing out cron entries -- Invoked by: -- Arguments: -- Example: - -```shell -#!/usr/bin/env bash - -set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" - -# TODO -``` - ### `dependencies` - Description: Used to install system-level dependencies. @@ -2445,6 +2429,26 @@ DOKKU_SCHEDULER="$1"; APP="$2"; # TODO ``` +### `scheduler-cron-write` + +> [!WARNING] +> 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 entries. Arguments are optional. +- Invoked by: `ps:start`, `ps:stop`, `cron:set` +- Arguments: `$DOKKU_SCHEDULER $APP` +- Example: + +```shell +#!/usr/bin/env bash + +set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x +source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" + +# TODO +``` + ### `scheduler-deploy` > [!WARNING] diff --git a/plugins/cron/subcommands.go b/plugins/cron/subcommands.go index 4e0e56462..aede7d39b 100644 --- a/plugins/cron/subcommands.go +++ b/plugins/cron/subcommands.go @@ -130,8 +130,10 @@ func CommandSet(appName string, property string, value string) error { } common.CommandPropertySet("cron", appName, property, value, DefaultProperties, GlobalProperties) + scheduler := common.GetAppScheduler(appName) _, err := common.CallPlugnTrigger(common.PlugnTriggerInput{ - Trigger: "cron-write", + Trigger: "scheduler-cron-write", + Args: []string{scheduler, appName}, StreamStdio: true, }) return err diff --git a/plugins/scheduler-docker-local/Makefile b/plugins/scheduler-docker-local/Makefile index 09f04eabb..bd7c75ae1 100644 --- a/plugins/scheduler-docker-local/Makefile +++ b/plugins/scheduler-docker-local/Makefile @@ -1,4 +1,4 @@ -TRIGGERS = triggers/cron-write +TRIGGERS = triggers/scheduler-cron-write BUILD = triggers PLUGIN_NAME = scheduler-docker-local diff --git a/plugins/scheduler-docker-local/post-delete b/plugins/scheduler-docker-local/post-delete index 472643544..703343f15 100755 --- a/plugins/scheduler-docker-local/post-delete +++ b/plugins/scheduler-docker-local/post-delete @@ -15,7 +15,7 @@ trigger-scheduler-docker-local-post-delete() { # remove all application containers "$DOCKER_BIN" container ls --filter "label=com.dokku.app-name=${APP}" -q | xargs -n1 -I {} "$DOCKER_BIN" container rm --force {} &>/dev/null || true - plugn trigger cron-write + plugn trigger scheduler-cron-write } trigger-scheduler-docker-local-post-delete "$@" diff --git a/plugins/scheduler-docker-local/post-deploy b/plugins/scheduler-docker-local/post-deploy index 3c19037f1..9541385ce 100755 --- a/plugins/scheduler-docker-local/post-deploy +++ b/plugins/scheduler-docker-local/post-deploy @@ -7,7 +7,7 @@ trigger-scheduler-docker-local-post-deploy() { declare trigger="post-deploy" declare APP="$1" - plugn trigger cron-write + plugn trigger scheduler-cron-write } trigger-scheduler-docker-local-post-deploy "$@" diff --git a/plugins/scheduler-docker-local/src/triggers/triggers.go b/plugins/scheduler-docker-local/src/triggers/triggers.go index 86b030d9d..d3ce7aa11 100644 --- a/plugins/scheduler-docker-local/src/triggers/triggers.go +++ b/plugins/scheduler-docker-local/src/triggers/triggers.go @@ -18,7 +18,7 @@ func main() { var err error switch trigger { - case "cron-write": + case "scheduler-cron-write": err = schedulerdockerlocal.TriggerCronWrite() default: err = fmt.Errorf("Invalid plugin trigger call: %s", trigger) diff --git a/tests/unit/cron.bats b/tests/unit/cron.bats index 097104af9..e22145aa7 100644 --- a/tests/unit/cron.bats +++ b/tests/unit/cron.bats @@ -121,7 +121,7 @@ teardown() { 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 cron-write" + run /bin/bash -c "dokku plugin:trigger scheduler-cron-write" echo "output: $output" echo "status: $status" assert_success @@ -136,7 +136,7 @@ teardown() { 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 cron-write" + run /bin/bash -c "dokku plugin:trigger scheduler-cron-write" echo "output: $output" echo "status: $status" assert_success @@ -151,7 +151,7 @@ teardown() { 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 cron-write" + run /bin/bash -c "dokku plugin:trigger scheduler-cron-write" echo "output: $output" echo "status: $status" assert_success @@ -166,7 +166,7 @@ teardown() { 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 cron-write" + run /bin/bash -c "dokku plugin:trigger scheduler-cron-write" echo "output: $output" echo "status: $status" assert_success From b663f71878d388a595f175623b4cebf16248a7fe Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 9 Mar 2025 05:51:27 -0400 Subject: [PATCH 04/16] feat: only write cron entries when working on docker-local scheduler --- plugins/scheduler-docker-local/functions.go | 7 ++++++- plugins/scheduler-docker-local/post-delete | 3 ++- plugins/scheduler-docker-local/post-deploy | 3 ++- plugins/scheduler-docker-local/scheduler-stop | 2 ++ plugins/scheduler-docker-local/src/triggers/triggers.go | 3 ++- plugins/scheduler-docker-local/triggers.go | 4 ++-- 6 files changed, 16 insertions(+), 6 deletions(-) diff --git a/plugins/scheduler-docker-local/functions.go b/plugins/scheduler-docker-local/functions.go index e6ec71eea..adf576314 100644 --- a/plugins/scheduler-docker-local/functions.go +++ b/plugins/scheduler-docker-local/functions.go @@ -115,7 +115,12 @@ func generateCronEntries() ([]cron.TemplateCommand, error) { return commands, nil } -func writeCronEntries() error { +func writeCronEntries(scheduler string) error { + // allow empty scheduler, which means all apps (used by letsencrypt) + if scheduler != "docker-local" && scheduler != "" { + return nil + } + commands, err := generateCronEntries() if err != nil { return err diff --git a/plugins/scheduler-docker-local/post-delete b/plugins/scheduler-docker-local/post-delete index 703343f15..cd8480f80 100755 --- a/plugins/scheduler-docker-local/post-delete +++ b/plugins/scheduler-docker-local/post-delete @@ -15,7 +15,8 @@ trigger-scheduler-docker-local-post-delete() { # remove all application containers "$DOCKER_BIN" container ls --filter "label=com.dokku.app-name=${APP}" -q | xargs -n1 -I {} "$DOCKER_BIN" container rm --force {} &>/dev/null || true - plugn trigger scheduler-cron-write + local DOKKU_SCHEDULER="$(get_app_scheduler "$APP")" + plugn trigger scheduler-cron-write "$DOKKU_SCHEDULER" "$APP" } trigger-scheduler-docker-local-post-delete "$@" diff --git a/plugins/scheduler-docker-local/post-deploy b/plugins/scheduler-docker-local/post-deploy index 9541385ce..325b1bd52 100755 --- a/plugins/scheduler-docker-local/post-deploy +++ b/plugins/scheduler-docker-local/post-deploy @@ -7,7 +7,8 @@ trigger-scheduler-docker-local-post-deploy() { declare trigger="post-deploy" declare APP="$1" - plugn trigger scheduler-cron-write + local DOKKU_SCHEDULER="$(get_app_scheduler "$APP")" + plugn trigger scheduler-cron-write "$DOKKU_SCHEDULER" "$APP" } trigger-scheduler-docker-local-post-deploy "$@" diff --git a/plugins/scheduler-docker-local/scheduler-stop b/plugins/scheduler-docker-local/scheduler-stop index aaea1299e..12fcb5400 100755 --- a/plugins/scheduler-docker-local/scheduler-stop +++ b/plugins/scheduler-docker-local/scheduler-stop @@ -50,6 +50,8 @@ 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 "$@" diff --git a/plugins/scheduler-docker-local/src/triggers/triggers.go b/plugins/scheduler-docker-local/src/triggers/triggers.go index d3ce7aa11..4d11721ca 100644 --- a/plugins/scheduler-docker-local/src/triggers/triggers.go +++ b/plugins/scheduler-docker-local/src/triggers/triggers.go @@ -19,7 +19,8 @@ func main() { var err error switch trigger { case "scheduler-cron-write": - err = schedulerdockerlocal.TriggerCronWrite() + scheduler := flag.Arg(0) + err = schedulerdockerlocal.TriggerCronWrite(scheduler) default: err = fmt.Errorf("Invalid plugin trigger call: %s", trigger) } diff --git a/plugins/scheduler-docker-local/triggers.go b/plugins/scheduler-docker-local/triggers.go index 018ff411e..785fc9f31 100644 --- a/plugins/scheduler-docker-local/triggers.go +++ b/plugins/scheduler-docker-local/triggers.go @@ -1,6 +1,6 @@ package schedulerdockerlocal // TriggerCronWrite force updates the cron file for all apps -func TriggerCronWrite() error { - return writeCronEntries() +func TriggerCronWrite(scheduler string) error { + return writeCronEntries(scheduler) } From 36da77a2ff47a62eca913b774f0fb4af6c4e8b60 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 9 Mar 2025 06:33:16 -0400 Subject: [PATCH 05/16] fix: write cron in pre-delete phase --- plugins/cron/Makefile | 2 +- plugins/cron/src/triggers/triggers.go | 3 +++ plugins/cron/triggers.go | 17 ++++++++++++++++- plugins/scheduler-docker-local/post-delete | 3 --- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/plugins/cron/Makefile b/plugins/cron/Makefile index 920941136..20ddf156e 100644 --- a/plugins/cron/Makefile +++ b/plugins/cron/Makefile @@ -1,5 +1,5 @@ SUBCOMMANDS = subcommands/list subcommands/report subcommands/run subcommands/set -TRIGGERS = triggers/cron-get-property triggers/install triggers/post-app-clone-setup triggers/post-app-rename-setup triggers/post-delete triggers/scheduler-stop +TRIGGERS = triggers/cron-get-property triggers/install triggers/post-app-clone-setup triggers/post-app-rename-setup triggers/pre-delete triggers/post-delete triggers/scheduler-stop BUILD = commands subcommands triggers PLUGIN_NAME = cron diff --git a/plugins/cron/src/triggers/triggers.go b/plugins/cron/src/triggers/triggers.go index 6e0de5e7a..a5a5ed98d 100644 --- a/plugins/cron/src/triggers/triggers.go +++ b/plugins/cron/src/triggers/triggers.go @@ -40,6 +40,9 @@ func main() { case "post-delete": appName := flag.Arg(0) err = cron.TriggerPostDelete(appName) + case "pre-delete": + appName := flag.Arg(0) + err = cron.TriggerPreDelete(appName) case "report": appName := flag.Arg(0) err = cron.ReportSingleApp(appName, "", "") diff --git a/plugins/cron/triggers.go b/plugins/cron/triggers.go index 321e2b8a9..3b5c5d929 100644 --- a/plugins/cron/triggers.go +++ b/plugins/cron/triggers.go @@ -55,9 +55,24 @@ func TriggerPostAppRenameSetup(oldAppName string, newAppName string) error { return nil } +// TriggerPreDelete stops cron for a given app +func TriggerPreDelete(appName string) error { + scheduler := common.GetAppScheduler(appName) + _, err := common.CallPlugnTrigger(common.PlugnTriggerInput{ + Trigger: "scheduler-cron-write", + Args: []string{scheduler, appName}, + StreamStdio: true, + }) + return err +} + // TriggerPostDelete destroys the cron property for a given app container func TriggerPostDelete(appName string) error { - return common.PropertyDestroy("cron", appName) + if err := common.PropertyDestroy("cron", appName); err != nil { + return err + } + + return nil } // TriggerSchedulerStop stops the scheduler for a given app container diff --git a/plugins/scheduler-docker-local/post-delete b/plugins/scheduler-docker-local/post-delete index cd8480f80..109b07748 100755 --- a/plugins/scheduler-docker-local/post-delete +++ b/plugins/scheduler-docker-local/post-delete @@ -14,9 +14,6 @@ trigger-scheduler-docker-local-post-delete() { # remove all application containers "$DOCKER_BIN" container ls --filter "label=com.dokku.app-name=${APP}" -q | xargs -n1 -I {} "$DOCKER_BIN" container rm --force {} &>/dev/null || true - - local DOKKU_SCHEDULER="$(get_app_scheduler "$APP")" - plugn trigger scheduler-cron-write "$DOKKU_SCHEDULER" "$APP" } trigger-scheduler-docker-local-post-delete "$@" From 2758b3cc47846ae19fa932adf73e96f879ad7e1d Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Tue, 10 Jun 2025 23:36:10 -0400 Subject: [PATCH 06/16] chore: rename trigger function --- plugins/scheduler-docker-local/src/triggers/triggers.go | 2 +- plugins/scheduler-docker-local/triggers.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/scheduler-docker-local/src/triggers/triggers.go b/plugins/scheduler-docker-local/src/triggers/triggers.go index 4d11721ca..7ee657f92 100644 --- a/plugins/scheduler-docker-local/src/triggers/triggers.go +++ b/plugins/scheduler-docker-local/src/triggers/triggers.go @@ -20,7 +20,7 @@ func main() { switch trigger { case "scheduler-cron-write": scheduler := flag.Arg(0) - err = schedulerdockerlocal.TriggerCronWrite(scheduler) + err = schedulerdockerlocal.TriggerSchedulerCronWrite(scheduler) default: err = fmt.Errorf("Invalid plugin trigger call: %s", trigger) } diff --git a/plugins/scheduler-docker-local/triggers.go b/plugins/scheduler-docker-local/triggers.go index 785fc9f31..4f9fded0d 100644 --- a/plugins/scheduler-docker-local/triggers.go +++ b/plugins/scheduler-docker-local/triggers.go @@ -1,6 +1,6 @@ package schedulerdockerlocal -// TriggerCronWrite force updates the cron file for all apps -func TriggerCronWrite(scheduler string) error { +// TriggerSchedulerCronWrite force updates the cron file for all apps +func TriggerSchedulerCronWrite(scheduler string) error { return writeCronEntries(scheduler) } From 67003ed469a5157c3d0a53f8419d5016c9089b50 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Tue, 10 Jun 2025 23:39:35 -0400 Subject: [PATCH 07/16] fix: skip cron entries that are missing a command or a schedule --- plugins/cron/cron.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/plugins/cron/cron.go b/plugins/cron/cron.go index dd30c25e8..667f5ecf7 100644 --- a/plugins/cron/cron.go +++ b/plugins/cron/cron.go @@ -4,6 +4,7 @@ import ( "fmt" appjson "github.com/dokku/dokku/plugins/app-json" + "github.com/dokku/dokku/plugins/common" "github.com/multiformats/go-base36" cronparser "github.com/robfig/cron/v3" @@ -75,7 +76,17 @@ func FetchCronEntries(appName string) ([]TemplateCommand, error) { return commands, nil } - for _, c := range appJSON.Cron { + for i, c := range appJSON.Cron { + if c.Command == "" { + common.LogWarn(fmt.Sprintf("Missing cron command for app %s (index %d)", appName, i)) + continue + } + + if c.Schedule == "" { + common.LogWarn(fmt.Sprintf("Missing cron schedule for app %s (index %d)", appName, i)) + continue + } + parser := cronparser.NewParser(cronparser.Minute | cronparser.Hour | cronparser.Dom | cronparser.Month | cronparser.Dow | cronparser.Descriptor) _, err := parser.Parse(c.Schedule) if err != nil { From 73be6989ff1deff46af6fa5b7411cdef426a8ba2 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Tue, 10 Jun 2025 23:55:06 -0400 Subject: [PATCH 08/16] fix: ensure invalid cron triggers fail a deploy --- plugins/app-json/triggers.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/plugins/app-json/triggers.go b/plugins/app-json/triggers.go index 35d88240c..92a896e4e 100644 --- a/plugins/app-json/triggers.go +++ b/plugins/app-json/triggers.go @@ -1,6 +1,7 @@ package appjson import ( + "encoding/json" "fmt" "os" "path" @@ -140,6 +141,35 @@ func TriggerCorePostExtract(appName string, sourceWorkDir string) error { } } + if hasAppJSON(appName) { + b, err := os.ReadFile(processSpecificAppJSON) + if err != nil { + return err + } + + content := strings.TrimSpace(string(b)) + if content == "" { + return nil + } + + var appJSON AppJSON + if err := json.Unmarshal([]byte(content), &appJSON); err != nil { + return fmt.Errorf("Unable to unmarshal app.json: %v", err.Error()) + } + + if appJSON.Cron != nil { + for i, c := range appJSON.Cron { + if c.Command == "" { + return fmt.Errorf("Missing cron command for app %s (index %d)", appName, i) + } + + if c.Schedule == "" { + return fmt.Errorf("Missing cron schedule for app %s (index %d)", appName, i) + } + } + } + } + // TODO: add validation to app.json file by ensuring it can be deserialized return nil } From 6f0039ebf15b7ebd84b509188846c5c647fed5fc Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Wed, 18 Jun 2025 23:51:39 -0400 Subject: [PATCH 09/16] fix: add missing source --- plugins/scheduler-docker-local/post-deploy | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/scheduler-docker-local/post-deploy b/plugins/scheduler-docker-local/post-deploy index 325b1bd52..0b56c6d24 100755 --- a/plugins/scheduler-docker-local/post-deploy +++ b/plugins/scheduler-docker-local/post-deploy @@ -1,5 +1,6 @@ #!/usr/bin/env bash set -eo pipefail +source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" [[ $DOKKU_TRACE ]] && set -x trigger-scheduler-docker-local-post-deploy() { From 776013b0f49c3495279bd7dc36144b1461a36e31 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Thu, 19 Jun 2025 01:09:15 -0400 Subject: [PATCH 10/16] fix: write the cron config after the app.json has been deleted Without this, we try to write the config for the app even though it doesn't exist anymore, causing issues with deleting apps with invalid cron configs. --- plugins/cron/Makefile | 2 +- plugins/cron/src/triggers/triggers.go | 3 --- plugins/cron/triggers.go | 11 +++++------ 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/plugins/cron/Makefile b/plugins/cron/Makefile index 20ddf156e..920941136 100644 --- a/plugins/cron/Makefile +++ b/plugins/cron/Makefile @@ -1,5 +1,5 @@ SUBCOMMANDS = subcommands/list subcommands/report subcommands/run subcommands/set -TRIGGERS = triggers/cron-get-property triggers/install triggers/post-app-clone-setup triggers/post-app-rename-setup triggers/pre-delete triggers/post-delete triggers/scheduler-stop +TRIGGERS = triggers/cron-get-property triggers/install triggers/post-app-clone-setup triggers/post-app-rename-setup triggers/post-delete triggers/scheduler-stop BUILD = commands subcommands triggers PLUGIN_NAME = cron diff --git a/plugins/cron/src/triggers/triggers.go b/plugins/cron/src/triggers/triggers.go index a5a5ed98d..6e0de5e7a 100644 --- a/plugins/cron/src/triggers/triggers.go +++ b/plugins/cron/src/triggers/triggers.go @@ -40,9 +40,6 @@ func main() { case "post-delete": appName := flag.Arg(0) err = cron.TriggerPostDelete(appName) - case "pre-delete": - appName := flag.Arg(0) - err = cron.TriggerPreDelete(appName) case "report": appName := flag.Arg(0) err = cron.ReportSingleApp(appName, "", "") diff --git a/plugins/cron/triggers.go b/plugins/cron/triggers.go index 3b5c5d929..383a7cd3a 100644 --- a/plugins/cron/triggers.go +++ b/plugins/cron/triggers.go @@ -55,19 +55,18 @@ func TriggerPostAppRenameSetup(oldAppName string, newAppName string) error { return nil } -// TriggerPreDelete stops cron for a given app -func TriggerPreDelete(appName string) error { +// TriggerPostDelete destroys the cron property for a given app container +func TriggerPostDelete(appName string) error { scheduler := common.GetAppScheduler(appName) _, err := common.CallPlugnTrigger(common.PlugnTriggerInput{ Trigger: "scheduler-cron-write", Args: []string{scheduler, appName}, StreamStdio: true, }) - return err -} + if err != nil { + return err + } -// TriggerPostDelete destroys the cron property for a given app container -func TriggerPostDelete(appName string) error { if err := common.PropertyDestroy("cron", appName); err != nil { return err } From ed851fe3996934059d9e4f2c4db7a2fbaa441785 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Thu, 19 Jun 2025 01:11:29 -0400 Subject: [PATCH 11/16] refactor: warn when invalid cron entries might be written --- plugins/cron/cron.go | 2 +- plugins/scheduler-docker-local/functions.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/cron/cron.go b/plugins/cron/cron.go index 667f5ecf7..77e9a6194 100644 --- a/plugins/cron/cron.go +++ b/plugins/cron/cron.go @@ -90,7 +90,7 @@ func FetchCronEntries(appName string) ([]TemplateCommand, error) { parser := cronparser.NewParser(cronparser.Minute | cronparser.Hour | cronparser.Dom | cronparser.Month | cronparser.Dow | cronparser.Descriptor) _, err := parser.Parse(c.Schedule) if err != nil { - return commands, fmt.Errorf("Invalid cron schedule %s: %s", c.Schedule, err.Error()) + return commands, fmt.Errorf("Invalid cron schedule for app %s (schedule %s): %s", appName, c.Schedule, err.Error()) } commands = append(commands, TemplateCommand{ diff --git a/plugins/scheduler-docker-local/functions.go b/plugins/scheduler-docker-local/functions.go index adf576314..6d4bca180 100644 --- a/plugins/scheduler-docker-local/functions.go +++ b/plugins/scheduler-docker-local/functions.go @@ -55,7 +55,8 @@ func generateCronEntries() ([]cron.TemplateCommand, error) { c, err := cron.FetchCronEntries(appName) if err != nil { results <- []cron.TemplateCommand{} - return err + common.LogWarn(err.Error()) + return nil } results <- c From 4ae9cca6f17c3a3c00c1f73d1409f88875bc45e6 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Thu, 19 Jun 2025 02:19:08 -0400 Subject: [PATCH 12/16] feat: implement app-json-is-valid plugin trigger --- docs/development/plugin-triggers.md | 15 +++++++++ plugins/app-json/appjson.go | 35 ++++++++++++--------- plugins/app-json/triggers.go | 33 ++++++------------- plugins/cron/cron.go | 23 ++++++++++---- plugins/cron/report.go | 2 +- plugins/cron/subcommands.go | 4 +-- plugins/cron/triggers.go | 20 ++++++++++++ plugins/scheduler-docker-local/functions.go | 2 +- plugins/scheduler-k3s/triggers.go | 2 +- 9 files changed, 87 insertions(+), 49 deletions(-) diff --git a/docs/development/plugin-triggers.md b/docs/development/plugin-triggers.md index e37e22c90..c790eec4e 100644 --- a/docs/development/plugin-triggers.md +++ b/docs/development/plugin-triggers.md @@ -119,6 +119,21 @@ set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x # TODO ``` +### `app-json-is-valid` + +- Description: Checks to see if the provided app.json file is valid +- Invoked by: Appjson extraction during deployment +- Arguments: `$APP $APP_JSON_PATH` +- Example: + +```shell +#!/usr/bin/env bash + +set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x + +# TODO +``` + ### `app-maybe-create` - Description: Creates an app (gated by whether this is globally enabled or not) diff --git a/plugins/app-json/appjson.go b/plugins/app-json/appjson.go index 5ea123f1c..75f0ccced 100644 --- a/plugins/app-json/appjson.go +++ b/plugins/app-json/appjson.go @@ -200,21 +200,9 @@ func GetAppJSON(appName string) (AppJSON, error) { return AppJSON{}, nil } - b, err := os.ReadFile(getProcessSpecificAppJSONPath(appName)) - if err != nil { - return AppJSON{}, fmt.Errorf("Cannot read app.json file: %v", err) - } + appJSONPath := getProcessSpecificAppJSONPath(appName) - if strings.TrimSpace(string(b)) == "" { - return AppJSON{}, nil - } - - var appJSON AppJSON - if err = json.Unmarshal(b, &appJSON); err != nil { - return AppJSON{}, fmt.Errorf("Cannot parse app.json: %v", err) - } - - return appJSON, nil + return ReadAppJSON(appJSONPath) } func GetAutoscalingConfig(appName string, processType string, replicas int) (FormationAutoscaling, bool, error) { @@ -256,3 +244,22 @@ func GetAutoscalingConfig(appName string, processType string, replicas int) (For return autoscaling, true, nil } + +// ReadAppJSON reads an app.json file from a given path +func ReadAppJSON(path string) (AppJSON, error) { + b, err := os.ReadFile(path) + if err != nil { + return AppJSON{}, fmt.Errorf("Cannot read app.json file: %v", err) + } + + if strings.TrimSpace(string(b)) == "" { + return AppJSON{}, nil + } + + var appJSON AppJSON + if err = json.Unmarshal(b, &appJSON); err != nil { + return AppJSON{}, fmt.Errorf("Cannot parse app.json: %v", err) + } + + return appJSON, nil +} diff --git a/plugins/app-json/triggers.go b/plugins/app-json/triggers.go index 92a896e4e..c5d051b5e 100644 --- a/plugins/app-json/triggers.go +++ b/plugins/app-json/triggers.go @@ -1,7 +1,7 @@ package appjson import ( - "encoding/json" + "errors" "fmt" "os" "path" @@ -142,31 +142,16 @@ func TriggerCorePostExtract(appName string, sourceWorkDir string) error { } if hasAppJSON(appName) { - b, err := os.ReadFile(processSpecificAppJSON) + result, err := common.CallPlugnTrigger(common.PlugnTriggerInput{ + Trigger: "app-json-is-valid", + Args: []string{appName, processSpecificAppJSON}, + }) if err != nil { - return err - } - - content := strings.TrimSpace(string(b)) - if content == "" { - return nil - } - - var appJSON AppJSON - if err := json.Unmarshal([]byte(content), &appJSON); err != nil { - return fmt.Errorf("Unable to unmarshal app.json: %v", err.Error()) - } - - if appJSON.Cron != nil { - for i, c := range appJSON.Cron { - if c.Command == "" { - return fmt.Errorf("Missing cron command for app %s (index %d)", appName, i) - } - - if c.Schedule == "" { - return fmt.Errorf("Missing cron schedule for app %s (index %d)", appName, i) - } + if result.StderrContents() != "" { + return errors.New(result.StderrContents()) } + + return err } } diff --git a/plugins/cron/cron.go b/plugins/cron/cron.go index 77e9a6194..5768065a3 100644 --- a/plugins/cron/cron.go +++ b/plugins/cron/cron.go @@ -62,21 +62,32 @@ func (t TemplateCommand) CronCommand() string { return fmt.Sprintf("dokku run --cron-id %s %s %s", t.ID, t.App, t.Command) } +// FetchCronEntriesInput is the input for the FetchCronEntries function +type FetchCronEntriesInput struct { + AppName string + AppJSON *appjson.AppJSON +} + // FetchCronEntries returns a list of cron commands for a given app -func FetchCronEntries(appName string) ([]TemplateCommand, error) { +func FetchCronEntries(input FetchCronEntriesInput) ([]TemplateCommand, error) { + appName := input.AppName commands := []TemplateCommand{} isMaintenance := reportComputedMaintenance(appName) == "true" - appJSON, err := appjson.GetAppJSON(appName) - if err != nil { - return commands, fmt.Errorf("Unable to fetch app.json for app %s: %s", appName, err.Error()) + if input.AppJSON == nil { + appJSON, err := appjson.GetAppJSON(appName) + if err != nil { + return commands, fmt.Errorf("Unable to fetch app.json for app %s: %s", appName, err.Error()) + } + + input.AppJSON = &appJSON } - if appJSON.Cron == nil { + if input.AppJSON.Cron == nil { return commands, nil } - for i, c := range appJSON.Cron { + for i, c := range input.AppJSON.Cron { if c.Command == "" { common.LogWarn(fmt.Sprintf("Missing cron command for app %s (index %d)", appName, i)) continue diff --git a/plugins/cron/report.go b/plugins/cron/report.go index 769ed59a6..3514b8f53 100644 --- a/plugins/cron/report.go +++ b/plugins/cron/report.go @@ -41,7 +41,7 @@ func reportMailto(_ string) string { } func reportTasks(appName string) string { - c, _ := FetchCronEntries(appName) + c, _ := FetchCronEntries(FetchCronEntriesInput{AppName: appName}) return strconv.Itoa(len(c)) } diff --git a/plugins/cron/subcommands.go b/plugins/cron/subcommands.go index aede7d39b..2d5717b4b 100644 --- a/plugins/cron/subcommands.go +++ b/plugins/cron/subcommands.go @@ -26,7 +26,7 @@ func CommandList(appName string, format string) error { return fmt.Errorf("Invalid format specified, supported formats: json, stdout") } - entries, err := FetchCronEntries(appName) + entries, err := FetchCronEntries(FetchCronEntriesInput{AppName: appName}) if err != nil { return err } @@ -79,7 +79,7 @@ func CommandRun(appName string, cronID string, detached bool) error { return err } - entries, err := FetchCronEntries(appName) + entries, err := FetchCronEntries(FetchCronEntriesInput{AppName: appName}) if err != nil { return err } diff --git a/plugins/cron/triggers.go b/plugins/cron/triggers.go index 383a7cd3a..a0f192e74 100644 --- a/plugins/cron/triggers.go +++ b/plugins/cron/triggers.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" + appjson "github.com/dokku/dokku/plugins/app-json" "github.com/dokku/dokku/plugins/common" ) @@ -23,6 +24,25 @@ func TriggerCronGetProperty(appName string, key string) error { return nil } +// TriggerAppJSONIsValid validates the cron entries for a given app +func TriggerAppJSONIsValid(appName string, appJSONPath string) error { + if !common.FileExists(appJSONPath) { + return nil + } + + appJSON, err := appjson.ReadAppJSON(appJSONPath) + if err != nil { + return err + } + + _, err = FetchCronEntries(FetchCronEntriesInput{AppName: appName, AppJSON: &appJSON}) + if err != nil { + return err + } + + return nil +} + // TriggerInstall runs the install step for the cron plugin func TriggerInstall() error { if err := common.PropertySetup("cron"); err != nil { diff --git a/plugins/scheduler-docker-local/functions.go b/plugins/scheduler-docker-local/functions.go index 6d4bca180..e11d7c035 100644 --- a/plugins/scheduler-docker-local/functions.go +++ b/plugins/scheduler-docker-local/functions.go @@ -52,7 +52,7 @@ func generateCronEntries() ([]cron.TemplateCommand, error) { return nil } - c, err := cron.FetchCronEntries(appName) + c, err := cron.FetchCronEntries(cron.FetchCronEntriesInput{AppName: appName}) if err != nil { results <- []cron.TemplateCommand{} common.LogWarn(err.Error()) diff --git a/plugins/scheduler-k3s/triggers.go b/plugins/scheduler-k3s/triggers.go index 22fec8b5a..d81094cbe 100644 --- a/plugins/scheduler-k3s/triggers.go +++ b/plugins/scheduler-k3s/triggers.go @@ -266,7 +266,7 @@ func TriggerSchedulerDeploy(scheduler string, appName string, imageTag string) e workingDir := common.GetWorkingDir(appName, image) - allCronEntries, err := cron.FetchCronEntries(appName) + allCronEntries, err := cron.FetchCronEntries(cron.FetchCronEntriesInput{AppName: appName}) if err != nil { return fmt.Errorf("Error fetching cron entries: %w", err) } From 2324cb888bcf74e79d978e2c0b924bb76fbfb4d1 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Thu, 19 Jun 2025 20:53:20 -0400 Subject: [PATCH 13/16] refactor: check for extracted file and add logging message --- plugins/app-json/triggers.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/app-json/triggers.go b/plugins/app-json/triggers.go index c5d051b5e..28b37a7eb 100644 --- a/plugins/app-json/triggers.go +++ b/plugins/app-json/triggers.go @@ -141,7 +141,8 @@ func TriggerCorePostExtract(appName string, sourceWorkDir string) error { } } - if hasAppJSON(appName) { + if common.FileExists(processSpecificAppJSON) { + common.LogInfo1("Validating app.json file") result, err := common.CallPlugnTrigger(common.PlugnTriggerInput{ Trigger: "app-json-is-valid", Args: []string{appName, processSpecificAppJSON}, From 7cd077a79b60c55e59e1ca84b5fa29021ddf30e1 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Thu, 19 Jun 2025 21:19:24 -0400 Subject: [PATCH 14/16] debug: stream stdout/stderr --- plugins/app-json/triggers.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/app-json/triggers.go b/plugins/app-json/triggers.go index 28b37a7eb..253f89722 100644 --- a/plugins/app-json/triggers.go +++ b/plugins/app-json/triggers.go @@ -144,8 +144,10 @@ func TriggerCorePostExtract(appName string, sourceWorkDir string) error { if common.FileExists(processSpecificAppJSON) { common.LogInfo1("Validating app.json file") result, err := common.CallPlugnTrigger(common.PlugnTriggerInput{ - Trigger: "app-json-is-valid", - Args: []string{appName, processSpecificAppJSON}, + Trigger: "app-json-is-valid", + Args: []string{appName, processSpecificAppJSON}, + StreamStdout: true, + StreamStderr: true, }) if err != nil { if result.StderrContents() != "" { From 7e37f9dc79d2200ae6d42802beb2501cb6936957 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Mon, 23 Jun 2025 00:22:38 -0400 Subject: [PATCH 15/16] fix: add missing trigger --- plugins/app-json/triggers.go | 1 - plugins/cron/Makefile | 2 +- plugins/cron/src/triggers/triggers.go | 4 ++++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/plugins/app-json/triggers.go b/plugins/app-json/triggers.go index 253f89722..08bd24404 100644 --- a/plugins/app-json/triggers.go +++ b/plugins/app-json/triggers.go @@ -142,7 +142,6 @@ func TriggerCorePostExtract(appName string, sourceWorkDir string) error { } if common.FileExists(processSpecificAppJSON) { - common.LogInfo1("Validating app.json file") result, err := common.CallPlugnTrigger(common.PlugnTriggerInput{ Trigger: "app-json-is-valid", Args: []string{appName, processSpecificAppJSON}, diff --git a/plugins/cron/Makefile b/plugins/cron/Makefile index 920941136..98eeb5fd2 100644 --- a/plugins/cron/Makefile +++ b/plugins/cron/Makefile @@ -1,5 +1,5 @@ SUBCOMMANDS = subcommands/list subcommands/report subcommands/run subcommands/set -TRIGGERS = 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/scheduler-stop BUILD = commands subcommands triggers PLUGIN_NAME = cron diff --git a/plugins/cron/src/triggers/triggers.go b/plugins/cron/src/triggers/triggers.go index 6e0de5e7a..1a65cec73 100644 --- a/plugins/cron/src/triggers/triggers.go +++ b/plugins/cron/src/triggers/triggers.go @@ -19,6 +19,10 @@ func main() { var err error switch trigger { + case "app-json-is-valid": + appName := flag.Arg(0) + appJSONPath := flag.Arg(1) + err = cron.TriggerAppJSONIsValid(appName, appJSONPath) case "cron-get-property": appName := flag.Arg(0) property := flag.Arg(1) From 885178eed5d85d7c6da374e42e454bb7310b9766 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Mon, 23 Jun 2025 01:03:10 -0400 Subject: [PATCH 16/16] feat: turn warnings to failures when running validation against an app.json file --- plugins/cron/cron.go | 13 +++++++++++-- plugins/cron/triggers.go | 6 +++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/plugins/cron/cron.go b/plugins/cron/cron.go index 5768065a3..8013debe1 100644 --- a/plugins/cron/cron.go +++ b/plugins/cron/cron.go @@ -64,8 +64,9 @@ func (t TemplateCommand) CronCommand() string { // FetchCronEntriesInput is the input for the FetchCronEntries function type FetchCronEntriesInput struct { - AppName string - AppJSON *appjson.AppJSON + AppName string + AppJSON *appjson.AppJSON + WarnToFailure bool } // FetchCronEntries returns a list of cron commands for a given app @@ -89,11 +90,19 @@ func FetchCronEntries(input FetchCronEntriesInput) ([]TemplateCommand, error) { for i, c := range input.AppJSON.Cron { if c.Command == "" { + if input.WarnToFailure { + return commands, fmt.Errorf("Missing cron command for app %s (index %d)", appName, i) + } + common.LogWarn(fmt.Sprintf("Missing cron command for app %s (index %d)", appName, i)) continue } if c.Schedule == "" { + if input.WarnToFailure { + return commands, fmt.Errorf("Missing cron schedule for app %s (index %d)", appName, i) + } + common.LogWarn(fmt.Sprintf("Missing cron schedule for app %s (index %d)", appName, i)) continue } diff --git a/plugins/cron/triggers.go b/plugins/cron/triggers.go index a0f192e74..499405913 100644 --- a/plugins/cron/triggers.go +++ b/plugins/cron/triggers.go @@ -35,7 +35,11 @@ func TriggerAppJSONIsValid(appName string, appJSONPath string) error { return err } - _, err = FetchCronEntries(FetchCronEntriesInput{AppName: appName, AppJSON: &appJSON}) + _, err = FetchCronEntries(FetchCronEntriesInput{ + AppName: appName, + AppJSON: &appJSON, + WarnToFailure: true, + }) if err != nil { return err }