├── .github ├── FUNDING.yml └── workflows │ └── build.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── config-samples ├── config.sample.json ├── config.sample.mapping.json ├── config.sample.mapping.yml ├── config.sample.toml └── config.sample.yml ├── docker-compose.yml ├── docker-entrypoint └── test_logging /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [willfarrell]# Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - '*' 9 | schedule: 10 | - cron: '0 0 * * *' 11 | 12 | jobs: 13 | multi: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - 17 | name: Checkout 18 | uses: actions/checkout@v2 19 | - 20 | name: Set up QEMU 21 | uses: docker/setup-qemu-action@v1 22 | - 23 | name: Set up Docker Buildx 24 | id: buildx 25 | uses: docker/setup-buildx-action@v1 26 | - 27 | name: Login to DockerHub 28 | uses: docker/login-action@v1 29 | with: 30 | username: ${{ secrets.DOCKER_USERNAME }} 31 | password: ${{ secrets.DOCKER_PASSWORD }} 32 | 33 | - if: github.ref == 'refs/heads/main' 34 | name: Conditional(Set tag as `latest`) 35 | run: echo "tag=willfarrell/crontab:latest" >> $GITHUB_ENV 36 | 37 | - if: startsWith(github.ref, 'refs/tags/') 38 | name: Conditional(Set tag as `{version}`) 39 | run: echo "tag=willfarrell/crontab:${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 40 | 41 | - 42 | name: Build and push 43 | uses: docker/build-push-action@v2 44 | with: 45 | context: . 46 | file: ./Dockerfile 47 | push: true 48 | tags: | 49 | ${{ env.tag }} 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | 4 | config.json 5 | .vscode 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.12 as rq-build 2 | 3 | ENV RQ_VERSION=1.0.2 4 | WORKDIR /root/ 5 | 6 | RUN apk --update add upx \ 7 | && wget https://github.com/dflemstr/rq/releases/download/v${RQ_VERSION}/rq-v${RQ_VERSION}-x86_64-unknown-linux-musl.tar.gz \ 8 | && tar -xvf rq-v1.0.2-x86_64-unknown-linux-musl.tar.gz \ 9 | && upx --brute rq 10 | 11 | FROM library/docker:stable 12 | 13 | COPY --from=rq-build /root/rq /usr/local/bin 14 | 15 | ENV HOME_DIR=/opt/crontab 16 | RUN apk add --no-cache --virtual .run-deps gettext jq bash tini \ 17 | && mkdir -p ${HOME_DIR}/jobs ${HOME_DIR}/projects \ 18 | && adduser -S docker -D 19 | 20 | COPY docker-entrypoint / 21 | ENTRYPOINT ["/sbin/tini", "--", "/docker-entrypoint"] 22 | 23 | HEALTHCHECK --interval=5s --timeout=3s \ 24 | CMD ps aux | grep '[c]rond' || exit 1 25 | 26 | CMD ["crond", "-f", "-d", "6", "-c", "/etc/crontabs"] 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 will Farrell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docker-crontab 2 | 3 | A simple wrapper over `docker` to all complex cron job to be run in other containers. 4 | 5 | ## Supported tags and Dockerfile links 6 | 7 | - [`latest` (*Dockerfile*)](https://github.com/willfarrell/docker-crontab/blob/master/Dockerfile) 8 | - [`1.0.0` (*Dockerfile*)](https://github.com/willfarrell/docker-crontab/blob/1.0.0/Dockerfile) 9 | - [`0.6.0` (*Dockerfile*)](https://github.com/willfarrell/docker-crontab/blob/0.6.0/Dockerfile) 10 | 11 | ![](https://img.shields.io/docker/pulls/willfarrell/crontab "Total docker pulls") [![](https://images.microbadger.com/badges/image/willfarrell/crontab.svg)](http://microbadger.com/images/willfarrell/crontab "Get your own image badge on microbadger.com") 12 | 13 | ## Why? 14 | 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. 15 | A great project, don't get me wrong. It was just missing certain key enterprise features I felt were required to support where docker is heading. 16 | 17 | ## Features 18 | - Easy to read schedule syntax allowed. 19 | - Allows for comments, cause we all need friendly reminders of what `update_script.sh` actually does. 20 | - Start an image using `image`. 21 | - Run command in a container using `container`. 22 | - Run command on a instances of a scaled container using `project`. 23 | - Ability to trigger scripts in other containers on completion cron job using `trigger`. 24 | 25 | ## Config file 26 | 27 | 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) 28 | 29 | - `name`: Human readable name that will be used as the job filename. Will be converted into a slug. Optional. 30 | - `comment`: Comments to be included with crontab entry. Optional. 31 | - `schedule`: Crontab schedule syntax as described in https://en.wikipedia.org/wiki/Cron. Ex `@hourly`, `@every 1h30m`, `* * * * *`. Required. 32 | - `command`: Command to be run on in crontab container or docker container/image. Required. 33 | - `image`: Docker images name (ex `library/alpine:3.5`). Optional. 34 | - `project`: Docker Compose/Swarm project name. Optional, only applies when `contain` is included. 35 | - `container`: Full container name or container alias if `project` is set. Ignored if `image` is included. Optional. 36 | - `dockerargs`: Command line docker `run`/`exec` arguments for full control. Defaults to ` `. 37 | - `trigger`: Array of docker-crontab subset objects. Subset includes: `image`,`project`,`container`,`command`,`dockerargs` 38 | - `onstart`: Run the command on `crontab` container start, set to `true`. Optional, defaults to falsey. 39 | 40 | See [`config-samples`](config-samples) for examples. 41 | 42 | ```json 43 | [{ 44 | "schedule":"@every 5m", 45 | "command":"/usr/sbin/logrotate /etc/logrotate.conf" 46 | },{ 47 | "comment":"Regenerate Certificate then reload nginx", 48 | "schedule":"43 6,18 * * *", 49 | "command":"sh -c 'dehydrated --cron --out /etc/ssl --domain ${LE_DOMAIN} --challenge dns-01 --hook dehydrated-dns'", 50 | "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", 51 | "image":"willfarrell/letsencrypt", 52 | "trigger":[{ 53 | "command":"sh -c '/etc/scripts/make_hpkp ${NGINX_DOMAIN} && /usr/sbin/nginx -t && /usr/sbin/nginx -s reload'", 54 | "project":"conduit", 55 | "container":"nginx" 56 | }], 57 | "onstart":true 58 | }] 59 | ``` 60 | 61 | ## How to use 62 | 63 | ### Command Line 64 | 65 | ```bash 66 | docker build -t crontab . 67 | docker run -d \ 68 | -v /var/run/docker.sock:/var/run/docker.sock:ro \ 69 | -v ./env:/opt/env:ro \ 70 | -v /path/to/config/dir:/opt/crontab:rw \ 71 | -v /path/to/logs:/var/log/crontab:rw \ 72 | crontab 73 | ``` 74 | 75 | ### Use with docker-compose 76 | 77 | 1. Figure out which network name used for your docker-compose containers 78 | * use `docker network ls` to see existing networks 79 | * if your `docker-compose.yml` is in `my_dir` directory, you probably has network `my_dir_default` 80 | * otherwise [read the docker-compose docs](https://docs.docker.com/compose/networking/) 81 | 2. Add `dockerargs` to your docker-crontab `config.json` 82 | * use `--network NETWORK_NAME` to connect new container into docker-compose network 83 | * use `--rm --name NAME` to use named container 84 | * e.g. `"dockerargs": "--network my_dir_default --rm --name my-best-cron-job"` 85 | 86 | ### Dockerfile 87 | 88 | ```Dockerfile 89 | FROM willfarrell/crontab 90 | 91 | COPY config.json ${HOME_DIR}/ 92 | 93 | ``` 94 | 95 | ### Logrotate Dockerfile 96 | 97 | ```Dockerfile 98 | FROM willfarrell/crontab 99 | 100 | RUN apk add --no-cache logrotate 101 | RUN echo "*/5 * * * * /usr/sbin/logrotate /etc/logrotate.conf" >> /etc/crontabs/logrotate 102 | COPY logrotate.conf /etc/logrotate.conf 103 | 104 | CMD ["crond", "-f"] 105 | ``` 106 | 107 | ### Logging - In Dev 108 | 109 | All `stdout` is captured, formatted, and saved to `/var/log/crontab/jobs.log`. Set `LOG_FILE` to `/dev/null` to disable logging. 110 | 111 | example: `e6ced859-1563-493b-b1b1-5a190b29e938 2017-06-18T01:27:10+0000 [info] Start Cronjob **map-a-vol** map a volume` 112 | 113 | grok: `CRONTABLOG %{DATA:request_id} %{TIMESTAMP_ISO8601:timestamp} \[%{LOGLEVEL:severity}\] %{GREEDYDATA:message}` 114 | 115 | ## TODO 116 | - [ ] Have ability to auto regenerate crontab on file change (signal HUP?) 117 | - [ ] Run commands on host machine (w/ --privileged?) 118 | - [ ] Write tests 119 | - [ ] Setup TravisCI 120 | -------------------------------------------------------------------------------- /config-samples/config.sample.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "comment": "cron with triggered commands", 4 | "schedule": "* * * * *", 5 | "command": "echo hello", 6 | "project": "crontab", 7 | "container": "myapp", 8 | "trigger": [ 9 | { 10 | "command": "echo world", 11 | "container": "crontab_myapp_1" 12 | } 13 | ] 14 | }, 15 | { 16 | "comment": "map a volume", 17 | "schedule": "* * * * *", 18 | "dockerargs": "-d -v /tmp:/tmp", 19 | "command": "echo new", 20 | "image": "alpine:3.5" 21 | }, 22 | { 23 | "comment": "use an ENV from inside a container", 24 | "schedule": "@hourly", 25 | "dockerargs": "-d -e FOO=BAR", 26 | "command": "sh -c 'echo hourly ${FOO}'", 27 | "image": "alpine:3.5" 28 | }, 29 | { 30 | "comment": "trigger every 2 min", 31 | "schedule": "@every 2m", 32 | "command": "echo 2 minute", 33 | "image": "alpine:3.5", 34 | "trigger": [ 35 | { 36 | "command": "echo world", 37 | "container": "crontab_myapp_1" 38 | } 39 | ] 40 | }, 41 | { 42 | "schedule": "*/5 * * * *", 43 | "command": "/usr/sbin/logrotate /etc/logrotate.conf" 44 | }, 45 | { 46 | "comment": "Regenerate Certificate then reload nginx", 47 | "schedule": "43 6,18 * * *", 48 | "command": "sh -c 'dehydrated --cron --out /etc/ssl --domain ${LE_DOMAIN} --challenge dns-01 --hook dehydrated-dns'", 49 | "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", 50 | "image": "willfarrell/letsencrypt", 51 | "trigger": [ 52 | { 53 | "command": "sh -c '/etc/scripts/make_hpkp ${NGINX_DOMAIN} && /usr/sbin/nginx -t && /usr/sbin/nginx -s reload'", 54 | "project": "conduit", 55 | "container": "nginx" 56 | } 57 | ], 58 | "onstart": true 59 | } 60 | ] 61 | -------------------------------------------------------------------------------- /config-samples/config.sample.mapping.json: -------------------------------------------------------------------------------- 1 | { 2 | "cron with triggered commands": { 3 | "comment": "cron with triggered commands", 4 | "schedule": "* * * * *", 5 | "command": "echo hello", 6 | "project": "crontab", 7 | "container": "myapp", 8 | "trigger": [{ "command": "echo world", "container": "crontab_myapp_1" }] 9 | }, 10 | "map a volume": { 11 | "comment": "map a volume", 12 | "schedule": "* * * * *", 13 | "dockerargs": "-d -v /tmp:/tmp", 14 | "command": "echo new", 15 | "image": "alpine:3.5" 16 | }, 17 | "use an ENV from inside a container": { 18 | "comment": "use an ENV from inside a container", 19 | "schedule": "@hourly", 20 | "dockerargs": "-d -e FOO=BAR", 21 | "command": "sh -c 'echo hourly ${FOO}'", 22 | "image": "alpine:3.5" 23 | }, 24 | "trigger every 2 min": { 25 | "comment": "trigger every 2 min", 26 | "schedule": "@every 2m", 27 | "command": "echo 2 minute", 28 | "image": "alpine:3.5", 29 | "trigger": [{ "command": "echo world", "container": "crontab_myapp_1" }] 30 | }, 31 | "null": { 32 | "schedule": "*/5 * * * *", 33 | "command": "/usr/sbin/logrotate /etc/logrotate.conf" 34 | }, 35 | "Regenerate Certificate then reload nginx": { 36 | "comment": "Regenerate Certificate then reload nginx", 37 | "schedule": "43 6,18 * * *", 38 | "command": "sh -c 'dehydrated --cron --out /etc/ssl --domain ${LE_DOMAIN} --challenge dns-01 --hook dehydrated-dns'", 39 | "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", 40 | "image": "willfarrell/letsencrypt", 41 | "trigger": [ 42 | { 43 | "command": "sh -c '/etc/scripts/make_hpkp ${NGINX_DOMAIN} && /usr/sbin/nginx -t && /usr/sbin/nginx -s reload'", 44 | "project": "conduit", 45 | "container": "nginx" 46 | } 47 | ], 48 | "onstart": true 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /config-samples/config.sample.mapping.yml: -------------------------------------------------------------------------------- 1 | cron with triggered commands: 2 | command: echo hello 3 | comment: cron with triggered commands 4 | container: myapp 5 | project: crontab 6 | schedule: '* * * * *' 7 | trigger: 8 | - command: echo world 9 | container: crontab_myapp_1 10 | map a volume: 11 | command: echo new 12 | comment: map a volume 13 | dockerargs: -d -v /tmp:/tmp 14 | image: alpine:3.5 15 | schedule: '* * * * *' 16 | use an ENV from inside a container: 17 | command: sh -c 'echo hourly ${FOO}' 18 | comment: use an ENV from inside a container 19 | dockerargs: -d -e FOO=BAR 20 | image: alpine:3.5 21 | schedule: '@hourly' 22 | trigger every 2 min: 23 | command: echo 2 minute 24 | comment: trigger every 2 min 25 | image: alpine:3.5 26 | schedule: '@every 2m' 27 | trigger: 28 | - command: echo world 29 | container: crontab_myapp_1 30 | null: 31 | command: /usr/sbin/logrotate /etc/logrotate.conf 32 | schedule: '*/5 * * * *' 33 | Regenerate Certificate then reload nginx: 34 | command: sh -c 'dehydrated --cron --out /etc/ssl --domain ${LE_DOMAIN} --challenge 35 | dns-01 --hook dehydrated-dns' 36 | comment: Regenerate Certificate then reload nginx 37 | dockerargs: --env-file /opt/crontab/env/letsencrypt.env -v webapp_nginx_tls_cert:/etc/ssl 38 | -v webapp_nginx_acme_challenge:/var/www/.well-known/acme-challenge 39 | image: willfarrell/letsencrypt 40 | onstart: true 41 | schedule: 43 6,18 * * * 42 | trigger: 43 | - command: sh -c '/etc/scripts/make_hpkp ${NGINX_DOMAIN} && /usr/sbin/nginx -t && 44 | /usr/sbin/nginx -s reload' 45 | container: nginx 46 | project: conduit 47 | -------------------------------------------------------------------------------- /config-samples/config.sample.toml: -------------------------------------------------------------------------------- 1 | # toml files can only have top-loevl mappings, so this is the only sample 2 | ["cron with triggered commands"] 3 | comment = "cron with triggered commands" 4 | schedule = "* * * * *" 5 | command = "echo hello" 6 | project = "crontab" 7 | container = "myapp" 8 | [["cron with triggered commands".trigger]] 9 | command = "echo world" 10 | container = "crontab_myapp_1" 11 | 12 | ["map a volume"] 13 | comment = "map a volume" 14 | schedule = "* * * * *" 15 | dockerargs = "-d -v /tmp:/tmp" 16 | command = "echo new" 17 | image = "alpine:3.5" 18 | 19 | ["use an ENV from inside a container"] 20 | comment = "use an ENV from inside a container" 21 | schedule = "@hourly" 22 | dockerargs = "-d -e FOO=BAR" 23 | command = "sh -c 'echo hourly ${FOO}'" 24 | image = "alpine:3.5" 25 | 26 | ["trigger every 2 min"] 27 | comment = "trigger every 2 min" 28 | schedule = "@every 2m" 29 | command = "echo 2 minute" 30 | image = "alpine:3.5" 31 | [["trigger every 2 min".trigger]] 32 | command = "echo world" 33 | container = "crontab_myapp_1" 34 | 35 | ["? /usr/sbin/logrotate /etc/logrotate.conf*/5 * * * *"] 36 | schedule = "*/5 * * * *" 37 | command = "/usr/sbin/logrotate /etc/logrotate.conf" 38 | 39 | ["Regenerate Certificate then reload nginx"] 40 | comment = "Regenerate Certificate then reload nginx" 41 | schedule = "43 6,18 * * *" 42 | command = "sh -c 'dehydrated --cron --out /etc/ssl --domain ${LE_DOMAIN} --challenge dns-01 --hook dehydrated-dns'" 43 | dockerargs = "--env-file /opt/crontab/env/letsencrypt.env -v ${PWD}:/etc/ssl -v webapp_nginx_acme_challenge:/var/www/.well-known/acme-challenge" 44 | image = "willfarrell/letsencrypt" 45 | onstart = true 46 | [["Regenerate Certificate then reload nginx".trigger]] 47 | command = "sh -c '/etc/scripts/make_hpkp ${NGINX_DOMAIN} && /usr/sbin/nginx -t && /usr/sbin/nginx -s reload'" 48 | project = "conduit" 49 | container = "nginx" 50 | 51 | -------------------------------------------------------------------------------- /config-samples/config.sample.yml: -------------------------------------------------------------------------------- 1 | - command: echo hello 2 | comment: cron with triggered commands 3 | container: myapp 4 | project: crontab 5 | schedule: '* * * * *' 6 | trigger: 7 | - command: echo world 8 | container: crontab_myapp_1 9 | - command: echo new 10 | comment: map a volume 11 | dockerargs: -d -v /tmp:/tmp 12 | image: alpine:3.5 13 | schedule: '* * * * *' 14 | - command: sh -c 'echo hourly ${FOO}' 15 | comment: use an ENV from inside a container 16 | dockerargs: -d -e FOO=BAR 17 | image: alpine:3.5 18 | schedule: '@hourly' 19 | - command: echo 2 minute 20 | comment: trigger every 2 min 21 | image: alpine:3.5 22 | schedule: '@every 2m' 23 | trigger: 24 | - command: echo world 25 | container: crontab_myapp_1 26 | - command: /usr/sbin/logrotate /etc/logrotate.conf 27 | schedule: '*/5 * * * *' 28 | - command: sh -c 'dehydrated --cron --out /etc/ssl --domain ${LE_DOMAIN} --challenge 29 | dns-01 --hook dehydrated-dns' 30 | comment: Regenerate Certificate then reload nginx 31 | dockerargs: --env-file /opt/crontab/env/letsencrypt.env -v webapp_nginx_tls_cert:/etc/ssl 32 | -v webapp_nginx_acme_challenge:/var/www/.well-known/acme-challenge 33 | image: willfarrell/letsencrypt 34 | onstart: true 35 | schedule: 43 6,18 * * * 36 | trigger: 37 | - command: sh -c '/etc/scripts/make_hpkp ${NGINX_DOMAIN} && /usr/sbin/nginx -t && 38 | /usr/sbin/nginx -s reload' 39 | container: nginx 40 | project: conduit 41 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2.1" 2 | 3 | services: 4 | myapp: 5 | image: alpine:3.5 6 | restart: always 7 | command: "sh -c 'while :; do sleep 1; done'" 8 | 9 | crontab: 10 | build: . 11 | restart: always 12 | volumes: 13 | - "/var/run/docker.sock:/var/run/docker.sock:ro" 14 | - "${PWD}/config-samples/config.sample.mapping.json:/opt/crontab/config.json:rw" 15 | -------------------------------------------------------------------------------- /docker-entrypoint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | if [ -z "$DOCKER_HOST" -a "$DOCKER_PORT_2375_TCP" ]; then 5 | export DOCKER_HOST='tcp://docker:2375' 6 | fi 7 | 8 | # for local testing only 9 | #HOME_DIR=. 10 | 11 | if [ "${LOG_FILE}" == "" ]; then 12 | LOG_DIR=/var/log/crontab 13 | LOG_FILE=${LOG_DIR}/jobs.log 14 | mkdir -p ${LOG_DIR} 15 | touch ${LOG_FILE} 16 | fi 17 | 18 | get_config() { 19 | if [ -f "${HOME_DIR}/config.json" ]; then 20 | jq 'map(.)' ${HOME_DIR}/config.json > ${HOME_DIR}/config.working.json 21 | elif [ -f "${HOME_DIR}/config.toml" ]; then 22 | rq -t <<< $(cat ${HOME_DIR}/config.toml) | jq 'map(.)' > ${HOME_DIR}/config.json 23 | elif [ -f "${HOME_DIR}/config.yml" ]; then 24 | rq -y <<< $(cat ${HOME_DIR}/config.yml) | jq 'map(.)' > ${HOME_DIR}/config.json 25 | elif [ -f "${HOME_DIR}/config.yaml" ]; then 26 | rq -y <<< $(cat ${HOME_DIR}/config.yaml) | jq 'map(.)' > ${HOME_DIR}/config.json 27 | fi 28 | } 29 | 30 | DOCKER_SOCK=/var/run/docker.sock 31 | CRONTAB_FILE=/etc/crontabs/docker 32 | 33 | # Ensure dir exist - in case of volume mapping 34 | mkdir -p ${HOME_DIR}/jobs ${HOME_DIR}/projects 35 | 36 | ensure_docker_socket_accessible() { 37 | if ! grep -q "^docker:" /etc/group; then 38 | # Ensure 'docker' user has permissions for docker socket (without changing permissions) 39 | DOCKER_GID=$(stat -c '%g' ${DOCKER_SOCK}) 40 | if [ "${DOCKER_GID}" != "0" ]; then 41 | if ! grep -qE "^[^:]+:[^:]+:${DOCKER_GID}:" /etc/group; then 42 | # No group with such gid exists - create group docker 43 | addgroup -g ${DOCKER_GID} docker 44 | adduser docker docker 45 | else 46 | # Group with such gid exists - add user "docker" to this group 47 | DOCKER_GROUP_NAME=`getent group "${DOCKER_GID}" | awk -F':' '{{ print $1 }}'` 48 | adduser docker $DOCKER_GROUP_NAME 49 | fi 50 | else 51 | # Docker socket belongs to "root" group - add user "docker" to this group 52 | adduser docker root 53 | fi 54 | fi 55 | } 56 | 57 | slugify() { 58 | echo "$@" | iconv -t ascii | sed -r s/[~\^]+//g | sed -r s/[^a-zA-Z0-9]+/-/g | sed -r s/^-+\|-+$//g | tr A-Z a-z 59 | } 60 | 61 | make_image_cmd() { 62 | DOCKERARGS=$(echo ${1} | jq -r .dockerargs) 63 | VOLUMES=$(echo ${1} | jq -r '.volumes | map(" -v " + .) | join("")') 64 | PORTS=$(echo ${1} | jq -r '.ports | map(" -p " + .) | join("")') 65 | EXPOSE=$(echo ${1} | jq -r '.expose | map(" --expose " + .) | join("")') 66 | # We'll add name in, if it exists 67 | NAME=$(echo ${1} | jq -r 'select(.name != null) | .name') 68 | NETWORK=$(echo ${1} | jq -r 'select(.network != null) | .network') 69 | ENVIRONMENT=$(echo ${1} | jq -r '.environment | map(" -e " + .) | join("")') 70 | # echo ${1} | jq -r '.environment | join("\n")' > ${PWD}/${NAME}.env 71 | # ENVIRONMENT=" --env-file ${PWD}/${NAME}.env" 72 | if [ "${DOCKERARGS}" == "null" ]; then DOCKERARGS=; fi 73 | if [ ! -z "${NAME}" ]; then DOCKERARGS="${DOCKERARGS} --rm --name ${NAME} "; fi 74 | if [ ! -z "${NETWORK}" ]; then DOCKERARGS="${DOCKERARGS} --network ${NETWORK} "; fi 75 | if [ ! -z "${VOLUMES}" ]; then DOCKERARGS="${DOCKERARGS}${VOLUMES}"; fi 76 | if [ ! -z "${ENVIRONMENT}" ]; then DOCKERARGS="${DOCKERARGS}${ENVIRONMENT}"; fi 77 | if [ ! -z "${PORTS}" ]; then DOCKERARGS="${DOCKERARGS}${PORTS}"; fi 78 | if [ ! -z "${EXPOSE}" ]; then DOCKERARGS="${DOCKERARGS}${EXPOSE}"; fi 79 | IMAGE=$(echo ${1} | jq -r .image | envsubst) 80 | TMP_COMMAND=$(echo ${1} | jq -r .command) 81 | echo "docker run ${DOCKERARGS} ${IMAGE} ${TMP_COMMAND}" 82 | } 83 | 84 | make_container_cmd() { 85 | DOCKERARGS=$(echo ${1} | jq -r .dockerargs) 86 | if [ "${DOCKERARGS}" == "null" ]; then DOCKERARGS=; fi 87 | SCRIPT_NAME=$(echo ${1} | jq -r .name) 88 | SCRIPT_NAME=$(slugify $SCRIPT_NAME) 89 | PROJECT=$(echo ${1} | jq -r .project) 90 | CONTAINER=$(echo ${1} | jq -r .container | envsubst) 91 | TMP_COMMAND=$(echo ${1} | jq -r .command) 92 | 93 | if [ "${PROJECT}" != "null" ]; then 94 | 95 | # create bash script to detect all running containers 96 | if [ "${SCRIPT_NAME}" == "null" ]; then 97 | SCRIPT_NAME=$(cat /proc/sys/kernel/random/uuid) 98 | fi 99 | cat << EOF > ${HOME_DIR}/projects/${SCRIPT_NAME}.sh 100 | #!/usr/bin/env bash 101 | set -e 102 | 103 | CONTAINERS=\$(docker ps --format '{{.Names}}' | grep -E "^${PROJECT}_${CONTAINER}.[0-9]+") 104 | for CONTAINER_NAME in \$CONTAINERS; do 105 | docker exec ${DOCKERARGS} \${CONTAINER_NAME} ${TMP_COMMAND} 106 | done 107 | EOF 108 | echo "/bin/bash ${HOME_DIR}/projects/${SCRIPT_NAME}.sh" 109 | # cat "/bin/bash ${HOME_DIR}/projects/${SCRIPT_NAME}.sh" 110 | else 111 | echo "docker exec ${DOCKERARGS} ${CONTAINER} ${TMP_COMMAND}" 112 | fi 113 | } 114 | 115 | #make_host_cmd() { 116 | # HOST_BINARY=$(echo ${1} | jq -r .host) 117 | # TMP_COMMAND=$(echo ${1} | jq -r .command) 118 | # echo "${HOST_BINARY} ${TMP_COMMAND}" 119 | #} 120 | 121 | make_cmd() { 122 | if [ "$(echo ${1} | jq -r .image)" != "null" ]; then 123 | make_image_cmd "$1" 124 | elif [ "$(echo ${1} | jq -r .container)" != "null" ]; then 125 | make_container_cmd "$1" 126 | #elif [ "$(echo ${1} | jq -r .host)" != "null" ]; then 127 | # make_host_cmd "$1" 128 | else 129 | echo ${1} | jq -r .command 130 | fi 131 | } 132 | 133 | parse_schedule() { 134 | case $1 in 135 | "@yearly") 136 | echo "0 0 1 1 *" 137 | ;; 138 | "@annually") 139 | echo "0 0 1 1 *" 140 | ;; 141 | "@monthly") 142 | echo "0 0 1 * *" 143 | ;; 144 | "@weekly") 145 | echo "0 0 * * 0" 146 | ;; 147 | "@daily") 148 | echo "0 0 * * *" 149 | ;; 150 | "@midnight") 151 | echo "0 0 * * *" 152 | ;; 153 | "@hourly") 154 | echo "0 * * * *" 155 | ;; 156 | "@every") 157 | TIME=$2 158 | TOTAL=0 159 | 160 | M=$(echo $TIME | grep -o '[0-9]\+m') 161 | H=$(echo $TIME | grep -o '[0-9]\+h') 162 | D=$(echo $TIME | grep -o '[0-9]\+d') 163 | 164 | if [ -n "${M}" ]; then 165 | TOTAL=$(($TOTAL + ${M::-1})) 166 | fi 167 | if [ -n "${H}" ]; then 168 | TOTAL=$(($TOTAL + ${H::-1} * 60)) 169 | fi 170 | if [ -n "${D}" ]; then 171 | TOTAL=$(($TOTAL + ${D::-1} * 60 * 24)) 172 | fi 173 | 174 | echo "*/${TOTAL} * * * *" 175 | ;; 176 | *) 177 | echo "${@}" 178 | ;; 179 | esac 180 | } 181 | 182 | function build_crontab() { 183 | 184 | rm -rf ${CRONTAB_FILE} 185 | 186 | ONSTART=() 187 | while read i ; do 188 | 189 | SCHEDULE=$(jq -r .[$i].schedule ${CONFIG} | sed 's/\*/\\*/g') 190 | if [ "${SCHEDULE}" == "null" ]; then 191 | echo "Schedule Missing: $(jq -r .[$i].schedule ${CONFIG})" 192 | continue 193 | fi 194 | SCHEDULE=$(parse_schedule ${SCHEDULE} | sed 's/\\//g') 195 | 196 | if [ "$(jq -r .[$i].command ${CONFIG})" == "null" ]; then 197 | echo "Command Missing: $(jq -r .[$i].command ${CONFIG})" 198 | continue 199 | fi 200 | 201 | COMMENT=$(jq -r .[$i].comment ${CONFIG}) 202 | if [ "${COMMENT}" != "null" ]; then 203 | echo "# ${COMMENT}" >> ${CRONTAB_FILE} 204 | fi 205 | 206 | SCRIPT_NAME=$(jq -r .[$i].name ${CONFIG}) 207 | SCRIPT_NAME=$(slugify $SCRIPT_NAME) 208 | if [ "${SCRIPT_NAME}" == "null" ]; then 209 | SCRIPT_NAME=$(cat /proc/sys/kernel/random/uuid) 210 | fi 211 | 212 | COMMAND="/bin/bash ${HOME_DIR}/jobs/${SCRIPT_NAME}.sh" 213 | cat << EOF > ${HOME_DIR}/jobs/${SCRIPT_NAME}.sh 214 | #!/usr/bin/env bash 215 | set -e 216 | 217 | # TODO find workaround 218 | # [error] write /dev/stdout: broken pipe <- when using docker commands 219 | #UUID=\$(cat /proc/sys/kernel/random/uuid) 220 | #exec > >(read message; echo "\${UUID} \$(date -Iseconds) [info] \$message" | tee -a ${LOG_FILE} ) 221 | #exec 2> >(read message; echo "\${UUID} \$(date -Iseconds) [error] \$message" | tee -a ${LOG_FILE} >&2) 222 | 223 | echo "Start Cronjob **${SCRIPT_NAME}** ${COMMENT}" 224 | 225 | $(make_cmd "$(jq -c .[$i] ${CONFIG})") 226 | EOF 227 | 228 | 229 | 230 | if [ "$(jq -r .[$i].trigger ${CONFIG})" != "null" ]; then 231 | while read j ; do 232 | if [ "$(jq .[$i].trigger[$j].command ${CONFIG})" == "null" ]; then 233 | echo "Command Missing: $(jq -r .[$i].trigger[$j].command ${CONFIG})" 234 | continue 235 | fi 236 | #TRIGGER_COMMAND=$(make_cmd "$(jq -c .[$i].trigger[$j] ${CONFIG})") 237 | echo "$(make_cmd "$(jq -c .[$i].trigger[$j] ${CONFIG})")" >> ${HOME_DIR}/jobs/${SCRIPT_NAME}.sh 238 | #COMMAND="${COMMAND} && ${TRIGGER_COMMAND}" 239 | done < <(jq -r '.['$i'].trigger|keys[]' ${CONFIG}) 240 | fi 241 | 242 | echo "echo \"End Cronjob **${SCRIPT_NAME}** ${COMMENT}\"" >> ${HOME_DIR}/jobs/${SCRIPT_NAME}.sh 243 | 244 | echo "${SCHEDULE} ${COMMAND}" >> ${CRONTAB_FILE} 245 | 246 | if [ "$(jq -r .[$i].onstart ${CONFIG})" == "true" ]; then 247 | ONSTART+=("${COMMAND}") 248 | fi 249 | done < <(jq -r '.|keys[]' ${CONFIG}) 250 | 251 | echo "##### crontab generation complete #####" 252 | cat ${CRONTAB_FILE} 253 | 254 | echo "##### run commands with onstart #####" 255 | for COMMAND in "${ONSTART[@]}"; do 256 | echo "${COMMAND}" 257 | ${COMMAND} & 258 | done 259 | } 260 | 261 | 262 | ensure_docker_socket_accessible 263 | 264 | start_app() { 265 | get_config 266 | if [ -f "${HOME_DIR}/config.working.json" ]; then 267 | export CONFIG=${HOME_DIR}/config.working.json 268 | elif [ -f "${HOME_DIR}/config.json" ]; then 269 | export CONFIG=${HOME_DIR}/config.json 270 | else 271 | echo "NO CONFIG FILE FOUND" 272 | fi 273 | if [ "$1" = "crond" ]; then 274 | if [ -f ${CONFIG} ]; then 275 | build_crontab 276 | else 277 | echo "Unable to find ${CONFIG}" 278 | fi 279 | fi 280 | echo "$@" 281 | exec "$@" 282 | } 283 | 284 | start_app "$@" 285 | -------------------------------------------------------------------------------- /test_logging: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | # This file is for testing the logging of docker output #8 5 | 6 | LOG_FILE=./jobs.log 7 | touch ${LOG_FILE} 8 | UUID="xxxxxxxxxxxxxxxxx" 9 | 10 | exec > >(read message; echo "${UUID} $(date) [info] $message" | tee -a ${LOG_FILE} ) 11 | exec 2> >(read message; echo "${UUID} $(date) [error] $message" | tee -a ${LOG_FILE} >&2) 12 | 13 | echo "Start" 14 | 15 | docker run alpine sh -c 'while :; do echo "ping"; sleep 1; done' 16 | # [error] write /dev/stdout: broken pipe 17 | # --log-driver syslog <- errors 18 | # --log-driver none <- errors 19 | 20 | echo "End" 21 | --------------------------------------------------------------------------------