2023-07-02 02:43:55 -04:00
|
|
|
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 {
|
2024-02-13 01:09:24 -05:00
|
|
|
result, err := common.CallExecCommand(common.ExecCommandInput{
|
2024-03-06 09:36:48 -05:00
|
|
|
Command: "crontab",
|
2024-02-13 01:09:24 -05:00
|
|
|
Args: []string{"-l", "-u", "dokku"},
|
|
|
|
|
})
|
2024-02-13 01:23:36 -05:00
|
|
|
if err != nil || result.ExitCode != 0 {
|
|
|
|
|
return nil
|
2023-07-02 02:43:55 -04:00
|
|
|
}
|
|
|
|
|
|
2024-02-13 01:09:24 -05:00
|
|
|
result, err = common.CallExecCommand(common.ExecCommandInput{
|
2024-03-06 09:36:48 -05:00
|
|
|
Command: "crontab",
|
2024-02-13 01:09:24 -05:00
|
|
|
Args: []string{"-r", "-u", "dokku"},
|
|
|
|
|
})
|
2023-07-02 02:43:55 -04:00
|
|
|
if err != nil {
|
2024-02-13 01:09:24 -05:00
|
|
|
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())
|
2023-07-02 02:43:55 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
common.LogInfo1("Removed")
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-07 22:40:53 -05:00
|
|
|
func generateCronTasks() ([]cron.CronTask, error) {
|
2023-07-02 02:43:55 -04:00
|
|
|
apps, _ := common.UnfilteredDokkuApps()
|
|
|
|
|
|
|
|
|
|
g := new(errgroup.Group)
|
2025-11-07 22:40:53 -05:00
|
|
|
results := make(chan []cron.CronTask, len(apps)+1)
|
2023-07-02 02:43:55 -04:00
|
|
|
for _, appName := range apps {
|
|
|
|
|
appName := appName
|
|
|
|
|
g.Go(func() error {
|
|
|
|
|
scheduler := common.GetAppScheduler(appName)
|
|
|
|
|
if scheduler != "docker-local" {
|
2025-11-07 22:40:53 -05:00
|
|
|
results <- []cron.CronTask{}
|
2023-07-02 02:43:55 -04:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-07 22:30:54 -05:00
|
|
|
c, err := cron.FetchCronTasks(cron.FetchCronTasksInput{AppName: appName})
|
2023-07-02 02:43:55 -04:00
|
|
|
if err != nil {
|
2025-11-07 22:40:53 -05:00
|
|
|
results <- []cron.CronTask{}
|
2025-06-19 01:11:29 -04:00
|
|
|
common.LogWarn(err.Error())
|
|
|
|
|
return nil
|
2023-07-02 02:43:55 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
results <- c
|
|
|
|
|
return nil
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
g.Go(func() error {
|
2025-11-07 22:40:53 -05:00
|
|
|
tasks := []cron.CronTask{}
|
2024-03-14 00:46:55 -04:00
|
|
|
response, _ := common.CallPlugnTrigger(common.PlugnTriggerInput{
|
|
|
|
|
Trigger: "cron-entries",
|
|
|
|
|
Args: []string{"docker-local"},
|
|
|
|
|
})
|
|
|
|
|
for _, line := range strings.Split(response.StdoutContents(), "\n") {
|
2023-07-02 02:43:55 -04:00
|
|
|
if strings.TrimSpace(line) == "" {
|
2025-11-07 22:40:53 -05:00
|
|
|
results <- []cron.CronTask{}
|
2023-07-02 02:43:55 -04:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
parts := strings.Split(line, ";")
|
|
|
|
|
if len(parts) != 2 && len(parts) != 3 {
|
2025-11-07 22:40:53 -05:00
|
|
|
results <- []cron.CronTask{}
|
2023-07-02 02:43:55 -04:00
|
|
|
return fmt.Errorf("Invalid injected cron task: %v", line)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
id := base36.EncodeToStringLc([]byte(strings.Join(parts, ";;;")))
|
2025-11-07 22:40:53 -05:00
|
|
|
task := cron.CronTask{
|
2025-03-09 05:03:43 -04:00
|
|
|
ID: id,
|
|
|
|
|
Schedule: parts[0],
|
|
|
|
|
AltCommand: parts[1],
|
|
|
|
|
Maintenance: false,
|
2023-07-02 02:43:55 -04:00
|
|
|
}
|
|
|
|
|
if len(parts) == 3 {
|
2025-11-07 22:40:53 -05:00
|
|
|
task.LogFile = parts[2]
|
2023-07-02 02:43:55 -04:00
|
|
|
}
|
2025-11-07 22:40:53 -05:00
|
|
|
tasks = append(tasks, task)
|
2023-07-02 02:43:55 -04:00
|
|
|
}
|
2025-11-07 22:40:53 -05:00
|
|
|
results <- tasks
|
2023-07-02 02:43:55 -04:00
|
|
|
return nil
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
err := g.Wait()
|
|
|
|
|
close(results)
|
|
|
|
|
|
2025-11-07 22:40:53 -05:00
|
|
|
tasks := []cron.CronTask{}
|
2023-07-02 02:43:55 -04:00
|
|
|
if err != nil {
|
2025-11-07 22:40:53 -05:00
|
|
|
return tasks, err
|
2023-07-02 02:43:55 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for result := range results {
|
2025-11-10 01:57:46 -05:00
|
|
|
for _, task := range result {
|
|
|
|
|
if !task.Maintenance {
|
|
|
|
|
tasks = append(tasks, task)
|
|
|
|
|
}
|
2023-07-02 02:43:55 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-07 22:40:53 -05:00
|
|
|
return tasks, nil
|
2023-07-02 02:43:55 -04:00
|
|
|
}
|
|
|
|
|
|
2025-09-21 20:58:56 -04:00
|
|
|
func writeCronTab(scheduler string) error {
|
2025-03-09 05:51:27 -04:00
|
|
|
// allow empty scheduler, which means all apps (used by letsencrypt)
|
|
|
|
|
if scheduler != "docker-local" && scheduler != "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-07 22:40:53 -05:00
|
|
|
tasks, err := generateCronTasks()
|
2023-07-02 02:43:55 -04:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-07 22:40:53 -05:00
|
|
|
if len(tasks) == 0 {
|
2023-07-02 02:43:55 -04:00
|
|
|
return deleteCrontab()
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-17 00:27:08 -05:00
|
|
|
resultfromResults, _ := common.CallPlugnTrigger(common.PlugnTriggerInput{
|
|
|
|
|
Trigger: "cron-get-property",
|
|
|
|
|
Args: []string{"--global", "mailfrom"},
|
|
|
|
|
})
|
|
|
|
|
mailfrom := resultfromResults.StdoutContents()
|
|
|
|
|
|
|
|
|
|
mailtoResults, _ := common.CallPlugnTrigger(common.PlugnTriggerInput{
|
2024-03-14 00:46:55 -04:00
|
|
|
Trigger: "cron-get-property",
|
|
|
|
|
Args: []string{"--global", "mailto"},
|
|
|
|
|
})
|
2024-12-17 00:27:08 -05:00
|
|
|
mailto := mailtoResults.StdoutContents()
|
2023-08-19 23:59:00 -04:00
|
|
|
|
2023-07-02 02:43:55 -04:00
|
|
|
data := map[string]interface{}{
|
2025-11-07 22:40:53 -05:00
|
|
|
"Tasks": tasks,
|
2024-12-17 00:27:08 -05:00
|
|
|
"Mailfrom": mailfrom,
|
2023-08-19 23:59:00 -04:00
|
|
|
"Mailto": mailto,
|
2023-07-02 02:43:55 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
t, err := getCronTemplate()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-21 20:58:56 -04:00
|
|
|
tmpFile, err := os.CreateTemp(os.TempDir(), fmt.Sprintf("dokku-%s-%s", common.MustGetEnv("DOKKU_PID"), "WriteCronTab"))
|
2023-07-02 02:43:55 -04:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-13 01:09:24 -05:00
|
|
|
result, err := common.CallExecCommand(common.ExecCommandInput{
|
2024-03-06 09:36:48 -05:00
|
|
|
Command: "crontab",
|
2024-02-13 01:09:24 -05:00
|
|
|
Args: []string{"-u", "dokku", tmpFile.Name()},
|
|
|
|
|
})
|
2023-07-02 02:43:55 -04:00
|
|
|
if err != nil {
|
2024-02-13 01:09:24 -05:00
|
|
|
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())
|
2023-07-02 02:43:55 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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")
|
2023-12-22 01:59:22 +08:00
|
|
|
b, err := os.ReadFile(templatePath)
|
2023-07-02 02:43:55 -04:00
|
|
|
if err != nil {
|
|
|
|
|
return t, fmt.Errorf("Cannot read template file: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
s := strings.TrimSpace(string(b))
|
|
|
|
|
return t.Parse(s)
|
|
|
|
|
}
|