feat: add initial scheduled task implementation
This functionality enables users to manage a global crontab for the Dokku user that contains all scheduled tasks across all apps. Alternative schedulers can implement cron tasks as desired.
This commit is contained in:
6
plugins/cron/.gitignore
vendored
Normal file
6
plugins/cron/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/commands
|
||||
/subcommands/*
|
||||
/triggers/*
|
||||
/triggers
|
||||
/post-*
|
||||
/report
|
||||
6
plugins/cron/Makefile
Normal file
6
plugins/cron/Makefile
Normal file
@@ -0,0 +1,6 @@
|
||||
SUBCOMMANDS = subcommands/list subcommands/report
|
||||
TRIGGERS = triggers/post-delete triggers/post-deploy triggers/report
|
||||
BUILD = commands subcommands triggers
|
||||
PLUGIN_NAME = cron
|
||||
|
||||
include ../../common.mk
|
||||
152
plugins/cron/functions.go
Normal file
152
plugins/cron/functions.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
appjson "github.com/dokku/dokku/plugins/app-json"
|
||||
"github.com/dokku/dokku/plugins/common"
|
||||
|
||||
cronparser "github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
type templateCommand struct {
|
||||
ID string
|
||||
App string
|
||||
Command string
|
||||
Schedule string
|
||||
}
|
||||
|
||||
func fetchCronEntries(appName string) ([]templateCommand, error) {
|
||||
commands := []templateCommand{}
|
||||
scheduler := common.GetAppScheduler(appName)
|
||||
if scheduler != "docker-local" {
|
||||
return commands, nil
|
||||
}
|
||||
|
||||
appjsonPath := appjson.GetAppjsonPath(appName)
|
||||
if !common.FileExists(appjsonPath) {
|
||||
return commands, nil
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadFile(appjsonPath)
|
||||
if err != nil {
|
||||
return commands, fmt.Errorf("Cannot read app.json file for %s: %v", appName, err)
|
||||
}
|
||||
|
||||
if strings.TrimSpace(string(b)) == "" {
|
||||
return commands, nil
|
||||
}
|
||||
|
||||
var appJSON appjson.AppJSON
|
||||
if err = json.Unmarshal(b, &appJSON); err != nil {
|
||||
return commands, fmt.Errorf("Cannot parse app.json for %s: %v", appName, err)
|
||||
}
|
||||
|
||||
for _, c := range appJSON.Cron {
|
||||
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, err
|
||||
}
|
||||
|
||||
commands = append(commands, templateCommand{
|
||||
App: appName,
|
||||
Command: c.Command,
|
||||
Schedule: c.Schedule,
|
||||
ID: generateCommandID(appName, c),
|
||||
})
|
||||
}
|
||||
|
||||
return commands, nil
|
||||
}
|
||||
|
||||
func writeCronEntries() error {
|
||||
apps, err := common.DokkuApps()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
commands := []templateCommand{}
|
||||
for _, appName := range apps {
|
||||
scheduler := common.GetAppScheduler(appName)
|
||||
if scheduler != "docker-local" {
|
||||
continue
|
||||
}
|
||||
|
||||
c, err := fetchCronEntries(appName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
commands = append(commands, c...)
|
||||
}
|
||||
|
||||
if len(commands) == 0 {
|
||||
if !common.FileExists("/var/spool/cron/crontabs/dokku") {
|
||||
return nil
|
||||
}
|
||||
|
||||
command := common.NewShellCmd("crontab -r -u dokku")
|
||||
command.ShowOutput = false
|
||||
out, err := command.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to remove schedule file: %v", out)
|
||||
}
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Commands": commands,
|
||||
}
|
||||
|
||||
t, err := getCronTemplate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmpFile, err := ioutil.TempFile(os.TempDir(), fmt.Sprintf("dokku-%s-%s", common.MustGetEnv("DOKKU_PID"), "CopyFromImage"))
|
||||
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)
|
||||
}
|
||||
|
||||
command := common.NewShellCmd(fmt.Sprintf("crontab -u dokku %s", tmpFile.Name()))
|
||||
command.ShowOutput = false
|
||||
out, err := command.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to update schedule file: %v", out)
|
||||
}
|
||||
|
||||
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 := ioutil.ReadFile(templatePath)
|
||||
if err != nil {
|
||||
return t, fmt.Errorf("Cannot read template file: %v", err)
|
||||
}
|
||||
|
||||
s := strings.TrimSpace(string(b))
|
||||
return t.Parse(s)
|
||||
}
|
||||
|
||||
func generateCommandID(appName string, c appjson.CronCommand) string {
|
||||
return base64.StdEncoding.EncodeToString([]byte(appName + "===" + c.Command + "===" + c.Schedule))
|
||||
}
|
||||
15
plugins/cron/go.mod
Normal file
15
plugins/cron/go.mod
Normal file
@@ -0,0 +1,15 @@
|
||||
module github.com/dokku/dokku/plugins/cron
|
||||
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
github.com/dokku/dokku/plugins/app-json v0.0.0-00010101000000-000000000000
|
||||
github.com/dokku/dokku/plugins/common v0.0.0-00010101000000-000000000000
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/ryanuber/columnize v1.1.2-0.20190319233515-9e6335e58db3
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
)
|
||||
|
||||
replace github.com/dokku/dokku/plugins/app-json => ../app-json
|
||||
|
||||
replace github.com/dokku/dokku/plugins/common => ../common
|
||||
15
plugins/cron/go.sum
Normal file
15
plugins/cron/go.sum
Normal file
@@ -0,0 +1,15 @@
|
||||
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q=
|
||||
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
|
||||
github.com/codeskyblue/go-sh v0.0.0-20190412065543-76bd3d59ff27 h1:HHUr4P/aKh4quafGxDT9LDasjGdlGkzLbfmmrlng3kA=
|
||||
github.com/codeskyblue/go-sh v0.0.0-20190412065543-76bd3d59ff27/go.mod h1:VQx0hjo2oUeQkQUET7wRwradO6f+fN5jzXgB/zROxxE=
|
||||
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/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
|
||||
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/ryanuber/columnize v1.1.2-0.20190319233515-9e6335e58db3 h1:utdYOikI1XjNtTFGCwSM6OmFJblU4ld4gACoJsbadJg=
|
||||
github.com/ryanuber/columnize v1.1.2-0.20190319233515-9e6335e58db3/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
4
plugins/cron/plugin.toml
Normal file
4
plugins/cron/plugin.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
[plugin]
|
||||
description = "dokku core cron plugin"
|
||||
version = "0.22.9"
|
||||
[plugin.config]
|
||||
33
plugins/cron/report.go
Normal file
33
plugins/cron/report.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/dokku/dokku/plugins/common"
|
||||
)
|
||||
|
||||
// ReportSingleApp is an internal function that displays the cron report for one or more apps
|
||||
func ReportSingleApp(appName, infoFlag string) error {
|
||||
if err := common.VerifyAppName(appName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
flags := map[string]common.ReportFunc{
|
||||
"--cron-task-count": reportTasks,
|
||||
}
|
||||
|
||||
flagKeys := []string{}
|
||||
for flagKey := range flags {
|
||||
flagKeys = append(flagKeys, flagKey)
|
||||
}
|
||||
|
||||
trimPrefix := false
|
||||
uppercaseFirstCharacter := true
|
||||
infoFlags := common.CollectReport(appName, infoFlag, flags)
|
||||
return common.ReportSingleApp("cron", appName, infoFlag, infoFlags, flagKeys, trimPrefix, uppercaseFirstCharacter)
|
||||
}
|
||||
|
||||
func reportTasks(appName string) string {
|
||||
c, _ := fetchCronEntries(appName)
|
||||
return strconv.Itoa(len(c))
|
||||
}
|
||||
56
plugins/cron/src/commands/commands.go
Normal file
56
plugins/cron/src/commands/commands.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/dokku/dokku/plugins/common"
|
||||
)
|
||||
|
||||
const (
|
||||
helpHeader = `Usage: dokku cron[:COMMAND]
|
||||
|
||||
Manage scheduled tasks
|
||||
|
||||
Additional commands:`
|
||||
|
||||
helpContent = `
|
||||
cron:list <app>, List scheduled tasks for an app
|
||||
cron:report [<app>] [<flag>], Display report about an app
|
||||
`
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
|
||||
cmd := flag.Arg(0)
|
||||
switch cmd {
|
||||
case "cron", "cron:help":
|
||||
usage()
|
||||
case "help":
|
||||
command := common.NewShellCmd(fmt.Sprintf("ps -o command= %d", os.Getppid()))
|
||||
command.ShowOutput = false
|
||||
output, err := command.Output()
|
||||
|
||||
if err == nil && strings.Contains(string(output), "--all") {
|
||||
fmt.Println(helpContent)
|
||||
} else {
|
||||
fmt.Print("\n cron, Manage scheduled cron tasks\n")
|
||||
}
|
||||
default:
|
||||
dokkuNotImplementExitCode, err := strconv.Atoi(os.Getenv("DOKKU_NOT_IMPLEMENTED_EXIT"))
|
||||
if err != nil {
|
||||
fmt.Println("failed to retrieve DOKKU_NOT_IMPLEMENTED_EXIT environment variable")
|
||||
dokkuNotImplementExitCode = 10
|
||||
}
|
||||
os.Exit(dokkuNotImplementExitCode)
|
||||
}
|
||||
}
|
||||
|
||||
func usage() {
|
||||
common.CommandUsage(helpHeader, helpContent)
|
||||
}
|
||||
41
plugins/cron/src/subcommands/subcommands.go
Normal file
41
plugins/cron/src/subcommands/subcommands.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/dokku/dokku/plugins/common"
|
||||
"github.com/dokku/dokku/plugins/cron"
|
||||
|
||||
flag "github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// main entrypoint to all subcommands
|
||||
func main() {
|
||||
parts := strings.Split(os.Args[0], "/")
|
||||
subcommand := parts[len(parts)-1]
|
||||
|
||||
var err error
|
||||
switch subcommand {
|
||||
case "list":
|
||||
args := flag.NewFlagSet("cron:list", flag.ExitOnError)
|
||||
args.Parse(os.Args[2:])
|
||||
appName := args.Arg(0)
|
||||
err = cron.CommandList(appName)
|
||||
case "report":
|
||||
args := flag.NewFlagSet("cron:report", flag.ExitOnError)
|
||||
osArgs, infoFlag, flagErr := common.ParseReportArgs("cron", os.Args[2:])
|
||||
if flagErr == nil {
|
||||
args.Parse(osArgs)
|
||||
appName := args.Arg(0)
|
||||
err = cron.CommandReport(appName, infoFlag)
|
||||
}
|
||||
default:
|
||||
common.LogFail(fmt.Sprintf("Invalid plugin subcommand call: %s", subcommand))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
common.LogFail(err.Error())
|
||||
}
|
||||
}
|
||||
35
plugins/cron/src/triggers/triggers.go
Normal file
35
plugins/cron/src/triggers/triggers.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/dokku/dokku/plugins/common"
|
||||
cron "github.com/dokku/dokku/plugins/cron"
|
||||
)
|
||||
|
||||
// main entrypoint to all triggers
|
||||
func main() {
|
||||
parts := strings.Split(os.Args[0], "/")
|
||||
trigger := parts[len(parts)-1]
|
||||
flag.Parse()
|
||||
|
||||
var err error
|
||||
switch trigger {
|
||||
case "post-delete":
|
||||
err = cron.TriggerPostDelete()
|
||||
case "post-deploy":
|
||||
err = cron.TriggerPostDeploy()
|
||||
case "report":
|
||||
appName := flag.Arg(0)
|
||||
err = cron.ReportSingleApp(appName, "")
|
||||
default:
|
||||
common.LogFail(fmt.Sprintf("Invalid plugin trigger call: %s", trigger))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
common.LogFail(err.Error())
|
||||
}
|
||||
}
|
||||
47
plugins/cron/subcommands.go
Normal file
47
plugins/cron/subcommands.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/dokku/dokku/plugins/common"
|
||||
"github.com/ryanuber/columnize"
|
||||
)
|
||||
|
||||
// CommandList lists all scheduled cron tasks for a given app
|
||||
func CommandList(appName string) error {
|
||||
if err := common.VerifyAppName(appName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entries, err := fetchCronEntries(appName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
output := []string{"ID | Schedule | Command"}
|
||||
for _, entry := range entries {
|
||||
output = append(output, fmt.Sprintf("%s | %s | %s", entry.ID, entry.Schedule, entry.Command))
|
||||
}
|
||||
result := columnize.SimpleFormat(output)
|
||||
fmt.Println(result)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CommandReport displays a cron report for one or more apps
|
||||
func CommandReport(appName string, infoFlag string) error {
|
||||
if len(appName) == 0 {
|
||||
apps, err := common.DokkuApps()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, appName := range apps {
|
||||
if err := ReportSingleApp(appName, infoFlag); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return ReportSingleApp(appName, infoFlag)
|
||||
}
|
||||
6
plugins/cron/templates/cron.tmpl
Normal file
6
plugins/cron/templates/cron.tmpl
Normal file
@@ -0,0 +1,6 @@
|
||||
PATH=/usr/local/bin:/usr/bin:/bin
|
||||
SHELL=/bin/bash
|
||||
|
||||
{{ range $entry := .Commands -}}
|
||||
{{ $entry.Schedule }} dokku --rm run --cron-id {{ $entry.ID }} {{ $entry.App }} {{ $entry.Command }}
|
||||
{{ end -}}
|
||||
11
plugins/cron/triggers.go
Normal file
11
plugins/cron/triggers.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package cron
|
||||
|
||||
// TriggerPostDelete updates the cron entries for all apps
|
||||
func TriggerPostDelete() error {
|
||||
return writeCronEntries()
|
||||
}
|
||||
|
||||
// TriggerPostDeploy updates the cron entries for all apps
|
||||
func TriggerPostDeploy() error {
|
||||
return writeCronEntries()
|
||||
}
|
||||
Reference in New Issue
Block a user