diff --git a/Dockerfile b/Dockerfile index d30258e..4b84154 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,8 @@ RUN apk update && \ jq \ tini \ wget && \ - mkdir -p ${HOME_DIR}/jobs ${HOME_DIR}/projects && \ + mkdir -p ${HOME_DIR}/jobs && \ + rm -rf /etc/periodic /etc/crontabs/root && \ adduser -S docker -D COPY --from=rq-build /usr/bin/rq/rq /usr/local/bin diff --git a/README.md b/README.md index 0caf294..5a625ca 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# docker-crontab +# crontab -A simple wrapper over `docker` to all complex cron job to be run in other containers. Note, this is a maintained fork of [willfarrell/docker-crontab](https://github.com/willfarrell/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) (>250MB when this was created), it was the main inspiration for this project. @@ -11,42 +11,44 @@ A great project, don't get me wrong. It was just missing certain key enterprise - 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`. +- Ability to share settings between cron jobs using `~~shared-settings` as a key. ## Config file -The config file can be specifed in any of `json`, `toml`, or `yaml`, and can be defined as either an array or mapping (top-level keys will be ignored; can be useful for organizing commands) +The config file can be specified in any of `json`, `toml`, or `yaml`, and can be defined as either an array or mapping (top-level keys will be ignored; can be useful for organizing commands) - `name`: Human readable name that will be used as the job filename. Will be converted into a slug. Optional. - `comment`: Comments to be included with crontab entry. Optional. -- `schedule`: Crontab schedule syntax as described in https://en.wikipedia.org/wiki/Cron. Ex `@hourly`, `@every 1h30m`, `* * * * *`. Required. +- `schedule`: Crontab schedule syntax as described in https://en.wikipedia.org/wiki/Cron. Examples: `@hourly`, `@every 1h30m`, `* * * * *`. Required. - `command`: Command to be run on in crontab container or 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. Optional. +- `container`: Full container name. Ignored if `image` is included. Optional. - `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` -- `onstart`: Run the command on `crontab` container start, set to `true`. Optional, defaults to falsey. +- `trigger`: Array of docker-crontab subset objects. Sub-set includes: `image`, `container`, `command`, `dockerargs` +- `onstart`: Run the command on `crontab` container start, set to `true`. Optional, defaults to false. See [`config-samples`](config-samples) for examples. ```json -[{ - "schedule":"@every 5m", - "command":"/usr/sbin/logrotate /etc/logrotate.conf" - },{ - "comment":"Regenerate Certificate then reload nginx", - "schedule":"43 6,18 * * *", - "command":"sh -c 'dehydrated --cron --out /etc/ssl --domain ${LE_DOMAIN} --challenge dns-01 --hook dehydrated-dns'", - "dockerargs":"--env-file /opt/crontab/env/letsencrypt.env -v webapp_nginx_tls_cert:/etc/ssl -v webapp_nginx_acme_challenge:/var/www/.well-known/acme-challenge", - "image":"willfarrell/letsencrypt", - "trigger":[{ - "command":"sh -c '/etc/scripts/make_hpkp ${NGINX_DOMAIN} && /usr/sbin/nginx -t && /usr/sbin/nginx -s reload'", - "project":"conduit", - "container":"nginx" - }], - "onstart":true - }] +{ + "logrotate": { + "schedule":"@every 5m", + "command":"/usr/sbin/logrotate /etc/logrotate.conf" + }, + "cert-regen": { + "comment":"Regenerate Certificate then reload nginx", + "schedule":"43 6,18 * * *", + "command":"sh -c 'dehydrated --cron --out /etc/ssl --domain ${LE_DOMAIN} --challenge dns-01 --hook dehydrated-dns'", + "dockerargs":"--it --env-file /opt/crontab/env/letsencrypt.env", + "volumes":["webapp_nginx_tls_cert:/etc/ssl", "webapp_nginx_acme_challenge:/var/www/.well-known/acme-challenge"], + "image":"willfarrell/letsencrypt", + "trigger":[{ + "command":"sh -c '/etc/scripts/make_hpkp ${NGINX_DOMAIN} && /usr/sbin/nginx -t && /usr/sbin/nginx -s reload'", + "container":"nginx" + }], + "onstart":true + } +} ``` ## How to use @@ -69,5 +71,23 @@ docker run -d \ * otherwise [read the docker-compose docs](https://docs.docker.com/compose/networking/) 2. Add `dockerargs` to your docker-crontab `config.json` * use `--network NETWORK_NAME` to connect new container into docker-compose network - * use `--rm --name NAME` to use named container - * e.g. `"dockerargs": "--network my_dir_default --rm --name my-best-cron-job"` + * use `--name NAME` to use named container + * e.g. `"dockerargs": "--it"` + +### Dockerfile +```Dockerfile +FROM registry.gitlab.com/simplicityguy/docker/crontab + +COPY config.json ${HOME_DIR}/ +``` + +### Logrotate Dockerfile +```Dockerfile +FROM registry.gitlab.com/simplicityguy/docker/crontab + +RUN apk add --no-cache logrotate +RUN echo "*/5 * * * * /usr/sbin/logrotate /etc/logrotate.conf" >> /etc/crontabs/logrotate +COPY logrotate.conf /etc/logrotate.conf + +CMD ["crond", "-f"] +``` diff --git a/entrypoint.sh b/entrypoint.sh index c3e0aac..37e8b87 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -11,7 +11,7 @@ if [ -z "${HOME_DIR}" ]; then fi # Ensure dir exist - in case of volume mapping. -mkdir -p "${HOME_DIR}"/jobs "${HOME_DIR}"/projects +mkdir -p "${HOME_DIR}"/jobs if [ -z "${DOCKER_HOST}" ] && [ -a "${DOCKER_PORT_2375_TCP}" ]; then export DOCKER_HOST="tcp://docker:2375" @@ -35,7 +35,8 @@ normalize_config() { elif [ -f "${HOME_DIR}/config.yaml" ]; then JSON_CONFIG="$(rq -y <<< "$(cat "${HOME_DIR}"/config.yaml)")" fi - jq -r 'to_entries | map_values(.value + { name: .key })' <<< "${JSON_CONFIG}" > "${HOME_DIR}"/config.working.json + + jq -S -r '."~~shared-settings" as $shared | del(."~~shared-settings") | to_entries | map_values(.value + { name: .key } + $shared)' <<< "${JSON_CONFIG}" > "${HOME_DIR}"/config.working.json } ensure_docker_socket_accessible() { @@ -44,16 +45,16 @@ ensure_docker_socket_accessible() { DOCKER_GID=$(stat -c '%g' ${DOCKER_SOCK}) if [ "${DOCKER_GID}" != "0" ]; then if ! grep -qE "^[^:]+:[^:]+:${DOCKER_GID}:" /etc/group; then - # No group with such gid exists - create group docker. + # No group with such gid exists - create group 'docker'. addgroup -g "${DOCKER_GID}" docker adduser docker docker else - # Group with such gid exists - add user "docker" to this group. + # Group with such gid exists - add user 'docker' to this group. DOCKER_GROUP_NAME=$(getent group "${DOCKER_GID}" | awk -F':' '{{ print $1 }}') adduser docker "${DOCKER_GROUP_NAME}" fi else - # Docker socket belongs to "root" group - add user "docker" to this group. + # Docker socket belongs to 'root' group - add user 'docker' to this group. adduser docker root fi fi @@ -65,51 +66,41 @@ slugify() { make_image_cmd() { DOCKERARGS=$(echo "${1}" | jq -r .dockerargs) - if [ "${DOCKERARGS}" == "null" ]; then DOCKERARGS=; fi - VOLUMES=$(echo "${1}" | jq -r 'select(.volumes != null) | .volumes | map(" -v " + .) | join("")') - PORTS=$(echo "${1}" | jq -r 'select(.ports != null) | .ports | map(" -p " + .) | join("")') - EXPOSE=$(echo "${1}" | jq -r 'select(.expose != null) | .expose | map(" --expose " + .) | join("")') + ENVIRONMENT=$(echo "${1}" | jq -r 'select(.environment != null) | .environment | map("--env " + .) | join(" ")') + EXPOSE=$(echo "${1}" | jq -r 'select(.expose != null) | .expose | map("--expose " + .) | join(" ")' ) NAME=$(echo "${1}" | jq -r 'select(.name != null) | .name') - NETWORK=$(echo "${1}" | jq -r 'select(.network != null) | .network') - ENVIRONMENT=$(echo "${1}" | jq -r 'select(.environment != null) | .environment | map(" -e " + .) | join("")') - if [ -n "${NAME}" ]; then DOCKERARGS+=" --rm --name ${NAME} "; fi - if [ -n "${NETWORK}" ]; then DOCKERARGS+=" --network ${NETWORK} "; fi - if [ -n "${VOLUMES}" ]; then DOCKERARGS+="${VOLUMES}"; fi - if [ -n "${ENVIRONMENT}" ]; then DOCKERARGS+="${ENVIRONMENT}"; fi - if [ -n "${PORTS}" ]; then DOCKERARGS+="${PORTS}"; fi - if [ -n "${EXPOSE}" ]; then DOCKERARGS+="${EXPOSE}"; fi + NETWORK=$(echo "${1}" | jq -r 'select(.network != null) | .network | map("--network " + .) | join(" ")') + PORTS=$(echo "${1}" | jq -r 'select(.ports != null) | .ports | map("--publish " + .) | join(" ")') + VOLUMES=$(echo "${1}" | jq -r 'select(.volumes != null) | .volumes | map("--volume " + .) | join(" ")') + + if [ "${DOCKERARGS}" == "null" ]; then DOCKERARGS=; fi + DOCKERARGS+=" " + if [ -n "${ENVIRONMENT}" ]; then DOCKERARGS+="${ENVIRONMENT} "; fi + if [ -n "${EXPOSE}" ]; then DOCKERARGS+="${EXPOSE} "; fi + if [ -n "${NAME}" ]; then DOCKERARGS+="--name ${NAME} "; fi + if [ -n "${NETWORK}" ]; then DOCKERARGS+="${NETWORK} "; fi + if [ -n "${PORTS}" ]; then DOCKERARGS+="${PORTS} "; fi + if [ -n "${VOLUMES}" ]; then DOCKERARGS+="${VOLUMES} "; fi + IMAGE=$(echo "${1}" | jq -r .image | envsubst) - TMP_COMMAND=$(echo "${1}" | jq -r .command) - echo "docker run ${DOCKERARGS} ${IMAGE} ${TMP_COMMAND}" + if [ "${IMAGE}" == "null" ]; then return; fi + + COMMAND=$(echo "${1}" | jq -r .command) + + echo "docker run ${DOCKERARGS} ${IMAGE} ${COMMAND}" } make_container_cmd() { DOCKERARGS=$(echo "${1}" | jq -r .dockerargs) if [ "${DOCKERARGS}" == "null" ]; then DOCKERARGS=; fi - SCRIPT_NAME=$(echo "${1}" | jq -r .name) - SCRIPT_NAME=$(slugify "${SCRIPT_NAME}") - PROJECT=$(echo "${1}" | jq -r .project) + CONTAINER=$(echo "${1}" | jq -r .container | envsubst) - TMP_COMMAND=$(echo "${1}" | jq -r .command) + if [ "${CONTAINER}" == "null" ]; then return; fi - if [ "${PROJECT}" != "null" ]; then - # Create bash script to detect all running containers. - if [ "${SCRIPT_NAME}" == "null" ]; then - SCRIPT_NAME=$(cat /proc/sys/kernel/random/uuid) - fi -cat << EOF > "${HOME_DIR}"/projects/"${SCRIPT_NAME}".sh -#!/usr/bin/env bash -set -e + COMMAND=$(echo "${1}" | jq -r .command ) + if [ "${COMMAND}" == "null" ]; then return; fi -CONTAINERS=\$(docker ps --format '{{.Names}}' | grep -E "^${PROJECT}_${CONTAINER}.[0-9]+") -for CONTAINER_NAME in \${CONTAINERS}; do - docker exec "${DOCKERARGS} \${CONTAINER_NAME} ${TMP_COMMAND}" -done -EOF - echo "/bin/bash ${HOME_DIR}/projects/${SCRIPT_NAME}.sh" - else - echo "docker exec ${DOCKERARGS} ${CONTAINER} ${TMP_COMMAND}" - fi + echo "docker exec ${DOCKERARGS} ${CONTAINER} ${COMMAND}" } make_cmd() { @@ -176,84 +167,94 @@ function build_crontab() { ONSTART=() while read -r i ; do - SCHEDULE=$(jq -r .["$i"].schedule "${CONFIG}" | sed 's/\*/\\*/g') + KEY=$(jq -r .["$i"] "${CONFIG}") + + SCHEDULE=$(echo "${KEY}" | jq -r '.schedule' | sed 's/\*/\\*/g') if [ "${SCHEDULE}" == "null" ]; then - echo "'schedule' missing: $(jq -r .["$i"].schedule "${CONFIG}")" + echo "'schedule' missing: '${KEY}" continue fi SCHEDULE=$(parse_schedule "${SCHEDULE}" | sed 's/\\//g') - COMMAND=$(jq -r .["$i"].command "${CONFIG}") + COMMAND=$(echo "${KEY}" | jq -r '.command') if [ "${COMMAND}" == "null" ]; then - echo "'command' missing: '${COMMAND}'" + echo "'command' missing: '${KEY}'" continue fi - COMMENT=$(jq -r .["$i"].comment "${CONFIG}") - if [ "${COMMENT}" != "null" ]; then - COMMENT=" ${COMMENT}" - echo "#${COMMENT}" >> ${CRONTAB_FILE} - else - # Reset COMMENT to empty rather than keep the 'null' value. - COMMENT=" " - fi + COMMENT=$(echo "${KEY}" | jq -r '.comment') - SCRIPT_NAME=$(jq -r .["$i"].name "${CONFIG}") + SCRIPT_NAME=$(echo "${KEY}" | jq -r '.name') SCRIPT_NAME=$(slugify "${SCRIPT_NAME}") if [ "${SCRIPT_NAME}" == "null" ]; then SCRIPT_NAME=$(cat /proc/sys/kernel/random/uuid) fi - COMMAND="/bin/bash ${HOME_DIR}/jobs/${SCRIPT_NAME}.sh" -cat << EOF > "${HOME_DIR}"/jobs/"${SCRIPT_NAME}".sh -#!/usr/bin/env bash -set -e + CRON_COMMAND=$(make_cmd "${KEY}") -echo "start cron job **${SCRIPT_NAME}**${COMMENT}" -$(make_cmd "$(jq -c .["$i"] "${CONFIG}")") -EOF - TRIGGER=$(jq -r .["$i"].trigger "${CONFIG}") + SCRIPT_PATH="${HOME_DIR}/jobs/${SCRIPT_NAME}.sh" + + touch "${SCRIPT_PATH}" + chmod +x "${SCRIPT_PATH}" + + { + echo "#\!/usr/bin/env bash" + echo "set -e" + echo "" + echo "echo \"start cron job __${SCRIPT_NAME}__\"" + echo "${CRON_COMMAND}" + } >> "${SCRIPT_PATH}" + + TRIGGER=$(echo "${KEY}" | jq -r '.trigger') if [ "${TRIGGER}" != "null" ]; then while read -r j ; do - TRIGGER_COMMAND=$(jq .["$i"].trigger["$j"].command "${CONFIG}") + TRIGGER_KEY=$(echo "${KEY}" | jq -r .trigger["$j"]) + + TRIGGER_COMMAND=$(echo "${TRIGGER_KEY}" | jq -r '.command') if [ "${TRIGGER_COMMAND}" == "null" ]; then - echo "'command' missing: '${TRIGGER_COMMAND}'" continue fi - make_cmd "${TRIGGER_COMMAND}" >> "${HOME_DIR}"/jobs/"${SCRIPT_NAME}".sh - done < <(jq -r '.['"$i"'].trigger|keys[]' "${CONFIG}") + + make_cmd "${TRIGGER_KEY}" >> "${SCRIPT_PATH}" + done < <(echo "${KEY}" | jq -r '.trigger | keys[]') fi - echo "echo \"end cron job **${SCRIPT_NAME}**${COMMENT}\"" >> "${HOME_DIR}"/jobs/"${SCRIPT_NAME}".sh + echo "echo \"end cron job __${SCRIPT_NAME}__\"" >> "${SCRIPT_PATH}" - echo "${SCHEDULE} ${COMMAND}" >> ${CRONTAB_FILE} - - if [ "$(jq -r .["$i"].onstart "${CONFIG}")" == "true" ]; then - ONSTART+=("${COMMAND}") + if [ "${COMMENT}" != "null" ]; then + echo "# ${COMMENT}" >> ${CRONTAB_FILE} fi - done < <(jq -r '.|keys[]' "${CONFIG}") + echo "${SCHEDULE} ${SCRIPT_PATH}" >> ${CRONTAB_FILE} - echo "##### crontab generation complete #####" + ONSTART_COMMAND=$(echo "${KEY}" | jq -r '.onstart') + if [ "${ONSTART_COMMAND}" == "true" ]; then + ONSTART+=("${SCRIPT_PATH}") + fi + done < <(jq -r '. | keys[]' "${CONFIG}") + + printf "##### crontab generated #####\n" cat ${CRONTAB_FILE} - echo "##### run commands with onstart #####" - for COMMAND in "${ONSTART[@]}"; do - echo "${COMMAND}" - ${COMMAND} & + printf "##### run commands with onstart #####\n" + for ONSTART_COMMAND in "${ONSTART[@]}"; do + printf "%s\n" "${ONSTART_COMMAND}" + ${ONSTART_COMMAND} & done + + printf "##### cron running #####\n" } start_app() { normalize_config export CONFIG=${HOME_DIR}/config.working.json if [ ! -f "${CONFIG}" ]; then - echo "generated ${CONFIG} missing. exiting." + printf "missing generated %s. exiting.\n" "${CONFIG}" exit 1 fi if [ "${1}" == "crond" ]; then build_crontab fi - echo "${@}" + printf "%s\n" "${@}" exec "${@}" }