diff --git a/docker/etc/sudoers.d/dokku-docker b/docker/etc/sudoers.d/dokku-docker index ae9a60f3f..8150da4a5 100644 --- a/docker/etc/sudoers.d/dokku-docker +++ b/docker/etc/sudoers.d/dokku-docker @@ -1 +1 @@ -dokku ALL=NOPASSWD:SETENV:/usr/bin/docker,/usr/bin/docker-image-labeler,/usr/bin/pack +dokku ALL=NOPASSWD:SETENV:/usr/bin/docker,/usr/bin/docker-image-labeler,/usr/bin/pack,/usr/bin/crontab diff --git a/plugins/common/io.go b/plugins/common/io.go index 1423bdb32..75d878ac2 100644 --- a/plugins/common/io.go +++ b/plugins/common/io.go @@ -183,6 +183,10 @@ func SetPermissions(path string, fileMode os.FileMode) error { systemGroup := GetenvWithDefault("DOKKU_SYSTEM_GROUP", "dokku") systemUser := GetenvWithDefault("DOKKU_SYSTEM_USER", "dokku") + if strings.HasPrefix("/etc/sudoers.d/", path) { + systemGroup = "root" + systemUser = "root" + } group, err := user.LookupGroup(systemGroup) if err != nil { @@ -207,7 +211,20 @@ func SetPermissions(path string, fileMode os.FileMode) error { // WriteSliceToFile writes a slice of strings to a file func WriteSliceToFile(filename string, lines []string) error { - file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + mode := os.FileMode(0600) + if strings.HasPrefix("/etc/sudoers.d/", filename) { + // sudoers files should be either 0600 (rhel) or 0440 (debian) + defaultMode := map[string]bool{ + "centos": true, + "fedora": true, + "rhel": true, + } + if !defaultMode[os.Getenv("DOKKU_DISTRO")] { + mode = os.FileMode(0440) + } + } + + file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode) if err != nil { return err } @@ -220,8 +237,8 @@ func WriteSliceToFile(filename string, lines []string) error { return err } - file.Chmod(0600) - SetPermissions(filename, 0600) + file.Chmod(mode) + SetPermissions(filename, mode) return nil } diff --git a/plugins/cron/.gitignore b/plugins/cron/.gitignore index 6be9e9546..949e5b1cd 100644 --- a/plugins/cron/.gitignore +++ b/plugins/cron/.gitignore @@ -3,5 +3,6 @@ /triggers/* /triggers /cron-* +/install /post-* /report diff --git a/plugins/cron/Makefile b/plugins/cron/Makefile index 5f3310f57..e0d6a5320 100644 --- a/plugins/cron/Makefile +++ b/plugins/cron/Makefile @@ -1,5 +1,5 @@ SUBCOMMANDS = subcommands/list subcommands/report -TRIGGERS = triggers/cron-write triggers/post-delete triggers/post-deploy triggers/report +TRIGGERS = triggers/cron-write triggers/install triggers/post-delete triggers/post-deploy triggers/report BUILD = commands subcommands triggers PLUGIN_NAME = cron diff --git a/plugins/cron/functions.go b/plugins/cron/functions.go index ebdd7fb61..a3733d0b7 100644 --- a/plugins/cron/functions.go +++ b/plugins/cron/functions.go @@ -81,13 +81,13 @@ func fetchCronEntries(appName string) ([]templateCommand, error) { } func deleteCrontab() error { - command := common.NewShellCmd("crontab -l -u dokku") + command := common.NewShellCmd("sudo /usr/bin/crontab -l -u dokku") command.ShowOutput = false if !command.Execute() { return nil } - command = common.NewShellCmd("crontab -r -u dokku") + command = common.NewShellCmd("sudo /usr/bin/crontab -r -u dokku") command.ShowOutput = false out, err := command.CombinedOutput() if err != nil { @@ -163,7 +163,7 @@ func writeCronEntries() error { return fmt.Errorf("Unable to template out schedule file: %v", err) } - command := common.NewShellCmd(fmt.Sprintf("crontab -u dokku %s", tmpFile.Name())) + command := common.NewShellCmd(fmt.Sprintf("sudo /usr/bin/crontab -u dokku %s", tmpFile.Name())) command.ShowOutput = false out, err := command.CombinedOutput() if err != nil { diff --git a/plugins/cron/src/triggers/triggers.go b/plugins/cron/src/triggers/triggers.go index f48bdc58d..43ae583f7 100644 --- a/plugins/cron/src/triggers/triggers.go +++ b/plugins/cron/src/triggers/triggers.go @@ -20,6 +20,8 @@ func main() { switch trigger { case "cron-write": err = cron.TriggerCronWrite() + case "install": + err = cron.TriggerInstall() case "post-delete": err = cron.TriggerPostDelete() case "post-deploy": diff --git a/plugins/cron/triggers.go b/plugins/cron/triggers.go index 74c18b19d..407c528ca 100644 --- a/plugins/cron/triggers.go +++ b/plugins/cron/triggers.go @@ -1,5 +1,26 @@ package cron +import ( + "os" + + "github.com/dokku/dokku/plugins/common" +) + +// TriggerInstall installs a sudoers file so we can execute crontab via sudo +func TriggerInstall() error { + lines := []string{"%dokku ALL=(ALL) NOPASSWD:/usr/bin/crontab"} + notty := map[string]bool{ + "centos": true, + "fedora": true, + "rhel": true, + } + if notty[os.Getenv("DOKKU_DISTRO")] { + lines = append(lines, "Defaults:dokku !requiretty") + } + + return common.WriteSliceToFile("/etc/sudoers.d/dokku-cron", lines) +} + // TriggerPostDelete updates the cron entries for all apps func TriggerPostDelete() error { return writeCronEntries() diff --git a/tests/unit/cron.bats b/tests/unit/cron.bats index 4595cf3b2..0a0125bc9 100644 --- a/tests/unit/cron.bats +++ b/tests/unit/cron.bats @@ -37,6 +37,13 @@ teardown() { assert_output "$help_output" } +@test "(cron) installed" { + run /bin/bash -c "test -f /etc/sudoers.d/dokku-cron" + echo "output: $output" + echo "status: $status" + assert_success +} + @test "(cron) invalid [missing-keys]" { run deploy_app python dokku@dokku.me:$TEST_APP template_cron_file_invalid echo "output: $output"