Skip to content

Commit

Permalink
Merge pull request #18 from shizunge/readme
Browse files Browse the repository at this point in the history
Add pre and post run cmd. Find self service name automatically. Excluding update time from sleep time.
  • Loading branch information
shizunge authored Jan 19, 2024
2 parents f8cac1f + 43c1294 commit 6daefcb
Show file tree
Hide file tree
Showing 16 changed files with 246 additions and 34 deletions.
1 change: 1 addition & 0 deletions .github/workflows/on-push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ jobs:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
provenance: false
# - name: Post push tests
# run: |
# echo "Start running tests"
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/on-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ jobs:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
provenance: false
# - name: Post push tests
# run: |
# echo "Start running tests"
Expand Down
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Gantry

[*Gantry*](https://github.com/shizunge/gantry) is a tool to update docker swarm services, enhanced [Shepherd](https://github.com/containrrr/shepherd).
[*Gantry*](https://github.com/shizunge/gantry) is a tool to update docker swarm services, [enhanced Shepherd](docs/migration.md).

## Usage

We release *Gantry* as a container [image](https://hub.docker.com/r/shizunge/gantry). You can create a docker service and run it on a swarm manager node.
*Gantry* is released as a container [image](https://hub.docker.com/r/shizunge/gantry). You can create a docker service and run it on a swarm manager node.

```
docker service create \
Expand All @@ -16,9 +16,9 @@ docker service create \
shizunge/gantry
```

Or with docker compose, see the [example](examples/docker-compose.yml).
Or with docker compose, see the [example](examples/README.md).

You can also run *Gantry* as a script outside the container `source ./src/entrypoint.sh`. *Gantry* is written to work with `busybox ash`. It should also work with `bash`.
You can also run *Gantry* as a script outside the container `source ./src/entrypoint.sh`. *Gantry* is written to work with `busybox ash` as well as `bash`.

## Configurations

Expand All @@ -30,7 +30,9 @@ You can configure the most behaviors of *Gantry* via environment variables.
|-----------------------|---------|------------|
| GANTRY_LOG_LEVEL | INFO | Control how many logs generated by *Gantry*. Valid values are `NONE`, `ERROR`, `WARN`, `INFO`, `DEBUG` (case sensitive). |
| GANTRY_NODE_NAME | | Add node name to logs. |
| GANTRY_SLEEP_SECONDS | 0 | Sleep time between two updates. Set it to 0 to run *Gantry* once and then exit. |
| GANTRY_POST_RUN_CMD | | Command(s) to eval after each updating iteration. |
| GANTRY_PRE_RUN_CMD | | Command(s) to eval before each updating iteration. |
| GANTRY_SLEEP_SECONDS | 0 | Interval between two updates. Set it to 0 to run *Gantry* once and then exit. Sleep time will exclude the time spent on updating services. |
| TZ | | Set timezone for time in logs. |

### To login to registries
Expand All @@ -54,7 +56,7 @@ You can configure the most behaviors of *Gantry* via environment variables.
| GANTRY_SERVICES_EXCLUDED | | A space separated list of services names that are excluded from updating. |
| GANTRY_SERVICES_EXCLUDED_FILTERS | | A space separated list of [filters](https://docs.docker.com/engine/reference/commandline/service_ls/#filter). Exclude services which match the given filters from updating. |
| GANTRY_SERVICES_FILTERS | | A space separated list of [filters](https://docs.docker.com/engine/reference/commandline/service_ls/#filter) that are accepted by `docker service ls --filter` to select services to update. |
| GANTRY_SERVICES_SELF | | A service name to indicate whether a service is *Gantry* itself. *Gantry* will be the first service being updated. The manifest inspection will be always performed on the *Gantry* service to avoid an infinity loop of updating itself. |
| GANTRY_SERVICES_SELF | | This is optional. *Gantry* will try to find the service name of itself automatically, and update itself firstly. The manifest inspection will be always performed on the *Gantry* service to avoid an infinity loop of updating itself. User can use this to ask *Gantry* to update another service firstly or in case *Gantry* fails to find the service name of itself. |

### To check if new images are available

Expand Down
4 changes: 2 additions & 2 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ At the end of updating, *Gantry* optionally removes the old images.

### How to update standalone docker containers?

*Gantry* only works for docker swarm services. If you need to update standalone docker containers, you can try [watchtower](https://github.com/containrrr/watchtower).
*Gantry* only works for docker swarm services. If you need to update standalone docker containers, you can try [*watchtower*](https://github.com/containrrr/watchtower). *Gantry* can launch *watchtower* via `GANTRY_PRE_RUN_CMD` or `GANTRY_POST_RUN_CMD`. See the [example](../examples/prune-and-watchtower).

### How to filters multiple services by name?

Expand All @@ -30,7 +30,7 @@ gantry_finalize;

### How to run *Gantry* on a cron schedule?

You can start *Gantry* as a docker swarm service and use [`swarm-cronjob`](https://github.com/crazy-max/swarm-cronjob) to run it at a given time. When use `swarm-cronjob`, you need to set `GANTRY_SLEEP_SECONDS` to 0. See the [example](../examples/docker-compose.yml).
You can start *Gantry* as a docker swarm service and use [`swarm-cronjob`](https://github.com/crazy-max/swarm-cronjob) to run it at a given time. When use `swarm-cronjob`, you need to set `GANTRY_SLEEP_SECONDS` to 0. See the [example](../examples/cronjob).

### How to update services with no running tasks?

Expand Down
2 changes: 2 additions & 0 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ The label on the services to select config to enable authentication is renamed t
| GANTRY_MANIFEST_CMD |
| GANTRY_MANIFEST_OPTIONS |
| GANTRY_NOTIFICATION_TITLE |
| GANTRY_POST_RUN_CMD |
| GANTRY_PRE_RUN_CMD |
| GANTRY_REGISTRY_CONFIG |
| GANTRY_REGISTRY_CONFIG_FILE |
| GANTRY_REGISTRY_HOST_FILE |
Expand Down
10 changes: 10 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Examples

## [cronjob](./cronjob)

Run *gantry* at the specific time.

## [prune-and-watchtower](./prune-and-watchtower)

Remove unused containers, networks and images. Update standalone docker containers. Update docker swarm services.

4 changes: 4 additions & 0 deletions examples/cronjob/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# cronjob

Use [*swarm-cronjob*](https://github.com/crazy-max/swarm-cronjob) to launch *gantry* at a given time.

Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ services:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- "GANTRY_NODE_NAME={{.Node.Hostname}}"
- "GANTRY_SERVICES_SELF=${STACK}_gantry"
# The gantry service is able to find the name of itself service. Use GANTRY_SERVICES_SELF when you want to set a different value.
# - "GANTRY_SERVICES_SELF=${STACK}_gantry"
- "GANTRY_SLEEP_SECONDS=0"
deploy:
replicas: 0
Expand Down
8 changes: 8 additions & 0 deletions examples/prune-and-watchtower/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# prune and watchtower

This example runs `docker system prune` and *watchtower* before updating docker swarm services.

* [`docker system prune`](https://docs.docker.com/engine/reference/commandline/system_prune/) removes all unused containers, networks and images.
* [*watchtower*](https://github.com/containrrr/watchtower) updates standalone docker containers.
* [*gantry*](https://github.com/shizunge/gantry) updates docker swarm services.

53 changes: 53 additions & 0 deletions examples/prune-and-watchtower/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
version: "3.8"

services:
gantry:
image: shizunge/gantry
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- "GANTRY_NODE_NAME={{.Node.Hostname}}"
- "GANTRY_PRE_RUN_CMD=SERVICE_NAME=gantry-prune;
docker service remove $${SERVICE_NAME} 2>/dev/null;
docker service create --mode global-job --name $${SERVICE_NAME}
--mount type=bind,source=/var/run/docker.sock,destination=/var/run/docker.sock
--entrypoint docker
alpinelinux/docker-cli
system prune -f;
docker service logs $${SERVICE_NAME};
docker service remove $${SERVICE_NAME};
SERVICE_NAME=gantry-container;
docker service remove $${SERVICE_NAME} 2>/dev/null;
docker service create --mode global-job --name $${SERVICE_NAME}
--mount type=bind,source=/var/run/docker.sock,destination=/var/run/docker.sock
ghcr.io/containrrr/watchtower
--cleanup=true
--label-enable
--run-once=true
--stop-timeout=60s
--tlsverify=true;
docker service logs $${SERVICE_NAME};
docker service remove $${SERVICE_NAME};
"
- "GANTRY_POST_RUN_CMD=echo \"This is a post run command.\";"
- "GANTRY_SLEEP_SECONDS=0"
deploy:
replicas: 0
placement:
constraints:
- node.role==manager
restart_policy:
condition: none
labels:
- swarm.cronjob.enable=true
- swarm.cronjob.schedule=45 23 0 * * *
- swarm.cronjob.skip-running=true

cronjob:
image: crazymax/swarm-cronjob:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
deploy:
placement:
constraints:
- node.role==manager
48 changes: 40 additions & 8 deletions src/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,28 @@ skip_current_node() {
return 1
}

exec_pre_run_cmd() {
local CMD RT LOG
CMD=${GANTRY_PRE_RUN_CMD:-""}
[ -z "${CMD}" ] && return 0
log INFO "Run pre-run command: ${CMD}"
LOG=$(eval "${CMD}")
RT=$?
log INFO "${LOG}"
log INFO "Finish pre-run command. Return value ${RT}."
}

exec_post_run_cmd() {
local CMD RT LOG
CMD=${GANTRY_POST_RUN_CMD:-""}
[ -z "${CMD}" ] && return 0
log INFO "Run post-run command: ${CMD}"
LOG=$(eval "${CMD}")
RT=$?
log INFO "${LOG}"
log INFO "Finish post-run command. Return value ${RT}."
}

gantry() {
local STACK="${1:-gantry}"
local START_TIME=
Expand All @@ -64,6 +86,8 @@ gantry() {
local DOCKER_HUB_RATE_USED=
local TIME_ELAPSED=

exec_pre_run_cmd

log INFO "Starting."
gantry_initialize "${STACK}"
ACCUMULATED_ERRORS=$((ACCUMULATED_ERRORS + $?))
Expand Down Expand Up @@ -95,28 +119,36 @@ gantry() {
else
log INFO "${MESSAGE}"
fi

exec_post_run_cmd

return ${ACCUMULATED_ERRORS}
}

main() {
LOG_LEVEL="${GANTRY_LOG_LEVEL:-${LOG_LEVEL}}"
NODE_NAME="${GANTRY_NODE_NAME:-${NODE_NAME}}"
local SLEEP_SECONDS="${GANTRY_SLEEP_SECONDS:-0}"
if ! is_number "${SLEEP_SECONDS}"; then
export LOG_LEVEL NODE_NAME
local INTERVAL_SECONDS="${GANTRY_SLEEP_SECONDS:-0}"
if ! is_number "${INTERVAL_SECONDS}"; then
log ERROR "GANTRY_SLEEP_SECONDS must be a number. Got \"${GANTRY_SLEEP_SECONDS}\"."
return 1;
fi
local STACK="${1:-gantry}"
local RETURN_VALUE=0
local START_TIME PASSED_SECONDS SLEEP_SECONDS
while true; do
# SC2034 (warning): LOG_SCOPE appears unused. Verify use (or export if used externally).
# shellcheck disable=SC2034
LOG_SCOPE="${STACK}"
export LOG_SCOPE="${STACK}"
START_TIME=$(date +%s)
gantry "${@}"
RETURN_VALUE=$?
[ "${SLEEP_SECONDS}" -le 0 ] && break;
log INFO "Sleeping ${SLEEP_SECONDS} seconds before next update."
sleep "${SLEEP_SECONDS}"
[ "${INTERVAL_SECONDS}" -le 0 ] && break;
PASSED_SECONDS=$(difference_between "${START_TIME}" "$(date +%s)")
SLEEP_SECONDS=$((INTERVAL_SECONDS - PASSED_SECONDS))
if [ "${SLEEP_SECONDS}" -gt 0 ]; then
log INFO "Sleeping ${SLEEP_SECONDS} seconds before next update."
sleep "${SLEEP_SECONDS}"
fi
done
return ${RETURN_VALUE}
}
Expand Down
32 changes: 21 additions & 11 deletions src/lib-common.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/bin/sh
# Copyright (C) 2023 Shizun Ge
# Copyright (C) 2023-2024 Shizun Ge
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
Expand Down Expand Up @@ -75,6 +75,7 @@ log_docker_time() {
busybox date -d "@${EPOCH}" -Iseconds 2>&1
}

# Convert logs from `docker service logs` to `log` format.
# docker service logs --timestamps --no-task-ids <service>
# 2023-06-22T01:20:54.535860111Z <task>@<node> | <msg>
log_docker_line() {
Expand Down Expand Up @@ -188,6 +189,10 @@ swarm_network_arguments() {
echo "${NETWORK_ARG} --dns=${NETWORK_DNS_IP}"
}

timezone_arguments() {
echo "--env \"TZ=${TZ}\" --mount type=bind,source=/etc/localtime,destination=/etc/localtime,ro"
}

get_docker_command_name_arg() {
# get <NAME> from "--name <NAME>" or "--name=<NAME>"
echo "${@}" | tr '\n' ' ' | sed -E 's/.*--name[ =]([^ ]*).*/\1/'
Expand Down Expand Up @@ -244,12 +249,17 @@ docker_service_task_states() {
done
}

# Usage: wait_service_state <SERVICE_NAME> [--running] [--complete]
# Wait for the service, usually a global job or a replicated job, to reach either running or complete state.
# The function returns immediately when any of the tasks of the service fails.
# In case of task failing, the function returns a non-zero value.
wait_service_state() {
local SERVICE_NAME="${1}"
local WAIT_RUNNING="${2:-"false"}"
local WAIT_COMPLETE="${3:-"false"}"
local RETURN_VALUE="${4:-0}"
local SLEEP_SECONDS="${5:-1}"
local SERVICE_NAME="${1}"; shift;
local WAIT_RUNNING WAIT_COMPLETE;
WAIT_RUNNING=$(echo "${@}" | grep -q -- "--running" && echo "true" || echo "false")
WAIT_COMPLETE=$(echo "${@}" | grep -q -- "--complete" && echo "true" || echo "false")
local RETURN_VALUE=0
local SLEEP_SECONDS=1
local STATES=
STATES=$(docker_service_task_states "${SERVICE_NAME}" 2>&1)
while is_true "${WAIT_RUNNING}" || is_true "${WAIT_COMPLETE}" ; do
Expand Down Expand Up @@ -326,16 +336,16 @@ docker_replicated_job() {
IS_DETACH=$(get_docker_command_detach "${@}")
# Add "--detach" to work around https://github.com/docker/cli/issues/2979
# The Docker CLI does not exit on failures.
local WAIT_RUNNING="false"
local WAIT_COMPLETE=
WAIT_COMPLETE=$(if ${IS_DETACH}; then echo "false"; else echo "true"; fi)
log INFO "Starting service ${SERVICE_NAME}."
docker service create \
--mode replicated-job --detach \
"${@}" >/dev/null
local RETURN_VALUE=$?
# return the code from wait_service_state
wait_service_state "${SERVICE_NAME}" "${WAIT_RUNNING}" "${WAIT_COMPLETE}" "${RETURN_VALUE}"
# If the command line does not contain '--detach', the function returns til the replicated job is complete.
if ! "${IS_DETACH}"; then
wait_service_state "${SERVICE_NAME}" --complete || return $?
fi
return ${RETURN_VALUE}
}

container_status() {
Expand Down
Loading

0 comments on commit 6daefcb

Please sign in to comment.