From 53ffb16db527aaad905d4ce0023f96047dc4bb06 Mon Sep 17 00:00:00 2001 From: will Farrell Date: Sun, 12 Feb 2017 16:28:53 -0700 Subject: [PATCH] init commit --- .gitignore | 4 ++ Dockerfile | 16 +++++ README.md | 62 +++++++++++++++++ config.sample.json | 32 +++++++++ docker-compose.yml | 13 ++++ docker-entrypoint | 164 +++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 291 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 config.sample.json create mode 100644 docker-compose.yml create mode 100755 docker-entrypoint diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..88f27af --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea +*.iml + +config.json \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d8a6654 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM library/alpine:3.5 + +ENV HOME_DIR=/opt/crontab +RUN apk add --no-cache --virtual .run-deps bash curl jq docker \ + && mkdir -p ${HOME_DIR} + +# Dev +COPY config.json ${HOME_DIR}/ + +COPY docker-entrypoint / +ENTRYPOINT ["/docker-entrypoint"] + +HEALTHCHECK --interval=5s --timeout=3s \ + CMD ps aux | grep '[c]rond' || exit 1 + +CMD ["crond","-f"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a4a62d7 --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# docker-crontab + +A simple wrapper over `docker` to all complex cron job to be run in other containers. + +## Why? +Yes, I'm aware of [mcuadros/ofelia](https://github.com/mcuadros/ofelia), it was the main inspiration for this project. +A great project, don't get me wrong. It was just missing certain key enterprise features. + +## Features +- Easy to read schedule syntax allowed. +- Allows for comments, cause we all need friendly reminders of what `update_script.sh` actually does. +- Start an image using `image`. +- Run command in a container using `container`. +- Run command on a instances of a scaled container using `project`. +- Ability to trigger scripts in other containers on completion cron job using `trigger`. + +## Config.json +- `comment`: Comments to be included with crontab entry +- `schedule`: Crontab schedule syntax as described in https://godoc.org/github.com/robfig/cron. Ex `@hourly`, `@every 1h30m`, `* * * * * *`. Required. +- `command`: Command to be run on docker container/image. Required. +- `image`: Docker images name (ex `library/alpine:3.5`). Optional. +- `project`: Docker Compose/Swarm project name. Optional, only applies when `contain` is included. +- `container`: Full container name or container alias if `project` is set. Ignored if `image` is included. +- `dockerargs`: Command line docker `run`/`exec` arguments for full control. Defaults to ` `. +- `trigger`: Array of docker-crontab subset objects. Subset includes: `image`,`project`,`container`,`command`,`dockerargs` + +See `./config.sample.json` for examples. + +## Examples + +### Command Line +```bash +docer build -t crontab . +docker run -d \ + -v /var/run/docker.sock:/var/run/docker.sock \ + crontab +``` + +### Dockerfile +```Dockerfile +FROM willfarrell/crontab + +COPY config.json ${HOME_DIR}/ +``` + +### Logrotate Dockerfile +```Dockerfile +FROM willfarrell/crontab + +RUN apk add --no-cache logrotate +RUN echo "*/5 * * * * /usr/sbin/logrotate /etc/logrotate.conf" >> /etc/crontabs/logrotate +ADD logrotate.conf /etc/logrotate.conf + +CMD ["crond", "-f"] +``` + +## TODO +- [ ] Make smaller by using busybox? +- [ ] Have ability to auto regenerate crontab on file change +- [ ] Run commands on host machine +- [ ] Write tests +- [ ] Setup TravisCI \ No newline at end of file diff --git a/config.sample.json b/config.sample.json new file mode 100644 index 0000000..dd25c2c --- /dev/null +++ b/config.sample.json @@ -0,0 +1,32 @@ +[{ + "comment":"cron with triggered commands", + "schedule":"* * * * *", + "command":"echo hello", + "project":"crontab", + "container":"myapp", + "trigger":[{ + "command":"echo world", + "container":"crontab_myapp_1" + }] +},{ + "comment":"map a volume", + "schedule":"* * * * *", + "dockerargs":"-d -v /tmp:/tmp", + "command":"echo new", + "image":"alpine:3.5" +},{ + "comment":"use an ENV from inside a container", + "schedule":"@hourly", + "dockerargs":"-d -e FOO=BAR", + "command":"sh -c 'echo hourly ${FOO}'", + "image":"alpine:3.5" +},{ + "comment":"trigger every 2 min", + "schedule":"@every 2m", + "command":"echo 2 minute", + "image":"alpine:3.5", + "trigger":[{ + "command":"echo world", + "container":"crontab_myapp_1" + }] +}] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..888fc59 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +version: "2.1" + +services: + myapp: + image: alpine:3.5 + restart: always + command: "sh -c 'while :; do sleep 1; done'" + + crontab: + build: . + restart: always + volumes: + - "/var/run/docker.sock:/var/run/docker.sock" diff --git a/docker-entrypoint b/docker-entrypoint new file mode 100755 index 0000000..ed29632 --- /dev/null +++ b/docker-entrypoint @@ -0,0 +1,164 @@ +#!/usr/bin/env bash +set -e + +# for local testing only +#HOME_DIR=. + +CONFIG=${HOME_DIR}/config.json +DOCKER_SOCK=/var/run/docker.sock +CRONTAB_FILE=${HOME_DIR}/docker + +make_image_cmd() { + DOCKERARGS=$(echo ${TMP_JSON} | jq -r .dockerargs) + if [ "${DOCKERARGS}" == "null" ]; then DOCKERARGS=; fi + IMAGE=$(echo ${TMP_JSON} | jq -r .image) + TMP_COMMAND=$(echo ${TMP_JSON} | jq -r .command) + echo "docker run ${DOCKERARGS} ${IMAGE} ${TMP_COMMAND}" +} + +make_container_cmd() { + DOCKERARGS=$(echo ${TMP_JSON} | jq -r .dockerargs) + if [ "${DOCKERARGS}" == "null" ]; then DOCKERARGS=; fi + PROJECT=$(echo ${TMP_JSON} | jq -r .project) + CONTAINER=$(echo ${TMP_JSON} | jq -r .container) + TMP_COMMAND=$(echo ${TMP_JSON} | jq -r .command) + + COMMAND_ARR=() + + if [ "${PROJECT}" != "null" ]; then + + # create bash script to detect all running containers + SCRIPT_NAME=$(cat /proc/sys/kernel/random/uuid) +cat << EOF > ${HOME_DIR}/${SCRIPT_NAME} +!#/bin/bash +set -e + +CONTAINERS=\$(curl --no-buffer -s -XGET --unix-socket ${DOCKER_SOCK} http://localhost/containers/json | jq -r .[].Names[0] | sed 's@/@@')" +for CONTAINER_NAME in \$CONTAINERS; do + if [[ "\${CONTAINER_NAME}" =~ ^${PROJECT}_${CONTAINER}.+ ]]; then + docker exec ${DOCKERARGS} \${CONTAINER_NAME} ${TMP_COMMAND} + fi +done +EOF + echo "sh ${HOME_DIR}/${SCRIPT_NAME}" + else + echo "docker exec ${DOCKERARGS} ${CONTAINER} ${TMP_COMMAND}" + fi +} + +make_cmd() { + IMAGE=$(echo ${TMP_JSON} | jq -r .image) + CONTAINER=$(echo ${TMP_JSON} | jq -r .container) + if [ "${IMAGE}" != "null" ]; then + make_image_cmd + elif [ "${CONTAINER}" != "null" ]; then + make_container_cmd + else + echo "echo 'Error making docker command, image or container param missing.'" + fi +} + +parse_schedule() { + case $1 in + "@yearly") + echo "0 0 0 1 1 *" + ;; + "@annually") + echo "0 0 0 1 1 *" + ;; + "@monthly") + echo "0 0 0 1 * *" + ;; + "@weekly") + echo "0 0 0 * * 0" + ;; + "@daily") + echo "0 0 0 * * *" + ;; + "@midnight") + echo "0 0 0 * * *" + ;; + "@hourly") + echo "0 0 * * * *" + ;; + "@every") + TIME=$2 + TOTAL=0 + + M=$(echo $TIME | grep -o '[0-9]\+m') + H=$(echo $TIME | grep -o '[0-9]\+h') + D=$(echo $TIME | grep -o '[0-9]\+d') + + if [ -n "${M}" ]; then + TOTAL=$(($TOTAL + ${M::-1})) + fi + if [ -n "${H}" ]; then + TOTAL=$(($TOTAL + ${H::-1} * 60)) + fi + if [ -n "${D}" ]; then + TOTAL=$(($TOTAL + ${D::-1} * 60 * 24)) + fi + + echo "*/${TOTAL} * * * * *" + ;; + *) + echo "${@}" + ;; + esac +} + +function build_crontab() { + rm -rf ${CRONTAB_FILE} + while read i ; do + #echo "parse $(jq .[$i] ${CONFIG})" + + SCHEDULE=$(jq -r .[$i].schedule ${CONFIG} | sed 's/\*/\\*/g') + if [ "${SCHEDULE}" == "null" ]; then + echo "Schedule Missing: $(jq -r .[$i].schedule ${CONFIG})" + continue + fi + SCHEDULE=$(parse_schedule ${SCHEDULE} | sed 's/\\//g') + + if [ "$(jq -r .[$i].command ${CONFIG})" == "null" ]; then + echo "Command Missing: $(jq -r .[$i].command ${CONFIG})" + continue + fi + + TMP_JSON=$(jq -c .[$i] ${CONFIG}) + COMMAND=$(make_cmd) + if [ "$(jq -r .[$i].trigger ${CONFIG})" != "null" ]; then + while read j ; do + echo "trigger parse $(jq .[$i].trigger[$j] ${CONFIG})" + if [ "$(jq .[$i].trigger[$j].command ${CONFIG})" == "null" ]; then + echo "Command Missing: $(jq -r .[$i].trigger[$j].command ${CONFIG})" + continue + fi + TMP_JSON=$(jq -c .[$i].trigger[$j] ${CONFIG}) + COMMAND="$COMMAND && $(make_cmd)" + done < <(jq -r '.['$i'].trigger|keys[]' ${CONFIG}) + fi + + NAME=$(jq -r .[$i].name ${CONFIG}) + COMMENT=$(jq -r .[$i].comment ${CONFIG}) + if [ "${NAME}" != "null" ] && [ "${COMMENT}" != "null" ]; then + echo "# ${NAME}: ${COMMENT}" >> ${CRONTAB_FILE} + elif [ "${COMMENT}" != "null" ]; then + echo "# ${COMMENT}" >> ${CRONTAB_FILE} + fi + + echo "${SCHEDULE} ${COMMAND}" >> ${CRONTAB_FILE} + done < <(jq -r '.|keys[]' ${CONFIG}) + + echo "crontab generation complete" + cat ${CRONTAB_FILE} +} + +# Used to pass json to functions - total hack, I know +TMP_JSON= + +if [ "$1" = "crond" ] && [ -f ${CONFIG} ]; then + build_crontab +fi + +echo "$@" +exec "$@" \ No newline at end of file