diff --git a/.envrc.sample b/.envrc.sample new file mode 100644 index 0000000..ae805d6 --- /dev/null +++ b/.envrc.sample @@ -0,0 +1 @@ +export GOPATH=`pwd`/.vendor:$GOPATH diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..97e8bff --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# Ignore Bunch vendor directory +.vendor + +# Ignore direnv .envrc +.envrc + +# Ignore compiled versions +bin diff --git a/Bunchfile b/Bunchfile new file mode 100644 index 0000000..7e7a978 --- /dev/null +++ b/Bunchfile @@ -0,0 +1,3 @@ +bitbucket.org/masteryconnectetl/docker-cron !self +github.com/codegangsta/cli +github.com/robfig/cron diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..43a3b27 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 MasteryConnect + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3db1109 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# docker-cron +A simple one liner to run a command on a cron schedule. + +## Overview +`docker-cron` was born out of wanting a simple one liner way to keep a docker container up with a command being run at specific intervals. Integrating cron into a container in a generic way always presented itself as a much longer and harder process than was really worth it to just run a simple script. So we built `docker-cron` to make getting a simple single purpose docker container up a running faster. + +## Code Example + +Print out "hello world" every 10 seconds +``` +docker-cron --seconds=*/10 echo hello world +``` + +Run file every 15 minutes +``` +docker-cron --seconds=0 --minutes=*/15 ./test.sh +``` + +To see the help text +``` +docker-cron --help +``` diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..e1de552 --- /dev/null +++ b/build.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +build() { + echo "Building for Linux..." + GOOS=linux GOARCH=amd64 go build -o ./bin/linux/amd64/docker-cron . + GOOS=linux GOARCH=386 go build -o ./bin/linux/386/docker-cron . + GOOS=linux GOARCH=arm go build -o ./bin/linux/arm/docker-cron . + + echo "Building for Mac..." + GOOS=darwin GOARCH=amd64 go build -o ./bin/darwin/amd64/docker-cron . + GOOS=darwin GOARCH=386 go build -o ./bin/darwin/386/docker-cron . + + echo "Building for FreeBSD..." + GOOS=freebsd GOARCH=amd64 go build -o ./bin/freebsd/amd64/docker-cron . + GOOS=freebsd GOARCH=386 go build -o ./bin/freebsd/386/docker-cron . + + echo "Building for Windows..." + GOOS=windows GOARCH=amd64 go build -o ./bin/windows/amd64/docker-cron . + GOOS=windows GOARCH=386 go build -o ./bin/windows/386/docker-cron . +} + +build diff --git a/main.go b/main.go new file mode 100644 index 0000000..9cb05aa --- /dev/null +++ b/main.go @@ -0,0 +1,150 @@ +package main + +import ( + "bufio" + "log" + "os" + "os/exec" + "os/signal" + "strings" + "syscall" + "time" + + "github.com/codegangsta/cli" + "github.com/robfig/cron" +) + +func main() { + app := cli.NewApp() + app.Name = "docker-cron" + app.Usage = "used to run shell commands at specified intervals / times, based on cron syntax" + app.Version = "1.0" + app.Authors = []cli.Author{ + cli.Author{ + Name: "Daniel Baldwin", + }, + } + app.Copyright = ` +The MIT License (MIT) + +Copyright (c) 2015 MasteryConnect + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + ` + app.Flags = []cli.Flag{ + cli.StringFlag{ + Name: "seconds", + Value: "*", + Usage: "seconds: 0-59, */10", + }, + cli.StringFlag{ + Name: "minutes", + Value: "*", + Usage: "minutes: 0-59, */10", + }, + cli.StringFlag{ + Name: "hours", + Value: "*", + Usage: "hours: 0-23, */10", + }, + cli.StringFlag{ + Name: "day-of-month", + Value: "*", + Usage: "day of month: 1-31", + }, + cli.StringFlag{ + Name: "months", + Value: "*", + Usage: "month: 1-12 or JAN-DEC, */10", + }, + cli.StringFlag{ + Name: "day-of-week", + Value: "*", + Usage: "day of week: 0-6 or SUN-SAT", + }, + } + app.Action = func(con *cli.Context) { + // Vars + var command string + + // Checks + if len(con.Args()) > 0 { + command = strings.Join(con.Args(), " ") + } else { + log.Fatal("Not enough args, need a command to run.") + cli.ShowAppHelp(con) + } + + // Ensure handling of SIGTERM and Interrupt + signalChan := make(chan os.Signal, 1) + signal.Notify(signalChan, os.Interrupt) + signal.Notify(signalChan, syscall.SIGTERM) + go func() { + <-signalChan + os.Exit(1) + }() + + // Setup cron job + c := cron.New() + schedule := strings.Join([]string{ + con.String("seconds"), + con.String("minutes"), + con.String("hours"), + con.String("day-of-month"), + con.String("months"), + con.String("day-of-week"), + }, " ") + log.Printf("Setup cron to run on schedule: %s\n", schedule) + c.AddFunc(schedule, func() { + log.Printf("Running cron on schedule: %s\n", schedule) + + cmd := exec.Command("sh", "-c", command) + + cmdReader, err := cmd.StdoutPipe() + if err != nil { + log.Fatal("Error creating stdoutpipe for command", err) + } + + scanner := bufio.NewScanner(cmdReader) + go func() { + for scanner.Scan() { + log.Println(scanner.Text()) + } + }() + + err = cmd.Start() + if err != nil { + log.Fatal("Error running command", err) + } + + err = cmd.Wait() + if err != nil { + log.Fatal("Error waiting for command", err) + } + }) + c.Start() + + // Hold and let the cron job run + for { + time.Sleep(30 * time.Second) + } + } + + app.Run(os.Args) +}