From 0ada34b30b2ce5bf3a2ad6d17a5ccfd8d3679802 Mon Sep 17 00:00:00 2001 From: Shizun Ge Date: Wed, 17 Jan 2024 21:41:41 -0800 Subject: [PATCH 01/13] find self service automatically --- README.md | 4 ++-- src/lib-gantry.sh | 41 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5a48fe2..a2e334c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 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 @@ -54,7 +54,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 diff --git a/src/lib-gantry.sh b/src/lib-gantry.sh index aab2c5a..46bad91 100755 --- a/src/lib-gantry.sh +++ b/src/lib-gantry.sh @@ -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 @@ -246,8 +246,45 @@ in_list() { return 1 } +current_container_name() { + local ALL_NETWORKS GWBRIDGE_NETWORK IPS; + ALL_NETWORKS=$(docker network ls --format {{.ID}}) || return 1; + [ -z "${ALL_NETWORKS}" ] && return 0; + GWBRIDGE_NETWORK=$(docker network ls --format {{.ID}} --filter "name=docker_gwbridge") || return 1; + IPS=$(ip route | grep src | sed -n "s/.* src \(\S*\).*$/\1/p"); + [ -z "${IPS}" ] && return 0; + local NID; + for NID in ${ALL_NETWORKS}; do + [ "${NID}" = "${GWBRIDGE_NETWORK}" ] && continue; + local ALL_LOCAL_NAME_AND_IP; + ALL_LOCAL_NAME_AND_IP=$(docker network inspect ${NID} --format "{{range .Containers}}{{.Name}}={{println .IPv4Address}}{{end}}") || return 1; + for NAME_AND_IP in ${ALL_LOCAL_NAME_AND_IP}; do + [ -z "${NAME_AND_IP}" ] && continue; + for IP in ${IPS}; do + [ ! $(echo ${NAME_AND_IP} | grep ${IP}) ] && continue; + local NAME=$(echo ${NAME_AND_IP} | sed "s/\(.*\)=${IP}.*$/\1/"); + echo ${NAME}; + return 0; + done; + done; + done; + return 0; +} + +current_service_name() { + local CNAME + CNAME=$(current_container_name) || return 1 + [ -z "${CNAME}" ] && return 0 + SNAME=$(docker container inspect ${CNAME} --format '{{range $key,$value := .Config.Labels}}{{$key}}={{println $value}}{{end}}' | grep "com.docker.swarm.service.name" | sed "s/com.docker.swarm.service.name=\(.*\)$/\1/") || return 1 + echo ${SNAME} +} + service_is_self() { - local SELF="${GANTRY_SERVICES_SELF:-""}" + if [ -z "${GANTRY_SERVICES_SELF}" ]; then + export GANTRY_SERVICES_SELF=$(current_service_name) + [ -n "${GANTRY_SERVICES_SELF}" ] && log INFO "Set GANTRY_SERVICES_SELF to ${GANTRY_SERVICES_SELF}." + fi + local SELF="${GANTRY_SERVICES_SELF}" local SERVICE_NAME="${1}" [ "${SERVICE_NAME}" = "${SELF}" ] } From 08949c4663bc281b1fdac2a9f927762626ea1fbf Mon Sep 17 00:00:00 2001 From: Shizun Ge Date: Wed, 17 Jan 2024 21:57:01 -0800 Subject: [PATCH 02/13] fix shellcheck and update test comments --- src/lib-gantry.sh | 20 +++++++++++--------- tests/test_entrypoint.sh | 8 +++++++- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/lib-gantry.sh b/src/lib-gantry.sh index 46bad91..10d6deb 100755 --- a/src/lib-gantry.sh +++ b/src/lib-gantry.sh @@ -248,22 +248,23 @@ in_list() { current_container_name() { local ALL_NETWORKS GWBRIDGE_NETWORK IPS; - ALL_NETWORKS=$(docker network ls --format {{.ID}}) || return 1; + ALL_NETWORKS=$(docker network ls --format '{{.ID}}') || return 1; [ -z "${ALL_NETWORKS}" ] && return 0; - GWBRIDGE_NETWORK=$(docker network ls --format {{.ID}} --filter "name=docker_gwbridge") || return 1; + GWBRIDGE_NETWORK=$(docker network ls --format '{{.ID}}' --filter 'name=docker_gwbridge') || return 1; IPS=$(ip route | grep src | sed -n "s/.* src \(\S*\).*$/\1/p"); [ -z "${IPS}" ] && return 0; local NID; for NID in ${ALL_NETWORKS}; do [ "${NID}" = "${GWBRIDGE_NETWORK}" ] && continue; local ALL_LOCAL_NAME_AND_IP; - ALL_LOCAL_NAME_AND_IP=$(docker network inspect ${NID} --format "{{range .Containers}}{{.Name}}={{println .IPv4Address}}{{end}}") || return 1; + ALL_LOCAL_NAME_AND_IP=$(docker network inspect "${NID}" --format "{{range .Containers}}{{.Name}}={{println .IPv4Address}}{{end}}") || return 1; for NAME_AND_IP in ${ALL_LOCAL_NAME_AND_IP}; do [ -z "${NAME_AND_IP}" ] && continue; for IP in ${IPS}; do - [ ! $(echo ${NAME_AND_IP} | grep ${IP}) ] && continue; - local NAME=$(echo ${NAME_AND_IP} | sed "s/\(.*\)=${IP}.*$/\1/"); - echo ${NAME}; + echo "${NAME_AND_IP}" | grep -q "${IP}" || continue; + local NAME; + NAME=$(echo "${NAME_AND_IP}" | sed "s/\(.*\)=${IP}.*$/\1/"); + echo "${NAME}"; return 0; done; done; @@ -275,13 +276,14 @@ current_service_name() { local CNAME CNAME=$(current_container_name) || return 1 [ -z "${CNAME}" ] && return 0 - SNAME=$(docker container inspect ${CNAME} --format '{{range $key,$value := .Config.Labels}}{{$key}}={{println $value}}{{end}}' | grep "com.docker.swarm.service.name" | sed "s/com.docker.swarm.service.name=\(.*\)$/\1/") || return 1 - echo ${SNAME} + SNAME=$(docker container inspect "${CNAME}" --format '{{range $key,$value := .Config.Labels}}{{$key}}={{println $value}}{{end}}' | grep "com.docker.swarm.service.name" | sed "s/com.docker.swarm.service.name=\(.*\)$/\1/") || return 1 + echo "${SNAME}" } service_is_self() { if [ -z "${GANTRY_SERVICES_SELF}" ]; then - export GANTRY_SERVICES_SELF=$(current_service_name) + GANTRY_SERVICES_SELF=$(current_service_name) + export GANTRY_SERVICES_SELF [ -n "${GANTRY_SERVICES_SELF}" ] && log INFO "Set GANTRY_SERVICES_SELF to ${GANTRY_SERVICES_SELF}." fi local SELF="${GANTRY_SERVICES_SELF}" diff --git a/tests/test_entrypoint.sh b/tests/test_entrypoint.sh index e5763ab..9f457f3 100755 --- a/tests/test_entrypoint.sh +++ b/tests/test_entrypoint.sh @@ -471,6 +471,10 @@ test_MANIFEST_CMD_none() { export GANTRY_UPDATE_OPTIONS="--force" STDOUT=$(run_gantry "${FUNCNAME[0]}" 2>&1 | tee >(cat 1>&2)) + # Do not set GANTRY_SERVICES_SELF, it should be set autoamtically + # If we are not testing gantry inside a container, it should failed to find the service name. + # To test gantry container, we need to use run_gantry_container. + expect_no_message "${STDOUT}" ".*GRANTRY_SERVICES_SELF.*" # Gantry is still trying to update the service. # But it will see no new images. expect_no_message "${STDOUT}" "${SKIP_UPDATING_SERVICE}.*${SERVICE_NAME}" @@ -495,7 +499,7 @@ test_MANIFEST_CMD_none() { } test_MANIFEST_CMD_none_SERVICES_SELF() { - # If the service is self, it will always run manifest checking. + # If the service is self, it will always run manifest checking. Even if the CMD is set to none local IMAGE_WITH_TAG="${1}" local SERVICE_NAME STDOUT SERVICE_NAME="gantry-test-$(unique_id)" @@ -505,11 +509,13 @@ test_MANIFEST_CMD_none_SERVICES_SELF() { start_replicated_service "${SERVICE_NAME}" "${IMAGE_WITH_TAG}" # No image updates after service started. + # Explicitly set GANTRY_SERVICES_SELF export GANTRY_SERVICES_FILTERS="name=${SERVICE_NAME}" export GANTRY_SERVICES_SELF="${SERVICE_NAME}" export GANTRY_MANIFEST_CMD="none" STDOUT=$(run_gantry "${FUNCNAME[0]}" 2>&1 | tee >(cat 1>&2)) + expect_no_message "${STDOUT}" ".*GRANTRY_SERVICES_SELF.*" expect_no_message "${STDOUT}" "${SKIP_UPDATING_SERVICE}.*${SERVICE_NAME}" expect_message "${STDOUT}" "${SERVICE_NAME}.*${NO_NEW_IMAGE}" expect_no_message "${STDOUT}" "${SERVICE_NAME}.*${UPDATED}" From b4d9a79f1ea15a208bebd0e57b261a021fce18e6 Mon Sep 17 00:00:00 2001 From: Shizun Ge Date: Wed, 17 Jan 2024 22:36:27 -0800 Subject: [PATCH 03/13] add provenance: false to avoid unknown images --- .github/workflows/on-push.yml | 1 + .github/workflows/on-release.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/on-push.yml b/.github/workflows/on-push.yml index c808489..e97fb11 100644 --- a/.github/workflows/on-push.yml +++ b/.github/workflows/on-push.yml @@ -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" diff --git a/.github/workflows/on-release.yml b/.github/workflows/on-release.yml index da8af00..efd891c 100644 --- a/.github/workflows/on-release.yml +++ b/.github/workflows/on-release.yml @@ -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" From a6372643d470b0f1d3e001efcf3fb6b3809326ef Mon Sep 17 00:00:00 2001 From: Shizun Ge Date: Wed, 17 Jan 2024 22:48:30 -0800 Subject: [PATCH 04/13] remove image first then report overall status --- src/lib-gantry.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib-gantry.sh b/src/lib-gantry.sh index 10d6deb..70e06ea 100755 --- a/src/lib-gantry.sh +++ b/src/lib-gantry.sh @@ -586,6 +586,6 @@ gantry_update_services_list() { gantry_finalize() { local STACK="${1:-gantry}" - report_services; remove_images "${STACK}_image-remover" + report_services; } From e3cd279bcd6acfabfe9d95e62090b32a96422dd4 Mon Sep 17 00:00:00 2001 From: Shizun Ge Date: Wed, 17 Jan 2024 22:49:12 -0800 Subject: [PATCH 05/13] add pre and post update cmd --- src/entrypoint.sh | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/entrypoint.sh b/src/entrypoint.sh index 386c8d2..8a51d4e 100755 --- a/src/entrypoint.sh +++ b/src/entrypoint.sh @@ -50,6 +50,26 @@ skip_current_node() { return 1 } +run_pre_update_cmd() { + local CMD RT + CMD=${GANTRY_PRE_UPDATE_CMD:-""} + [ -z "${CMD}" ] && return 0 + log INFO "Run pre update command: ${CMD}" + eval "${CMD}" + RT=$? + log INFO "Finish pre update command. Return value ${RT}." +} + +run_post_update_cmd() { + local CMD RT + CMD=${GANTRY_POST_UPDATE_CMD:-""} + [ -z "${CMD}" ] && return 0 + log INFO "Run post update command: ${CMD}" + eval "${CMD}" + RT=$? + log INFO "Finish post update command. Return value ${RT}." +} + gantry() { local STACK="${1:-gantry}" local START_TIME= @@ -64,6 +84,8 @@ gantry() { local DOCKER_HUB_RATE_USED= local TIME_ELAPSED= + run_pre_update_cmd + log INFO "Starting." gantry_initialize "${STACK}" ACCUMULATED_ERRORS=$((ACCUMULATED_ERRORS + $?)) @@ -95,6 +117,9 @@ gantry() { else log INFO "${MESSAGE}" fi + + run_post_update_cmd + return ${ACCUMULATED_ERRORS} } From c58a0ec50c86a2dee860e369fdf5f52b471cdb33 Mon Sep 17 00:00:00 2001 From: Shizun Ge Date: Wed, 17 Jan 2024 23:26:35 -0800 Subject: [PATCH 06/13] add a test for pre and post update cmd. --- tests/run_all_tests.sh | 1 + tests/test_entrypoint.sh | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/tests/run_all_tests.sh b/tests/run_all_tests.sh index 478ba52..f2f56c7 100755 --- a/tests/run_all_tests.sh +++ b/tests/run_all_tests.sh @@ -102,6 +102,7 @@ main() { test_options_LOG_LEVEL_none \ test_options_UPDATE_OPTIONS \ test_options_CLEANUP_IMAGES_false \ + test_options_PRE_POST_UPDATE_CMD \ " local LOGIN_TESTS="\ test_login_config \ diff --git a/tests/test_entrypoint.sh b/tests/test_entrypoint.sh index 9f457f3..4df7eb7 100755 --- a/tests/test_entrypoint.sh +++ b/tests/test_entrypoint.sh @@ -865,3 +865,41 @@ test_options_CLEANUP_IMAGES_false() { prune_local_test_image "${IMAGE_WITH_TAG}" finalize_test "${FUNCNAME[0]}" } + +test_options_PRE_POST_UPDATE_CMD() { + local IMAGE_WITH_TAG="${1}" + local SERVICE_NAME STDOUT + SERVICE_NAME="gantry-test-$(unique_id)" + + initialize_test "${FUNCNAME[0]}" + build_and_push_test_image "${IMAGE_WITH_TAG}" + start_replicated_service "${SERVICE_NAME}" "${IMAGE_WITH_TAG}" + + export GANTRY_SERVICES_FILTERS="name=${SERVICE_NAME}" + export GANTRY_PRE_UPDATE_CMD="echo \"Pre update\"" + export GANTRY_POST_UPDATE_CMD="echo \"Post update\"" + STDOUT=$(run_gantry "${FUNCNAME[0]}" 2>&1 | tee >(cat 1>&2)) + + expect_message "${STDOUT}" "Pre update" + expect_no_message "${STDOUT}" "${SKIP_UPDATING_SERVICE}.*${SERVICE_NAME}" + expect_message "${STDOUT}" "${SERVICE_NAME}.*${NO_NEW_IMAGE}" + expect_no_message "${STDOUT}" "${SERVICE_NAME}.*${UPDATED}" + expect_no_message "${STDOUT}" "${SERVICE_NAME}.*${NO_UPDATES}" + expect_no_message "${STDOUT}" "${ROLLING_BACK}.*${SERVICE_NAME}" + expect_no_message "${STDOUT}" "${FAILED_TO_ROLLBACK}.*${SERVICE_NAME}" + expect_no_message "${STDOUT}" "${ROLLED_BACK}.*${SERVICE_NAME}" + expect_message "${STDOUT}" "${NO_SERVICES_UPDATED}" + expect_no_message "${STDOUT}" "${NUM_SERVICES_UPDATED}" + expect_no_message "${STDOUT}" "${NUM_SERVICES_UPDATE_FAILED}" + expect_message "${STDOUT}" "${NO_IMAGES_TO_REMOVE}" + expect_no_message "${STDOUT}" "${REMOVING_NUM_IMAGES}" + expect_no_message "${STDOUT}" "${SKIP_REMOVING_IMAGES}" + expect_no_message "${STDOUT}" "${REMOVED_IMAGE}.*${IMAGE_WITH_TAG}" + expect_no_message "${STDOUT}" "${FAILED_TO_REMOVE_IMAGE}.*${IMAGE_WITH_TAG}" + expect_message "${STDOUT}" "Post update" + + stop_service "${SERVICE_NAME}" + prune_local_test_image "${IMAGE_WITH_TAG}" + finalize_test "${FUNCNAME[0]}" +} + From 8e9553fe99b45548d0c9513e47dafab1f0296865 Mon Sep 17 00:00:00 2001 From: Shizun Ge Date: Wed, 17 Jan 2024 23:38:10 -0800 Subject: [PATCH 07/13] update options to PRE_RUN_CMD and POST_RUN_CMD --- src/entrypoint.sh | 30 ++++++++++++++++-------------- tests/lib-gantry-test.sh | 4 ++++ tests/run_all_tests.sh | 2 +- tests/test_entrypoint.sh | 6 +++--- 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/entrypoint.sh b/src/entrypoint.sh index 8a51d4e..990b362 100755 --- a/src/entrypoint.sh +++ b/src/entrypoint.sh @@ -50,24 +50,26 @@ skip_current_node() { return 1 } -run_pre_update_cmd() { - local CMD RT - CMD=${GANTRY_PRE_UPDATE_CMD:-""} +exec_pre_run_cmd() { + local CMD RT LOG + CMD=${GANTRY_PRE_RUN_CMD:-""} [ -z "${CMD}" ] && return 0 - log INFO "Run pre update command: ${CMD}" - eval "${CMD}" + log INFO "Run pre-run command: ${CMD}" + LOG=$(eval "${CMD}") RT=$? - log INFO "Finish pre update command. Return value ${RT}." + log INFO "${LOG}" + log INFO "Finish pre-run command. Return value ${RT}." } -run_post_update_cmd() { - local CMD RT - CMD=${GANTRY_POST_UPDATE_CMD:-""} +exec_post_run_cmd() { + local CMD RT LOG + CMD=${GANTRY_POST_RUN_CMD:-""} [ -z "${CMD}" ] && return 0 - log INFO "Run post update command: ${CMD}" - eval "${CMD}" + log INFO "Run post-run command: ${CMD}" + LOG=$(eval "${CMD}") RT=$? - log INFO "Finish post update command. Return value ${RT}." + log INFO "${LOG}" + log INFO "Finish post-run command. Return value ${RT}." } gantry() { @@ -84,7 +86,7 @@ gantry() { local DOCKER_HUB_RATE_USED= local TIME_ELAPSED= - run_pre_update_cmd + exec_pre_run_cmd log INFO "Starting." gantry_initialize "${STACK}" @@ -118,7 +120,7 @@ gantry() { log INFO "${MESSAGE}" fi - run_post_update_cmd + exec_post_run_cmd return ${ACCUMULATED_ERRORS} } diff --git a/tests/lib-gantry-test.sh b/tests/lib-gantry-test.sh index 6ef7cff..58da6aa 100755 --- a/tests/lib-gantry-test.sh +++ b/tests/lib-gantry-test.sh @@ -38,6 +38,8 @@ initialize_test() { export GANTRY_SERVICES_SELF= export GANTRY_MANIFEST_CMD= export GANTRY_MANIFEST_OPTIONS= + export GANTRY_PRE_RUN_CMD= + export GANTRY_POST_RUN_CMD= export GANTRY_ROLLBACK_ON_FAILURE= export GANTRY_ROLLBACK_OPTIONS= export GANTRY_UPDATE_JOBS= @@ -334,6 +336,8 @@ run_gantry_container() { --env "GANTRY_SERVICES_SELF=${GANTRY_SERVICES_SELF}" \ --env "GANTRY_MANIFEST_CMD=${GANTRY_MANIFEST_CMD}" \ --env "GANTRY_MANIFEST_OPTIONS=${GANTRY_MANIFEST_OPTIONS}" \ + --env "GANTRY_PRE_RUN_CMD=${GANTRY_PRE_RUN_CMD}" \ + --env "GANTRY_POST_RUN_CMD=${GANTRY_POST_RUN_CMD}" \ --env "GANTRY_ROLLBACK_ON_FAILURE=${GANTRY_ROLLBACK_ON_FAILURE}" \ --env "GANTRY_ROLLBACK_OPTIONS=${GANTRY_ROLLBACK_OPTIONS}" \ --env "GANTRY_UPDATE_JOBS=${GANTRY_UPDATE_JOBS}" \ diff --git a/tests/run_all_tests.sh b/tests/run_all_tests.sh index f2f56c7..051a9a9 100755 --- a/tests/run_all_tests.sh +++ b/tests/run_all_tests.sh @@ -102,7 +102,7 @@ main() { test_options_LOG_LEVEL_none \ test_options_UPDATE_OPTIONS \ test_options_CLEANUP_IMAGES_false \ - test_options_PRE_POST_UPDATE_CMD \ + test_options_PRE_POST_RUN_CMD \ " local LOGIN_TESTS="\ test_login_config \ diff --git a/tests/test_entrypoint.sh b/tests/test_entrypoint.sh index 4df7eb7..9b8e66b 100755 --- a/tests/test_entrypoint.sh +++ b/tests/test_entrypoint.sh @@ -866,7 +866,7 @@ test_options_CLEANUP_IMAGES_false() { finalize_test "${FUNCNAME[0]}" } -test_options_PRE_POST_UPDATE_CMD() { +test_options_PRE_POST_RUN_CMD() { local IMAGE_WITH_TAG="${1}" local SERVICE_NAME STDOUT SERVICE_NAME="gantry-test-$(unique_id)" @@ -876,8 +876,8 @@ test_options_PRE_POST_UPDATE_CMD() { start_replicated_service "${SERVICE_NAME}" "${IMAGE_WITH_TAG}" export GANTRY_SERVICES_FILTERS="name=${SERVICE_NAME}" - export GANTRY_PRE_UPDATE_CMD="echo \"Pre update\"" - export GANTRY_POST_UPDATE_CMD="echo \"Post update\"" + export GANTRY_PRE_RUN_CMD="echo \"Pre update\"" + export GANTRY_POST_RUN_CMD="echo \"Post update\"" STDOUT=$(run_gantry "${FUNCNAME[0]}" 2>&1 | tee >(cat 1>&2)) expect_message "${STDOUT}" "Pre update" From 69db69a3195a0f12bbfb0eb7b8b35871830f6189 Mon Sep 17 00:00:00 2001 From: Shizun Ge Date: Wed, 17 Jan 2024 23:53:16 -0800 Subject: [PATCH 08/13] export LOG_LEVEL and NODE_NAME in entrypoint.sh --- src/entrypoint.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/src/entrypoint.sh b/src/entrypoint.sh index 990b362..d3799c4 100755 --- a/src/entrypoint.sh +++ b/src/entrypoint.sh @@ -128,6 +128,7 @@ gantry() { main() { LOG_LEVEL="${GANTRY_LOG_LEVEL:-${LOG_LEVEL}}" NODE_NAME="${GANTRY_NODE_NAME:-${NODE_NAME}}" + export LOG_LEVEL NODE_NAME local SLEEP_SECONDS="${GANTRY_SLEEP_SECONDS:-0}" if ! is_number "${SLEEP_SECONDS}"; then log ERROR "GANTRY_SLEEP_SECONDS must be a number. Got \"${GANTRY_SLEEP_SECONDS}\"." From 1183f72d137b200f4f07469d9c30e13df3c114de Mon Sep 17 00:00:00 2001 From: Shizun Ge Date: Thu, 18 Jan 2024 00:07:14 -0800 Subject: [PATCH 09/13] [readme] update readme about pre/post run cmd. --- README.md | 6 ++++-- tests/lib-gantry-test.sh | 10 +++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a2e334c..4ce8043 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## 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 \ @@ -18,7 +18,7 @@ docker service create \ Or with docker compose, see the [example](examples/docker-compose.yml). -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 @@ -30,6 +30,8 @@ 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_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 | Sleep time between two updates. Set it to 0 to run *Gantry* once and then exit. | | TZ | | Set timezone for time in logs. | diff --git a/tests/lib-gantry-test.sh b/tests/lib-gantry-test.sh index 58da6aa..5528d55 100755 --- a/tests/lib-gantry-test.sh +++ b/tests/lib-gantry-test.sh @@ -22,7 +22,10 @@ initialize_test() { echo "==============================" export GANTRY_LOG_LEVEL="DEBUG" export GANTRY_NODE_NAME= + export GANTRY_POST_RUN_CMD= + export GANTRY_PRE_RUN_CMD= export GANTRY_SLEEP_SECONDS= + export GANTRY_ROLLBACK_ON_FAILURE= export GANTRY_REGISTRY_CONFIG= export GANTRY_REGISTRY_CONFIG_FILE= export GANTRY_REGISTRY_CONFIGS_FILE= @@ -38,9 +41,6 @@ initialize_test() { export GANTRY_SERVICES_SELF= export GANTRY_MANIFEST_CMD= export GANTRY_MANIFEST_OPTIONS= - export GANTRY_PRE_RUN_CMD= - export GANTRY_POST_RUN_CMD= - export GANTRY_ROLLBACK_ON_FAILURE= export GANTRY_ROLLBACK_OPTIONS= export GANTRY_UPDATE_JOBS= export GANTRY_UPDATE_OPTIONS= @@ -320,6 +320,8 @@ run_gantry_container() { --mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \ --env "GANTRY_LOG_LEVEL=${GANTRY_LOG_LEVEL}" \ --env "GANTRY_NODE_NAME=${GANTRY_NODE_NAME}" \ + --env "GANTRY_POST_RUN_CMD=${GANTRY_POST_RUN_CMD}" \ + --env "GANTRY_PRE_RUN_CMD=${GANTRY_PRE_RUN_CMD}" \ --env "GANTRY_SLEEP_SECONDS=${GANTRY_SLEEP_SECONDS}" \ --env "GANTRY_REGISTRY_CONFIG=${GANTRY_REGISTRY_CONFIG}" \ --env "GANTRY_REGISTRY_CONFIG_FILE=${GANTRY_REGISTRY_CONFIG_FILE}" \ @@ -336,8 +338,6 @@ run_gantry_container() { --env "GANTRY_SERVICES_SELF=${GANTRY_SERVICES_SELF}" \ --env "GANTRY_MANIFEST_CMD=${GANTRY_MANIFEST_CMD}" \ --env "GANTRY_MANIFEST_OPTIONS=${GANTRY_MANIFEST_OPTIONS}" \ - --env "GANTRY_PRE_RUN_CMD=${GANTRY_PRE_RUN_CMD}" \ - --env "GANTRY_POST_RUN_CMD=${GANTRY_POST_RUN_CMD}" \ --env "GANTRY_ROLLBACK_ON_FAILURE=${GANTRY_ROLLBACK_ON_FAILURE}" \ --env "GANTRY_ROLLBACK_OPTIONS=${GANTRY_ROLLBACK_OPTIONS}" \ --env "GANTRY_UPDATE_JOBS=${GANTRY_UPDATE_JOBS}" \ From 1f16f9a53a6324437962f2e7ce920d6b8146ecea Mon Sep 17 00:00:00 2001 From: Shizun Ge Date: Thu, 18 Jan 2024 11:41:55 -0800 Subject: [PATCH 10/13] [lib] wait_service_state accepts --running and --complete arguments. --- src/lib-common.sh | 32 +++++++++++++++++++++----------- src/lib-gantry.sh | 2 +- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/lib-common.sh b/src/lib-common.sh index f462deb..2c0fc04 100755 --- a/src/lib-common.sh +++ b/src/lib-common.sh @@ -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 @@ -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 # 2023-06-22T01:20:54.535860111Z @ | log_docker_line() { @@ -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 from "--name " or "--name=" echo "${@}" | tr '\n' ' ' | sed -E 's/.*--name[ =]([^ ]*).*/\1/' @@ -244,12 +249,17 @@ docker_service_task_states() { done } +# Usage: wait_service_state [--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 @@ -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() { diff --git a/src/lib-gantry.sh b/src/lib-gantry.sh index 70e06ea..313e5c1 100755 --- a/src/lib-gantry.sh +++ b/src/lib-gantry.sh @@ -161,7 +161,7 @@ remove_images() { done; log INFO \"Done.\"; " - wait_service_state "${SERVICE_NAME}" "false" "true"; + wait_service_state "${SERVICE_NAME}" --complete; docker_service_logs "${SERVICE_NAME}" docker_service_remove "${SERVICE_NAME}" } From e205d06b3899bf98d167a9623a8e23d88dd4849c Mon Sep 17 00:00:00 2001 From: Shizun Ge Date: Thu, 18 Jan 2024 18:08:37 -0800 Subject: [PATCH 11/13] [examples] add an example for pre and post run command. --- README.md | 2 +- examples/README.md | 10 ++++ examples/{ => cronjob}/docker-compose.yml | 3 +- examples/prune-and-watchtower/README.md | 8 +++ .../prune-and-watchtower/docker-compose.yml | 53 +++++++++++++++++++ 5 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 examples/README.md rename examples/{ => cronjob}/docker-compose.yml (79%) create mode 100644 examples/prune-and-watchtower/README.md create mode 100644 examples/prune-and-watchtower/docker-compose.yml diff --git a/README.md b/README.md index 4ce8043..610fb5e 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ 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` as well as `bash`. diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..bbabf36 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,10 @@ +# Examples + +## [cronjob](./cronjob) + +Run *gantry* at the specific time. + +## [prune-and-watchtower](./prune-and-watchtower) + +Run [`docker system prune`](https://docs.docker.com/engine/reference/commandline/system_prune/) and [*watchtower*](https://github.com/containrrr/watchtower) with *gantry*. + diff --git a/examples/docker-compose.yml b/examples/cronjob/docker-compose.yml similarity index 79% rename from examples/docker-compose.yml rename to examples/cronjob/docker-compose.yml index 4e52cd7..b3ac1a0 100644 --- a/examples/docker-compose.yml +++ b/examples/cronjob/docker-compose.yml @@ -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 diff --git a/examples/prune-and-watchtower/README.md b/examples/prune-and-watchtower/README.md new file mode 100644 index 0000000..9389e98 --- /dev/null +++ b/examples/prune-and-watchtower/README.md @@ -0,0 +1,8 @@ +# prune and watchtower + +This example run `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. + diff --git a/examples/prune-and-watchtower/docker-compose.yml b/examples/prune-and-watchtower/docker-compose.yml new file mode 100644 index 0000000..1c6a8f0 --- /dev/null +++ b/examples/prune-and-watchtower/docker-compose.yml @@ -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 From d678ce04ff2602a33ee998fc7aa1c02dee979d64 Mon Sep 17 00:00:00 2001 From: Shizun Ge Date: Thu, 18 Jan 2024 18:37:50 -0800 Subject: [PATCH 12/13] [entrypoint] exclude the time spent on updating from sleep seconds. --- README.md | 2 +- src/entrypoint.sh | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 610fb5e..3ee24cc 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ You can configure the most behaviors of *Gantry* via environment variables. | GANTRY_NODE_NAME | | Add node name to logs. | | 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 | Sleep time between two updates. Set it to 0 to run *Gantry* once and then exit. | +| 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 diff --git a/src/entrypoint.sh b/src/entrypoint.sh index d3799c4..0bca75b 100755 --- a/src/entrypoint.sh +++ b/src/entrypoint.sh @@ -129,22 +129,26 @@ main() { LOG_LEVEL="${GANTRY_LOG_LEVEL:-${LOG_LEVEL}}" NODE_NAME="${GANTRY_NODE_NAME:-${NODE_NAME}}" export LOG_LEVEL NODE_NAME - local SLEEP_SECONDS="${GANTRY_SLEEP_SECONDS:-0}" - if ! is_number "${SLEEP_SECONDS}"; then + 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} } From 43c12944b27fe13f940317980aa0c062e55e305b Mon Sep 17 00:00:00 2001 From: Shizun Ge Date: Thu, 18 Jan 2024 18:54:47 -0800 Subject: [PATCH 13/13] [examples] add more links to examples --- docs/faq.md | 4 ++-- docs/migration.md | 2 ++ examples/README.md | 2 +- examples/cronjob/README.md | 4 ++++ examples/prune-and-watchtower/README.md | 2 +- 5 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 examples/cronjob/README.md diff --git a/docs/faq.md b/docs/faq.md index 19e1887..1f1a721 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -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? @@ -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? diff --git a/docs/migration.md b/docs/migration.md index d09536a..6b35088 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -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 | diff --git a/examples/README.md b/examples/README.md index bbabf36..9f21e18 100644 --- a/examples/README.md +++ b/examples/README.md @@ -6,5 +6,5 @@ Run *gantry* at the specific time. ## [prune-and-watchtower](./prune-and-watchtower) -Run [`docker system prune`](https://docs.docker.com/engine/reference/commandline/system_prune/) and [*watchtower*](https://github.com/containrrr/watchtower) with *gantry*. +Remove unused containers, networks and images. Update standalone docker containers. Update docker swarm services. diff --git a/examples/cronjob/README.md b/examples/cronjob/README.md new file mode 100644 index 0000000..3ef2def --- /dev/null +++ b/examples/cronjob/README.md @@ -0,0 +1,4 @@ +# cronjob + +Use [*swarm-cronjob*](https://github.com/crazy-max/swarm-cronjob) to launch *gantry* at a given time. + diff --git a/examples/prune-and-watchtower/README.md b/examples/prune-and-watchtower/README.md index 9389e98..3996863 100644 --- a/examples/prune-and-watchtower/README.md +++ b/examples/prune-and-watchtower/README.md @@ -1,6 +1,6 @@ # prune and watchtower -This example run `docker system prune` and *watchtower* before updating docker swarm services. +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.