diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a06675f..88e92f1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,3 +12,18 @@ jobs: - uses: actions/checkout@v4 - name: Run Docker build and test run: bash test.sh + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Log in to Docker Hub + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USER }} + password: ${{ secrets.DOCKER_PASS }} + - name: Build and push Docker image + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ secrets.DOCKER_USER }}/${{ github.event.repository.name }}:latest diff --git a/README.md b/README.md index b8d0ceb..09df45b 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,42 @@ This container runs a cron daemon configured by environment variables or an optional `config.json` file. +## Defining jobs with environment variables + +Jobs can be scheduled directly with pairs of `CMD_n` and `INTERVAL_n` variables. Each pair defines one job: + +```sh +docker run \ + -e CMD_1="echo hello" -e INTERVAL_1="*/5 * * * *" \ + -e CMD_2="date" -e INTERVAL_2="0 1 * * *" image +``` + +The index `n` starts at 1 and must increase sequentially with no gaps; the parser stops at the first missing pair. +Both variables of a pair are required. `INTERVAL_n` expects a standard five-field cron expression, and `CMD_n` +is any shell command. + +### Overriding values from `config.json` + +If a `CONFIG_FILE` is supplied, jobs from that file populate unset `CMD_n`/`INTERVAL_n` variables. Explicit +environment values override those from the file: + +```json +# config.json +{ + "jobs": [ + {"cmd": "echo from file", "interval": "*/5 * * * *"} + ] +} +``` + +```sh +docker run -v $(pwd)/config.json:/app/config.json -e CONFIG_FILE=/app/config.json \ + -e CMD_1="echo from env" -e INTERVAL_1="*/10 * * * *" image +``` + +In this example the job runs `echo from env` every ten minutes, ignoring the `cmd` and `interval` from +`config.json`. + ## Using a configuration file 1. Create a `config.json` containing jobs with a `cmd` and `interval`: diff --git a/run.sh b/run.sh index 58413bc..466a1ac 100755 --- a/run.sh +++ b/run.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash -set -e +set -euo pipefail +# Enable strict error handling: exit on errors, undefined variables, and +# failures within pipelines. generate_cron() { bash /app/config_parser.sh @@ -12,16 +14,36 @@ reload_crond() { watch_config() { local file="$1" if command -v inotifywait >/dev/null 2>&1; then - inotifywait -m -e close_write,move,delete "$file" | while read -r _; do - generate_cron - reload_crond - done + local dir="$(dirname "$file")" + local base="$(basename "$file")" + inotifywait -m -e close_write,move,create,delete "$dir" --format '%e %f' | + while read -r events fname; do + [ "$fname" = "$base" ] || continue + if echo "$events" | grep -qE 'DELETE|MOVED_FROM'; then + echo "Warning: configuration file '$file' missing, skipping cron generation" >&2 + elif [ -f "$file" ]; then + generate_cron + reload_crond + fi + done || true else local prev - prev="$(md5sum "$file" 2>/dev/null | awk '{print $1}')" + if [ -f "$file" ]; then + prev="$(md5sum "$file" 2>/dev/null | awk '{print $1}' || true)" + else + prev="missing" + echo "Warning: configuration file '$file' missing, skipping cron generation" >&2 + fi while sleep 5; do + if [ ! -f "$file" ]; then + if [ "$prev" != "missing" ]; then + prev="missing" + echo "Warning: configuration file '$file' missing, skipping cron generation" >&2 + fi + continue + fi local curr - curr="$(md5sum "$file" 2>/dev/null | awk '{print $1}')" + curr="$(md5sum "$file" 2>/dev/null | awk '{print $1}' || true)" if [ "$curr" != "$prev" ]; then prev="$curr" generate_cron @@ -32,13 +54,21 @@ watch_config() { } # Generate cron entries from environment/config -generate_cron +if [ -n "${CONFIG_FILE:-}" ]; then + if [ -f "$CONFIG_FILE" ]; then + generate_cron + else + echo "Warning: configuration file '$CONFIG_FILE' missing, skipping cron generation" >&2 + fi +else + generate_cron +fi crond -f -l 2 & CROND_PID=$! -if [ -n "$CONFIG_FILE" ] && [ -f "$CONFIG_FILE" ]; then +if [ -n "${CONFIG_FILE:-}" ]; then watch_config "$CONFIG_FILE" & fi -wait $CROND_PID +wait "$CROND_PID" diff --git a/test.sh b/test.sh index 772d64b..9a3a7fe 100755 --- a/test.sh +++ b/test.sh @@ -1,31 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -IMAGE_NAME=docker-cron-test -LOG_FILE=test.log -TEMP_CONFIG=config.json +bash tests/config_parser_test.sh +bash tests/watch_config_test.sh -cleanup() { - rm -f "$TEMP_CONFIG" "$LOG_FILE" -} -trap cleanup EXIT - -# Build docker image - docker build -t "$IMAGE_NAME" . - -# Create temporary config file -cat > "$TEMP_CONFIG" <<'CFG' -{ - "jobs": [ - {"cmd": "echo hello from cron", "interval": "* * * * *"} - ] -} -CFG - -# Run config parser inside container and display generated crontab - docker run --rm -v "$(pwd)/$TEMP_CONFIG:/config.json" -e CONFIG_FILE=/config.json --entrypoint /bin/sh "$IMAGE_NAME" -c '/app/config_parser.sh; cat /etc/crontabs/root' | tee "$LOG_FILE" - -# Verify expected command is present in log -grep -q "echo hello from cron" "$LOG_FILE" - -echo "Test completed successfully" +echo "All tests passed" diff --git a/tests/config_parser_test.sh b/tests/config_parser_test.sh index 387900e..b79cf3e 100755 --- a/tests/config_parser_test.sh +++ b/tests/config_parser_test.sh @@ -55,7 +55,37 @@ CFG rm -rf /etc/crontabs } +test_env_overrides_config() { + local tmpdir + tmpdir=$(mktemp -d) + cat >"$tmpdir/config.json" <<'CFG' +{ + "jobs": [ + {"cmd": "/bin/echo hi", "interval": "0 1 * * *"} + ] +} +CFG + + CMD_1="/bin/date" INTERVAL_1="*/5 * * * *" CONFIG_FILE="$tmpdir/config.json" bash app/config_parser.sh + + local expected=$'*/5 * * * * /bin/date' + local actual + actual=$(cat /etc/crontabs/root) + if [[ "$actual" != "$expected" ]]; then + echo "crontab content mismatch" >&2 + echo "expected:" >&2 + printf '%s\n' "$expected" >&2 + echo "actual:" >&2 + printf '%s\n' "$actual" >&2 + exit 1 + fi + + rm -rf "$tmpdir" + rm -rf /etc/crontabs +} + test_writes_crontab_from_config test_errors_when_missing_pair +test_env_overrides_config echo "All config_parser tests passed" diff --git a/tests/watch_config_test.sh b/tests/watch_config_test.sh new file mode 100755 index 0000000..56c69ea --- /dev/null +++ b/tests/watch_config_test.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Ensure crond is available +if ! command -v crond >/dev/null 2>&1; then + if command -v busybox >/dev/null 2>&1; then + ln -sf "$(command -v busybox)" /usr/bin/crond + elif command -v apt-get >/dev/null 2>&1; then + apt-get update >/dev/null + apt-get install -y busybox >/dev/null + ln -sf /bin/busybox /usr/bin/crond + else + echo "crond not available, skipping watch_config test" >&2 + exit 0 + fi +fi + +# Ensure config parser is accessible at /app +if [ ! -e /app ]; then + ln -s "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/app" /app +fi + +# busybox crond expects /var/spool/cron/crontabs; map it to /etc/crontabs +if [ ! -e /var/spool/cron/crontabs ]; then + mkdir -p /var/spool/cron + mkdir -p /etc/crontabs + ln -s /etc/crontabs /var/spool/cron/crontabs +fi + +TMPDIR=$(mktemp -d) +CONFIG="$TMPDIR/config.json" +LOG="$TMPDIR/run.log" +RUN_PID=0 + +cleanup() { + if [ "$RUN_PID" -ne 0 ]; then + pkill -P "$RUN_PID" >/dev/null 2>&1 || true + kill "$RUN_PID" >/dev/null 2>&1 || true + fi + rm -rf "$TMPDIR" +} +trap cleanup EXIT + +cat >"$CONFIG" <<'CFG' +{ + "jobs": [ + {"cmd": "/bin/echo first", "interval": "* * * * *"} + ] +} +CFG + +CONFIG_FILE="$CONFIG" bash ./run.sh >"$LOG" 2>&1 & +RUN_PID=$! + +# Allow initial generation +sleep 2 + +grep -q "echo first" /etc/crontabs/root + +# Remove config file and ensure watcher keeps running +rm "$CONFIG" + +sleep 7 + +kill -0 "$RUN_PID" + +grep -q "configuration file.*missing" "$LOG" + +# Recreate config file with different command +cat >"$CONFIG" <<'CFG' +{ + "jobs": [ + {"cmd": "/bin/echo second", "interval": "* * * * *"} + ] +} +CFG + +sleep 7 + +grep -q "echo second" /etc/crontabs/root + +echo "watch_config test passed"