Files
dokku/plugins/cron/functions.go
Jose Diaz-Gonzalez 257baa1d46 refactor: run crontab under sudo to support rhel systems
RHEL blocks users from executing crontab as themselves Because Of Reasons so we need to use sudo instead.

Also refactors file writing to properly support writing the permissions on sudoers files going forward.

Closes #5020
2022-02-27 19:17:04 -05:00

194 lines
4.6 KiB
Go

package cron
import (
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"text/template"
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
AltCommand string
LogFile string
}
func (t templateCommand) CronCommand() string {
if t.AltCommand != "" {
if t.LogFile != "" {
return fmt.Sprintf("%s &>> %s", t.AltCommand, t.LogFile)
}
return t.AltCommand
}
return fmt.Sprintf("dokku run --cron-id %s %s %s", t.ID, t.App, t.Command)
}
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 deleteCrontab() error {
command := common.NewShellCmd("sudo /usr/bin/crontab -l -u dokku")
command.ShowOutput = false
if !command.Execute() {
return nil
}
command = common.NewShellCmd("sudo /usr/bin/crontab -r -u dokku")
command.ShowOutput = false
out, err := command.CombinedOutput()
if err != nil {
return fmt.Errorf("Unable to remove schedule file: %v", string(out))
}
common.LogInfo1("Removed")
return nil
}
func writeCronEntries() error {
apps, _ := common.DokkuApps()
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...)
}
b, _ := common.PlugnTriggerOutput("cron-entries", "docker-local")
for _, line := range strings.Split(strings.TrimSpace(string(b[:])), "\n") {
if strings.TrimSpace(line) == "" {
continue
}
parts := strings.Split(line, ";")
if len(parts) != 2 && len(parts) != 3 {
return fmt.Errorf("Invalid injected cron task: %v", line)
}
id := base64.StdEncoding.EncodeToString([]byte(strings.Join(parts, ";;;")))
command := templateCommand{
ID: id,
Schedule: parts[0],
AltCommand: parts[1],
}
if len(parts) == 3 {
command.LogFile = parts[2]
}
commands = append(commands, command)
}
if len(commands) == 0 {
return deleteCrontab()
}
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"), "WriteCronEntries"))
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("sudo /usr/bin/crontab -u dokku %s", tmpFile.Name()))
command.ShowOutput = false
out, err := command.CombinedOutput()
if err != nil {
return fmt.Errorf("Unable to update schedule file: %s", string(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))
}