From 446906c16bc58d7470b31812636a0c5295532d50 Mon Sep 17 00:00:00 2001 From: Shizun Ge Date: Sun, 25 Jun 2023 20:12:53 -0700 Subject: [PATCH] fix shellcheck problems. fix error getting config name from a service --- .github/workflows/shellcheck.yml | 24 +++ .shellcheckrc | 30 ++++ src/docker_hub_rate.sh | 20 ++- src/entrypoint.sh | 44 +++-- src/lib-common.sh | 192 +++++++++++---------- src/lib-gantry.sh | 278 +++++++++++++++++-------------- src/notification.sh | 6 +- 7 files changed, 348 insertions(+), 246 deletions(-) create mode 100644 .github/workflows/shellcheck.yml create mode 100644 .shellcheckrc diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml new file mode 100644 index 0000000..d266622 --- /dev/null +++ b/.github/workflows/shellcheck.yml @@ -0,0 +1,24 @@ +name: On push to main + +on: + push: + paths: + - 'src/*' + pull_request: + branches: + - 'main' + +jobs: + shellcheck: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v3 + - name: Install shellcheck + run: sudo apt-get install -y bash shellcheck + - name: Analyse with shellcheck + run: | + cd src + shellcheck -V + shellcheck * + diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 0000000..c7e077b --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1,30 @@ +# .shellcheckrc + +# https://github.com/koalaman/shellcheck/wiki/Optional +enable=add-default-case +enable=avoid-nullary-conditions +# enable=check-unassigned-uppercase +enable=deprecate-which +enable=require-variable-braces + +# SC2039: In POSIX sh, 'local' is undefined. +disable=SC2039 + +# TODO: Try to fix and enable SC3001 +# SC3001 (warning): In POSIX sh, process substitution is undefined. +disable=SC3001 + +# SC3037 (warning): In POSIX sh, echo flags are undefined. +disable=SC3037 + +# SC3043 (warning): In POSIX sh, 'local' is undefined. +disable=SC3043 + +# SC3046 (warning): In POSIX sh, 'source' in place of '.' is undefined. +disable=SC3046 + +# SC3051 (warning): In POSIX sh, 'source' in place of '.' is undefined. +disable=SC3051 + +# SC3057 (warning): In POSIX sh, string indexing is undefined. +disable=SC3057 diff --git a/src/docker_hub_rate.sh b/src/docker_hub_rate.sh index cc79e22..ffd2650 100644 --- a/src/docker_hub_rate.sh +++ b/src/docker_hub_rate.sh @@ -16,16 +16,19 @@ # docker_hub_rate() { - local IMAGE=${1:-ratelimitpreview/test} - local RESPONSE=$(wget -qO- "https://auth.docker.io/token?service=registry.docker.io&scope=repository:${IMAGE}:pull") - [ $? -ne 0 ] && echo "[GET TOKEN RESPONSE ERROR]" && return 1 - local TOKEN=$(echo ${RESPONSE} | sed 's/.*"token":"\([^"]*\).*/\1/') + local IMAGE="${1:-ratelimitpreview/test}" + local RESPONSE= + if ! RESPONSE=$(wget -qO- "https://auth.docker.io/token?service=registry.docker.io&scope=repository:${IMAGE}:pull"); then + echo "[GET TOKEN RESPONSE ERROR]" + return 1 + fi + local TOKEN= + TOKEN=$(echo "${RESPONSE}" | sed 's/.*"token":"\([^"]*\).*/\1/') [ -z "${TOKEN}" ] && echo "[GET TOKEN ERROR]" && return 1 local HEADER="Authorization: Bearer ${TOKEN}" local URL="https://registry-1.docker.io/v2/${IMAGE}/manifests/latest" # adding --spider implies that you want to send a HEAD request (as opposed to GET or POST). - RESPONSE=$(wget -qS --spider --header="${HEADER}" -O /dev/null "${URL}" 2>&1) - if [ $? -ne 0 ]; then + if ! RESPONSE=$(wget -qS --spider --header="${HEADER}" -O /dev/null "${URL}" 2>&1); then if echo "${RESPONSE}" | grep -q "Too Many Requests" ; then echo "0" return 0 @@ -33,7 +36,8 @@ docker_hub_rate() { echo "[GET RATE RESPONSE ERROR]" return 1 fi - local RATE=$(echo ${RESPONSE} | sed -n 's/.*ratelimit-remaining: \([0-9]*\).*/\1/p' ) + local RATE= + RATE=$(echo "${RESPONSE}" | sed -n 's/.*ratelimit-remaining: \([0-9]*\).*/\1/p' ) [ -z "${RATE}" ] && echo "[GET RATE ERROR]" && return 1 - echo ${RATE} + echo "${RATE}" } \ No newline at end of file diff --git a/src/entrypoint.sh b/src/entrypoint.sh index 72886c3..2835140 100755 --- a/src/entrypoint.sh +++ b/src/entrypoint.sh @@ -20,7 +20,8 @@ source ./lib-common.sh source ./lib-gantry.sh skip_current_node() { - local SELF_ID=$(docker node inspect self --format {{.Description.Hostname}} 2>/dev/null); + local SELF_ID= + SELF_ID=$(docker node inspect self --format "{{.Description.Hostname}}" 2>/dev/null); if [ -z "${SELF_ID}" ]; then log WARN "Skip because the current node is not a swarm manager."; return 0 @@ -30,63 +31,72 @@ skip_current_node() { } gantry() { - local STACK=${1:-gantry} - local START_TIME=$(date +%s) + local STACK="${1:-gantry}" + local START_TIME= + START_TIME=$(date +%s) if skip_current_node ; then return 0 fi local ACCUMULATED_ERRORS=0 + local DOCKER_HUB_RATE_BEFORE= + local DOCKER_HUB_RATE_AFTER= + local DOCKER_HUB_RATE_USED= + local TIME_ELAPSED= log INFO "Starting." gantry_initialize "${STACK}" ACCUMULATED_ERRORS=$((ACCUMULATED_ERRORS + $?)) - local DOCKER_HUB_RATE_BEFORE=$(docker_hub_rate) + # SC2119: Use docker_hub_rate "$@" if function's $1 should mean script's $1. + # shellcheck disable=SC2119 + DOCKER_HUB_RATE_BEFORE=$(docker_hub_rate) ACCUMULATED_ERRORS=$((ACCUMULATED_ERRORS + $?)) log INFO "Before updating, Docker Hub rate remains ${DOCKER_HUB_RATE_BEFORE}." log INFO "Starting updating." - gantry_update_services_list $(gantry_get_services_list) + gantry_update_services_list "$(gantry_get_services_list)" ACCUMULATED_ERRORS=$((ACCUMULATED_ERRORS + $?)) - local DOCKER_HUB_RATE_AFTER=$(docker_hub_rate) + # SC2119: Use docker_hub_rate "$@" if function's $1 should mean script's $1. + # shellcheck disable=SC2119 + DOCKER_HUB_RATE_AFTER=$(docker_hub_rate) ACCUMULATED_ERRORS=$((ACCUMULATED_ERRORS + $?)) - local DOCKER_HUB_RATE_USED=$(difference_between "${DOCKER_HUB_RATE_BEFORE}" "${DOCKER_HUB_RATE_AFTER}") + DOCKER_HUB_RATE_USED=$(difference_between "${DOCKER_HUB_RATE_BEFORE}" "${DOCKER_HUB_RATE_AFTER}") log INFO "After updating, Docker Hub rate remains ${DOCKER_HUB_RATE_AFTER}. Used rate ${DOCKER_HUB_RATE_USED}." gantry_finalize "${STACK}"; ACCUMULATED_ERRORS=$((ACCUMULATED_ERRORS + $?)) - local TIME_ELAPSED=$(time_elapsed_since ${START_TIME}) + TIME_ELAPSED=$(time_elapsed_since "${START_TIME}") local MESSAGE="Done. Use ${TIME_ELAPSED}. ${ACCUMULATED_ERRORS} errors." if [ ${ACCUMULATED_ERRORS} -gt 0 ]; then - log WARN ${MESSAGE} + log WARN "${MESSAGE}" else - log INFO ${MESSAGE} + log INFO "${MESSAGE}" fi 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} + 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 log ERROR "GANTRY_SLEEP_SECONDS must be a number. Got \"${GANTRY_SLEEP_SECONDS}\"." return 1; fi - local STACK=${1:-gantry} + local STACK="${1:-gantry}" local RETURN_VALUE=0 while true; do LOG_SCOPE=${STACK} - gantry ${@} + gantry "${@}" RETURN_VALUE=$? [ "${SLEEP_SECONDS}" -le 0 ] && break; log INFO "Sleeping ${SLEEP_SECONDS} seconds before next update." - sleep ${SLEEP_SECONDS} + sleep "${SLEEP_SECONDS}" done return ${RETURN_VALUE} } -main ${@} +main "${@}" diff --git a/src/lib-common.sh b/src/lib-common.sh index eb5b6fc..5c2abef 100755 --- a/src/lib-common.sh +++ b/src/lib-common.sh @@ -19,27 +19,31 @@ # return 0 if LEVEL is supported. # return 1 if LEVLE is unsupported. log_level() { - local LEVEL=${1}; - [ -z "${LEVEL}" ] && echo $(log_level "INFO") && return 1; + local LEVEL="${1}"; + [ -z "${LEVEL}" ] && log_level "INFO" && return 1; [ "${LEVEL}" = "DEBUG" ] && echo 0 && return 0; [ "${LEVEL}" = "INFO" ] && echo 1 && return 0; [ "${LEVEL}" = "WARN" ] && echo 2 && return 0; [ "${LEVEL}" = "ERROR" ] && echo 3 && return 0; [ "${LEVEL}" = "NONE" ] && echo 4 && return 0; - echo $(log_level "NONE"); + log_level "NONE"; return 1; } log_formatter() { - local LOG_LEVEL=${LOG_LEVEL} - local LEVEL=${1}; shift; - [ $(log_level ${LEVEL}) -lt $(log_level ${LOG_LEVEL}) ] && return 0; - local TIME=${1}; shift; - local LOCATION=${1}; shift; - local SCOPE=${1}; shift; - local LOCATION_STR=$(if [ -n "${LOCATION}" ]; then echo "[${LOCATION}]"; else echo ""; fi); - local SCOPE_STR=$(if [ -n "${SCOPE}" ]; then echo "${SCOPE}: "; else echo ""; fi); - local MESSAGE="[${TIME}]${LOCATION_STR}[${LEVEL}] ${SCOPE_STR}$@"; + local LOG_LEVEL="${LOG_LEVEL}" + local LEVEL="${1}"; shift; + [ "$(log_level "${LEVEL}")" -lt "$(log_level "${LOG_LEVEL}")" ] && return 0; + local TIME="${1}"; shift; + local LOCATION="${1}"; shift; + local SCOPE="${1}"; shift; + local LOCATION_STR= + local SCOPE_STR= + local MESSAGE_STR= + LOCATION_STR=$(if [ -n "${LOCATION}" ]; then echo "[${LOCATION}]"; else echo ""; fi); + SCOPE_STR=$(if [ -n "${SCOPE}" ]; then echo "${SCOPE}: "; else echo ""; fi); + MESSAGE_STR=$(echo "${*}" | tr '\n' ' ') + local MESSAGE="[${TIME}]${LOCATION_STR}[${LEVEL}] ${SCOPE_STR}${MESSAGE_STR}"; if [ "${LEVEL}" = "ERROR" ]; then echo "${MESSAGE}" >&2; else @@ -50,7 +54,7 @@ log_formatter() { # We want to print an empty line for log without an argument. Thus we do not run the following check. # [ -z "${1}" ] && return 0 log() { - local NODE_NAME=${NODE_NAME} + local NODE_NAME="${NODE_NAME}" local LEVEL="INFO"; if log_level "${1}" >/dev/null; then LEVEL="${1}"; @@ -63,17 +67,18 @@ log() { # 2023-06-22T01:20:54.535860111Z @ | log_docker_line() { local LEVEL="INFO"; - local TIME_SEC=$(echo ${@} | cut -d ' ' -f 1 | cut -d '.' -f 1); + local TIME_SEC TIMEZONE TZH TZM TIME SCOPE NODE MESSAGE FIRST_WORD + TIME_SEC=$(echo "${@}" | cut -d ' ' -f 1 | cut -d '.' -f 1); # To match the timezone in $(date -Iseconds) (ISO-8601) # busybox date does not support +%:z which outputs "-07:00". %z outputs "-0700". - local TIMEZONE=$(date +%z); - local TZH=${TIMEZONE%??}; - local TZM=${TIMEZONE#"${TZH}"}; - local TIME="${TIME_SEC}${TZH}:${TZM}" - local SCOPE=$(echo ${@} | cut -d ' ' -f 2 | cut -d '@' -f 1); - local NODE=$(echo ${@} | cut -d ' ' -f 2 | cut -d '@' -f 2); - local MESSAGE=$(echo $(echo ${@} | cut -d '|' -f 2-)); - local FIRST_WORD=$(echo "${MESSAGE}" | cut -d ' ' -f 1); + TIMEZONE=$(date +%z); + TZH=${TIMEZONE%??}; + TZM=${TIMEZONE#"${TZH}"}; + TIME="${TIME_SEC}${TZH}:${TZM}" + SCOPE=$(echo "${@}" | cut -d ' ' -f 2 | cut -d '@' -f 1); + NODE=$(echo "${@}" | cut -d ' ' -f 2 | cut -d '@' -f 2); + MESSAGE=$(echo "${@}" | cut -d '|' -f 2-); + FIRST_WORD=$(echo "${MESSAGE}" | cut -d ' ' -f 1); if log_level "${FIRST_WORD}" >/dev/null; then LEVEL=${FIRST_WORD}; MESSAGE=$(echo "${MESSAGE}" | cut -d ' ' -f 2-); @@ -83,10 +88,10 @@ log_docker_line() { # Usage: echo "${LOGS}" | log_lines INFO log_lines() { - local LEVEL=${1}; + local LEVEL="${1}"; while read -r LINE; do [ -z "${LINE}" ] && continue; - log ${LEVEL} ${LINE}; + log "${LEVEL}" "${LINE}"; done } @@ -95,10 +100,10 @@ is_number() { } difference_between() { - local NUM0=${1} - local NUM1=${2} + local NUM0="${1}" + local NUM1="${2}" if is_number "${NUM0}" && is_number "${NUM1}"; then - if [ ${NUM0} -gt ${NUM1} ]; then + if [ "${NUM0}" -gt "${NUM1}" ]; then echo "$((NUM0 - NUM1))" else echo "$((NUM1 - NUM0))" @@ -110,42 +115,41 @@ difference_between() { } time_elapsed_between() { - local TIME0=${1} - local TIME1=${2} + local TIME0="${1}" + local TIME1="${2}" local SECONDS_ELAPSED= if ! SECONDS_ELAPSED=$(difference_between "${TIME0}" "${TIME1}"); then echo "NaN" return 1 fi - local TIME_ELAPSED=$(date -u -d @${SECONDS_ELAPSED} +'%-Mm %-Ss') - echo "${TIME_ELAPSED}" + date -u -d "@${SECONDS_ELAPSED}" +'%-Mm %-Ss' } time_elapsed_since() { - local START_TIME=${1} + local START_TIME="${1}" time_elapsed_between "$(date +%s)" "${START_TIME}" } # For a givne variable name , try to read content of _FILE if file exists. # otherwise echo the content of . read_config() { - local CONFIG_NAME=${1} + local CONFIG_NAME="${1}" [ -z "${CONFIG_NAME}" ] && return 1 local CONFIG_FILE_NAME="${CONFIG_NAME}_FILE" eval "local CONFIG_FILE=\${${CONFIG_FILE_NAME}}" if [ -r "${CONFIG_FILE}" ]; then - cat ${CONFIG_FILE} + cat "${CONFIG_FILE}" return $? elif [ -n "${CONFIG_FILE}" ]; then echo "Failed to read ${CONFIG_FILE}" >&2 return 1 fi eval "local CONFIG=\${${CONFIG_NAME}}" - echo ${CONFIG} + echo "${CONFIG}" } is_true() { - local CONFIG=${1} + local CONFIG="${1}" CONFIG=$(echo "${CONFIG}" | cut -d ' ' -f 1) echo "${CONFIG}" | grep -q -i "true" } @@ -155,7 +159,7 @@ swarm_network_arguments() { echo "" return 0 fi - NETWORK_NAME=$(docker network ls --filter name=${NETWORK_NAME} --format {{.Name}}) + NETWORK_NAME=$(docker network ls --filter "name=${NETWORK_NAME}" --format '{{.Name}}') if [ -z "${NETWORK_NAME}" ]; then echo "" return 0 @@ -170,7 +174,7 @@ swarm_network_arguments() { get_docker_command_name_arg() { # get from "--name " or "--name=" - echo ${@} | sed -E 's/.*-name[ =]([^ ]*).*/\1/' + echo "${@}" | tr '\n' ' ' | sed -E 's/.*--name[ =]([^ ]*).*/\1/' } get_docker_command_detach() { @@ -182,64 +186,67 @@ get_docker_command_detach() { } docker_service_logs () { - local SERVICE_NAME=${1} - local LOGS=$(docker service logs --timestamps --no-task-ids ${SERVICE_NAME} 2>&1) - if [ $? -ne 0 ]; then - log ERROR "Failed to obtain logs of service ${SERVICE_NAME}. $(echo ${LOGS})" + local SERVICE_NAME="${1}" + local LOGS= + if ! LOGS=$(docker service logs --timestamps --no-task-ids "${SERVICE_NAME}" 2>&1); then + log ERROR "Failed to obtain logs of service ${SERVICE_NAME}. ${LOGS})" return 1 fi echo "${LOGS}" | while read -r LINE; do - log_docker_line ${LINE} + log_docker_line "${LINE}" done } docker_service_logs_follow() { - local SERVICE_NAME=${1} - docker service logs --timestamps --no-task-ids --follow ${SERVICE_NAME} 2>&1 | + local SERVICE_NAME="${1}" + docker service logs --timestamps --no-task-ids --follow "${SERVICE_NAME}" 2>&1 | while read -r LINE; do - log_docker_line ${LINE} + log_docker_line "${LINE}" done } docker_service_task_states() { - local SERVICE_NAME=${1} + local SERVICE_NAME="${1}" # We won't get the return value of the command via $? if we use "local STATES=$(command)". local STATES= - STATES=$(docker service ps --no-trunc --format "[{{.Name}}][{{.Node}}] {{.CurrentState}} {{.Error}}" ${SERVICE_NAME} 2>&1) - if [ $? -ne 0 ]; then + if ! STATES=$(docker service ps --no-trunc --format '[{{.Name}}][{{.Node}}] {{.CurrentState}} {{.Error}}' "${SERVICE_NAME}" 2>&1); then echo "${STATES}" >&2 return 1 fi + local NAME_LIST= echo "${STATES}" | while read -r LINE; do - local NAME=$(echo ${LINE} | cut -d ']' -f 1 | cut -d '[' -f 2) - local NODE_STATE_AND_ERROR=$(echo ${LINE} | cut -d ']' -f 2-) + local NAME= + local NODE_STATE_AND_ERROR= + NAME=$(echo "${LINE}" | cut -d ']' -f 1 | cut -d '[' -f 2) + NODE_STATE_AND_ERROR=$(echo "${LINE}" | cut -d ']' -f 2-) # We assume that the first State of each task is the latest one that we want to report. - if ! $(echo "${NAME_LIST}" | grep -q "${NAME}"); then + if ! echo "${NAME_LIST}" | grep -q "${NAME}"; then echo "${NODE_STATE_AND_ERROR}" fi - local NAME_LIST=$(echo -e "${NAME_LIST}\n${NAME}" | sort | uniq) + NAME_LIST=$(echo -e "${NAME_LIST}\n${NAME}" | sort | uniq) done } 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 STATES=$(docker_service_task_states ${SERVICE_NAME} 2>&1) - while ${WAIT_RUNNING} || ${WAIT_COMPLETE} ; do + 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 STATES= + STATES=$(docker_service_task_states "${SERVICE_NAME}" 2>&1) + while is_true "${WAIT_RUNNING}" || is_true "${WAIT_COMPLETE}" ; do local NUM_LINES=0 local NUM_RUNS=0 local NUM_DONES=0 local NUM_FAILS=0 while read -r LINE; do [ -z "${LINE}" ] && continue; - NUM_LINES=$((NUM_LINES+1)) - echo "${LINE}" | grep -q "Running" && NUM_RUNS=$((NUM_RUNS+1)) - echo "${LINE}" | grep -q "Complete" && NUM_DONES=$((NUM_DONES+1)) - echo "${LINE}" | grep -q "Failed" && NUM_FAILS=$((NUM_FAILS+1)) + NUM_LINES=$((NUM_LINES+1)); + echo "${LINE}" | grep -q "Running" && NUM_RUNS=$((NUM_RUNS+1)); + echo "${LINE}" | grep -q "Complete" && NUM_DONES=$((NUM_DONES+1)); + echo "${LINE}" | grep -q "Failed" && NUM_FAILS=$((NUM_FAILS+1)); done < <(echo "${STATES}") if [ ${NUM_LINES} -gt 0 ]; then if ${WAIT_RUNNING} && [ ${NUM_RUNS} -eq ${NUM_LINES} ]; then @@ -250,33 +257,34 @@ wait_service_state() { fi if ${WAIT_COMPLETE} && [ ${NUM_FAILS} -gt 0 ]; then # Get return value of the task from the string "task: non-zero exit (1)". - local TASK_STATE=$(echo "${STATES}" | grep "Failed") - local TASK_RETURN_VALUE=$(echo "${TASK_STATE}" | sed -n 's/.*task: non-zero exit (\([0-9]\+\)).*/\1/p') + local TASK_STATE= + local TASK_RETURN_VALUE= + TASK_STATE=$(echo "${STATES}" | grep "Failed") + TASK_RETURN_VALUE=$(echo "${TASK_STATE}" | sed -n 's/.*task: non-zero exit (\([0-9]\+\)).*/\1/p') # Get the first error code. - RETURN_VALUE=$(echo ${TASK_RETURN_VALUE:-1} | cut -d ' ' -f 1) + RETURN_VALUE=$(echo "${TASK_RETURN_VALUE:-1}" | cut -d ' ' -f 1) break fi fi - sleep ${SLEEP_SECONDS} - STATES=$(docker_service_task_states ${SERVICE_NAME} 2>&1) - if [ $? -ne 0 ]; then - log ERROR "Failed to obtain task states of service ${SERVICE_NAME}: $(echo ${STATES})" + sleep "${SLEEP_SECONDS}" + if ! STATES=$(docker_service_task_states "${SERVICE_NAME}" 2>&1); then + log ERROR "Failed to obtain task states of service ${SERVICE_NAME}: ${STATES})" return 1 fi done echo "${STATES}" | while read -r LINE; do - log INFO "Service ${SERVICE_NAME}: $(echo ${LINE})." + log INFO "Service ${SERVICE_NAME}: ${LINE})." done - return ${RETURN_VALUE} + return "${RETURN_VALUE}" } docker_service_remove() { - local SERVICE_NAME=${1} - if ! docker service inspect --format {{.JobStatus}} ${SERVICE_NAME} >/dev/null 2>&1; then + local SERVICE_NAME="${1}" + if ! docker service inspect --format '{{.JobStatus}}' "${SERVICE_NAME}" >/dev/null 2>&1; then return 0 fi log INFO "Removing service ${SERVICE_NAME}." - docker service rm ${SERVICE_NAME} >/dev/null + docker service rm "${SERVICE_NAME}" >/dev/null local RETURN_VALUE=$? log INFO "Removed service ${SERVICE_NAME}." return ${RETURN_VALUE} @@ -286,51 +294,51 @@ docker_service_remove() { # Docker will try to restart the failed tasks. # We do not check the converge of the service. It must be used togther with wait_service_state. docker_global_job() { - local SERVICE_NAME=$(get_docker_command_name_arg ${@}) - local IS_DETACH=$(get_docker_command_detach ${@}) - local WAIT_RUNNING=${IS_DETACH} - local WAIT_COMPLETE="false" + local SERVICE_NAME= + SERVICE_NAME=$(get_docker_command_name_arg "${@}") log INFO "Starting service ${SERVICE_NAME}." docker service create \ --mode global-job \ "${@}" >/dev/null - local RETURN_VALUE=$? - return ${RETURN_VALUE} } # A job could fail when using docker_replicated_job. docker_replicated_job() { - local SERVICE_NAME=$(get_docker_command_name_arg ${@}) - local IS_DETACH=$(get_docker_command_detach ${@}) + local SERVICE_NAME= + local IS_DETACH= + SERVICE_NAME=$(get_docker_command_name_arg "${@}") + 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=$(if ${IS_DETACH}; then echo "false"; else echo "true"; fi) + 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} + wait_service_state "${SERVICE_NAME}" "${WAIT_RUNNING}" "${WAIT_COMPLETE}" "${RETURN_VALUE}" } container_status() { - local CNAME=${1} - docker container inspect --format '{{.State.Status}}' ${CNAME} 2>/dev/null + local CNAME="${1}" + docker container inspect --format '{{.State.Status}}' "${CNAME}" 2>/dev/null } docker_remove() { - local CNAME=${1} - local STATUS=$(container_status ${CNAME}) + local CNAME="${1}" + local STATUS= + STATUS=$(container_status "${CNAME}") if [ -z "${STATUS}" ]; then return 0 fi log INFO "Removing container ${CNAME}." if [ "${STATUS}" = "running" ]; then - docker stop ${CNAME} >/dev/null 2>/dev/null + docker stop "${CNAME}" >/dev/null 2>/dev/null fi - docker rm ${CNAME} >/dev/null + docker rm "${CNAME}" >/dev/null } docker_run() { diff --git a/src/lib-gantry.sh b/src/lib-gantry.sh index a02dd71..6f1f9cf 100755 --- a/src/lib-gantry.sh +++ b/src/lib-gantry.sh @@ -16,10 +16,10 @@ # login_registry() { - local USER=${1} - local PASSWORD=${2} - local HOST=${3} - local CONFIG=${4} + local USER="${1}" + local PASSWORD="${2}" + local HOST="${3}" + local CONFIG="${4}" [ -z "${USER}" ] && log ERROR "USER is empty." && return 1 [ -z "${PASSWORD}" ] && log ERROR "PASSWORD is empty." && return 1 local DOCKER_CONFIG= @@ -32,29 +32,31 @@ login_registry() { DOCKER_CONFIG="--config ${CONFIG}" CONFIG_MESSAGE="${CONFIG_MESSAGE} for config ${CONFIG}" fi - local LOGIN_MSG - LOGIN_MSG=$(echo "${PASSWORD}" | docker ${DOCKER_CONFIG} login --username="${USER}" --password-stdin "${HOST}" 2>&1) - if [ $? -ne 0 ]; then - log ERROR "Failed to login to registry${CONFIG_MESSAGE}. $(echo ${LOGIN_MSG})" + local LOGIN_MSG= + # SC2086: Double quote to prevent globbing and word splitting. + # shellcheck disable=SC2086 + if ! LOGIN_MSG=$(echo "${PASSWORD}" | docker ${DOCKER_CONFIG} login --username="${USER}" --password-stdin "${HOST}" 2>&1); then + log ERROR "Failed to login to registry${CONFIG_MESSAGE}. ${LOGIN_MSG}" else - log INFO "Logged into registry${CONFIG_MESSAGE}. $(echo ${LOGIN_MSG})" + log INFO "Logged into registry${CONFIG_MESSAGE}. ${LOGIN_MSG}" fi } authenticate_to_registries() { - local CONFIG= - local CONFIGS_FILE=${GANTRY_REGISTRY_CONFIGS_FILE} - local HOST= - local PASSWORD= - local USER= - CONFIG=$(read_config GANTRY_REGISTRY_CONFIG 2>&1) - [ $? -ne 0 ] && log ERROR "Failed to set CONFIG: ${CONFIG}" && return 1 - HOST=$(read_config GANTRY_REGISTRY_HOST 2>&1) - [ $? -ne 0 ] && log ERROR "Failed to set HOST: ${HOST}" && return 1 - PASSWORD=$(read_config GANTRY_REGISTRY_PASSWORD 2>&1) - [ $? -ne 0 ] && log ERROR "Failed to set PASSWORD: ${PASSWORD}" && return 1 - USER=$(read_config GANTRY_REGISTRY_USER 2>&1) - [ $? -ne 0 ] && log ERROR "Failed to set USER: ${USER}" && return 1 + local CONFIGS_FILE="${GANTRY_REGISTRY_CONFIGS_FILE}" + local CONFIG HOST PASSWORD USER + if ! CONFIG=$(read_config GANTRY_REGISTRY_CONFIG 2>&1); then + log ERROR "Failed to set CONFIG: ${CONFIG}" && return 1; + fi + if ! HOST=$(read_config GANTRY_REGISTRY_HOST 2>&1); then + log ERROR "Failed to set HOST: ${HOST}" && return 1; + fi + if ! PASSWORD=$(read_config GANTRY_REGISTRY_PASSWORD 2>&1); then + log ERROR "Failed to set PASSWORD: ${PASSWORD}" && return 1; + fi + if ! USER=$(read_config GANTRY_REGISTRY_USER 2>&1); then + log ERROR "Failed to set USER: ${USER}" && return 1; + fi if [ -n "${USER}" ]; then login_registry "${USER}" "${PASSWORD}" "${HOST}" "${CONFIG}" fi @@ -62,16 +64,16 @@ authenticate_to_registries() { [ ! -r "${CONFIGS_FILE}" ] && log ERROR "Failed to read ${CONFIGS_FILE}." && return 1 local LINE= while read -r LINE; do - LINE=$(echo ${LINE}) # skip comments [ -z "${LINE}" ] && continue [ "${LINE:0:1}" = "#" ] && continue - LINE=$(echo ${LINE} | tr '\t' ' ') - local CONFIG=$(echo ${LINE} | cut -d ' ' -f 1) - local HOST=$(echo ${LINE} | cut -d ' ' -f 2) - local USER=$(echo ${LINE} | cut -d ' ' -f 3) - local PASSWORD=$(echo ${LINE} | cut -d ' ' -f 4) - local OTHERS=$(echo ${LINE} | cut -d ' ' -f 5-) + LINE=$(echo "${LINE}" | tr '\t' ' ') + local OTHERS= + CONFIG=$(echo "${LINE}" | cut -d ' ' -f 1) + HOST=$(echo "${LINE}" | cut -d ' ' -f 2) + USER=$(echo "${LINE}" | cut -d ' ' -f 3) + PASSWORD=$(echo "${LINE}" | cut -d ' ' -f 4) + OTHERS=$(echo "${LINE}" | cut -d ' ' -f 5-) if [ -n "${OTHERS}" ] || [ -z "${CONFIG}" ] || \ [ -z "${HOST}" ] || [ -z "${USER}" ] || [ -z "${PASSWORD}" ]; then log ERROR "${CONFIGS_FILE} format error. A line should contains only \" \". Got \"${LINE}\"." @@ -91,7 +93,7 @@ send_notification() { } add_image_to_remove() { - local IMAGE=${1} + local IMAGE="${1}" if [ -z "${GLOBAL_IMAGES_TO_REMOVE}" ]; then GLOBAL_IMAGES_TO_REMOVE=${IMAGE} return 0 @@ -100,22 +102,22 @@ add_image_to_remove() { } remove_images() { - local CLEANUP_IMAGES=${GANTRY_CLEANUP_IMAGES:-"true"} - if ! is_true ${CLEANUP_IMAGES}; then + local CLEANUP_IMAGES="${GANTRY_CLEANUP_IMAGES:-"true"}" + if ! is_true "${CLEANUP_IMAGES}"; then log INFO "Skip removing images." return 0 fi - local SERVICE_NAME=${1:-"docker-image-remover"} - docker_service_remove ${SERVICE_NAME} + local SERVICE_NAME="${1:-"docker-image-remover"}" + docker_service_remove "${SERVICE_NAME}" if [ -z "${GLOBAL_IMAGES_TO_REMOVE}" ]; then log INFO "No images to remove." return 0 fi log INFO "Try to remove the following images:" - for I in $(echo ${GLOBAL_IMAGES_TO_REMOVE} | tr '\n' ' '); do + for I in $(echo "${GLOBAL_IMAGES_TO_REMOVE}" | tr '\n' ' '); do log INFO "- ${I}" done - docker_global_job --name ${SERVICE_NAME} \ + docker_global_job --name "${SERVICE_NAME}" \ --restart-condition on-failure \ --restart-max-attempts 1 \ --mount type=bind,source=/var/run/docker.sock,destination=/var/run/docker.sock \ @@ -129,12 +131,12 @@ remove_images() { remove_container() { local IMAGE=\${1}; local STATUS=\${2}; - if ! CIDS=\$(docker container ls --all --filter ancestor=\${IMAGE} --filter status=\${STATUS} --format {{.ID}} 2>&1); then + if ! CIDS=\$(docker container ls --all --filter \"ancestor=\${IMAGE}\" --filter \"status=\${STATUS}\" --format '{{.ID}}' 2>&1); then log ERROR \"Failed to list \${STATUS} containers with image \${IMAGE}. \$(echo \${CIDS})\"; return 1; fi; for CID in \${CIDS}; do - CNAME=\$(docker container inspect --format {{.Name}} \${CID}); + CNAME=\$(docker container inspect --format '{{.Name}}' \"\${CID}\"); if ! CRM_MSG=\$(docker container rm \${CID} 2>&1); then log ERROR \"Failed to remove \${STATUS} container \${CNAME}, which is using image \${IMAGE}. \$(echo \${CRM_MSG})\"; continue; @@ -157,13 +159,13 @@ remove_images() { done; log INFO \"Done.\"; " - wait_service_state ${SERVICE_NAME} "false" "true"; - docker_service_logs ${SERVICE_NAME} - docker_service_remove ${SERVICE_NAME} + wait_service_state "${SERVICE_NAME}" "false" "true"; + docker_service_logs "${SERVICE_NAME}" + docker_service_remove "${SERVICE_NAME}" } add_service_updated() { - local SERVICE_NAME=${1} + local SERVICE_NAME="${1}" if [ -z "${GLOBAL_SERVICES_UPDATED}" ]; then GLOBAL_SERVICES_UPDATED=${SERVICE_NAME} return 0 @@ -183,7 +185,7 @@ report_services_updated() { } add_service_update_failed() { - local SERVICE_NAME=${1} + local SERVICE_NAME="${1}" if [ -z "${GLOBAL_SERVICES_UPDATE_FAILED}" ]; then GLOBAL_SERVICES_UPDATE_FAILED=${SERVICE_NAME} return 0 @@ -202,23 +204,31 @@ report_services_update_failed() { } get_number_of_elements() { - local LIST=${@} + local LIST="${*}" [ -z "${LIST}" ] && echo 0 && return 0 + # SC2086: Double quote to prevent globbing and word splitting. + # shellcheck disable=SC2086 set ${LIST} local NUM=$# - echo ${NUM} + echo "${NUM}" } report_services() { - local UPDATED_MSG=$(report_services_updated) + local UPDATED_MSG= + local FAILED_MSG= + UPDATED_MSG=$(report_services_updated) echo "${UPDATED_MSG}" | log_lines INFO - local FAILED_MSG=$(report_services_update_failed) + FAILED_MSG=$(report_services_update_failed) echo "${FAILED_MSG}" | log_lines INFO # Send notification - local UPDATED_NUM=$(get_number_of_elements ${GLOBAL_SERVICES_UPDATED}) - local FAILED_NUM=$(get_number_of_elements ${GLOBAL_SERVICES_UPDATE_FAILED}) - local TITLE="[gantry] ${UPDATED_NUM} services updated ${FAILED_NUM} failed" - local BODY=$(echo -e "${UPDATED_MSG}\n${FAILED_MSG}") + local UPDATED_NUM= + local FAILED_NUM= + local TITLE= + local BODY= + UPDATED_NUM=$(get_number_of_elements "${GLOBAL_SERVICES_UPDATED}") + FAILED_NUM=$(get_number_of_elements "${GLOBAL_SERVICES_UPDATE_FAILED}") + TITLE="[gantry] ${UPDATED_NUM} services updated ${FAILED_NUM} failed" + BODY=$(echo -e "${UPDATED_MSG}\n${FAILED_MSG}") send_notification "${TITLE}" "${BODY}" } @@ -234,32 +244,30 @@ in_list() { } service_is_self() { - local SELF=${GANTRY_SERVICES_SELF} - local SERVICE_NAME=${1} + local SELF="${GANTRY_SERVICES_SELF}" + local SERVICE_NAME="${1}" [ "${SERVICE_NAME}" = "${SELF}" ] } # echo the mode when the service is replicated job or global job # return whether a service is replicated job or global job service_is_job() { - local SERVICE_NAME=${1} + local SERVICE_NAME="${1}" local MODE= - MODE=$(docker service ls --filter name=${SERVICE_NAME} --format {{.Mode}} 2>&1) - if [ $? -ne 0 ]; then - log ERROR "Failed to obtain the mode of the service ${SERVICE_NAME}: $(echo ${MODE})" + if ! MODE=$(docker service ls --filter "name=${SERVICE_NAME}" --format '{{.Mode}}' 2>&1); then + log ERROR "Failed to obtain the mode of the service ${SERVICE_NAME}: ${MODE}" return 1 fi # Looking for replicated-job or global-job - echo ${MODE} | grep "job" + echo "${MODE}" | grep "job" } get_config_from_service() { - local SERVICE_NAME=${1} + local SERVICE_NAME="${1}" local AUTH_CONFIG_LABEL="gantry.auth.config" local AUTH_CONFIG= - AUTH_CONFIG=$(docker service inspect -f '{{index .Spec.Labels "${AUTH_CONFIG_LABEL}"}}' "${SERVICE_NAME}" 2>&1) - if [ $? -ne 0 ]; then - log ERROR "Failed to obtain authentication config from service ${SERVICE_NAME}. $(echo ${AUTH_CONFIG})" + if ! AUTH_CONFIG=$(docker service inspect -f "{{index .Spec.Labels \"${AUTH_CONFIG_LABEL}\"}}" "${SERVICE_NAME}" 2>&1); then + log ERROR "Failed to obtain authentication config from service ${SERVICE_NAME}. ${AUTH_CONFIG}" AUTH_CONFIG= fi [ -z "${AUTH_CONFIG}" ] && return 0 @@ -267,35 +275,40 @@ get_config_from_service() { } get_image_info() { - local USE_MANIFEST_CMD=${GANTRY_MANIFEST_USE_MANIFEST_CMD} - local MANIFEST_OPTIONS=${GANTRY_MANIFEST_OPTIONS} - local IMAGE=${1} - local DOCKER_CONFIG=${2} - if is_true ${USE_MANIFEST_CMD}; then - docker ${DOCKER_CONFIG} manifest inspect ${MANIFEST_OPTIONS} ${IMAGE} + local USE_MANIFEST_CMD="${GANTRY_MANIFEST_USE_MANIFEST_CMD}" + local MANIFEST_OPTIONS="${GANTRY_MANIFEST_OPTIONS}" + local IMAGE="${1}" + local DOCKER_CONFIG="${2}" + if is_true "${USE_MANIFEST_CMD}"; then + # SC2086: Double quote to prevent globbing and word splitting. + # shellcheck disable=SC2086 + docker ${DOCKER_CONFIG} manifest inspect ${MANIFEST_OPTIONS} "${IMAGE}" return $? fi # https://github.com/orgs/community/discussions/45779 - docker ${DOCKER_CONFIG} buildx imagetools inspect ${MANIFEST_OPTIONS} ${IMAGE} + # SC2086: Double quote to prevent globbing and word splitting. + # shellcheck disable=SC2086 + docker ${DOCKER_CONFIG} buildx imagetools inspect ${MANIFEST_OPTIONS} "${IMAGE}" } # echo nothing if we found no new images. # echo the image if we found a new image. # return the number of errors. inspect_image() { - local MANIFEST_INSPECT=${GANTRY_MANIFEST_INSPECT:-"true"} - local SERVICE_NAME=${1} - local DOCKER_CONFIG=${2} + local MANIFEST_INSPECT="${GANTRY_MANIFEST_INSPECT:-"true"}" + local SERVICE_NAME="${1}" + local DOCKER_CONFIG="${2}" local IMAGE_WITH_DIGEST= - IMAGE_WITH_DIGEST=$(docker service inspect -f '{{.Spec.TaskTemplate.ContainerSpec.Image}}' "${SERVICE_NAME}" 2>&1) - if [ $? -ne 0 ]; then - log ERROR "Failed to obtain image from service ${SERVICE_NAME}. $(echo ${IMAGE_WITH_DIGEST})" + if ! IMAGE_WITH_DIGEST=$(docker service inspect -f '{{.Spec.TaskTemplate.ContainerSpec.Image}}' "${SERVICE_NAME}" 2>&1); then + log ERROR "Failed to obtain image from service ${SERVICE_NAME}. ${IMAGE_WITH_DIGEST}" return 1 fi - local IMAGE=$(echo "${IMAGE_WITH_DIGEST}" | cut -d@ -f1) - local DIGEST=$(echo "${IMAGE_WITH_DIGEST}" | cut -d@ -f2) + local IMAGE= + local DIGEST= + IMAGE=$(echo "${IMAGE_WITH_DIGEST}" | cut -d@ -f1) + DIGEST=$(echo "${IMAGE_WITH_DIGEST}" | cut -d@ -f2) # Always inspect self - if ! is_true ${MANIFEST_INSPECT} && ! service_is_self ${SERVICE_NAME}; then + if ! is_true "${MANIFEST_INSPECT}" && ! service_is_self "${SERVICE_NAME}"; then return 0 fi if in_list "${GLOBAL_NO_NEW_IMAGES}" "${DIGEST}"; then @@ -303,39 +316,41 @@ inspect_image() { fi local IMAGE_INFO= if ! IMAGE_INFO=$(get_image_info "${IMAGE}" "${DOCKER_CONFIG}" 2>&1); then - log ERROR "Image ${IMAGE} does not exist or it is not available. $(echo ${IMAGE_INFO})" + log ERROR "Image ${IMAGE} does not exist or it is not available. ${IMAGE_INFO}" return 1 fi if [ -n "${DIGEST}" ] && echo "${IMAGE_INFO}" | grep -q "${DIGEST}"; then GLOBAL_NO_NEW_IMAGES=$(echo -e "${GLOBAL_NO_NEW_IMAGES}\n${DIGEST}" | sort | uniq) return 0 fi - echo ${IMAGE} + echo "${IMAGE}" return 0 } get_number_of_running_tasks() { - local SERVICE_NAME=${1} + local SERVICE_NAME="${1}" local REPLICAS= - REPLICAS=$(docker service ls --filter name=${SERVICE_NAME} --format {{.Replicas}} 2>&1) - if [ $? -ne 0 ]; then - log ERROR "Failed to obtain task states of service ${SERVICE_NAME}: $(echo ${REPLICAS})" + if ! REPLICAS=$(docker service ls --filter "name=${SERVICE_NAME}" --format '{{.Replicas}}' 2>&1); then + log ERROR "Failed to obtain task states of service ${SERVICE_NAME}: ${REPLICAS}" return 1 fi # https://docs.docker.com/engine/reference/commandline/service_ls/#examples # The REPLICAS is like "5/5" or "1/1 (3/5 completed)" # Get the number before the first "/". - local NUM_RUNS=$(echo ${REPLICAS} | cut -d '/' -f 1) - echo ${NUM_RUNS} + local NUM_RUNS= + NUM_RUNS=$(echo "${REPLICAS}" | cut -d '/' -f 1) + echo "${NUM_RUNS}" } get_service_update_additional_option() { - local SERVICE_NAME=${1} + local SERVICE_NAME="${1}" local OPTION="--detach=true" local NUM_RUNS= - NUM_RUNS=$(get_number_of_running_tasks ${SERVICE_NAME}) - [ -z "${NUM_RUNS}" ] && return 1 - if [ ${NUM_RUNS} -eq 0 ]; then + NUM_RUNS=$(get_number_of_running_tasks "${SERVICE_NAME}") + if ! is_number "${NUM_RUNS}"; then + return 1 + fi + if [ "${NUM_RUNS}" -eq 0 ]; then # Add "--detach=true" when there is no running tasks. # https://github.com/docker/cli/issues/627 echo -n "${OPTION}" @@ -343,38 +358,41 @@ get_service_update_additional_option() { } rollback_service() { - local ROLLBACK_ON_FAILURE=${GANTRY_ROLLBACK_ON_FAILURE:-"true"} - local ROLLBACK_OPTIONS=${GANTRY_ROLLBACK_OPTIONS} - local SERVICE_NAME=${1} - local DOCKER_CONFIG=${2} - if ! is_true ${ROLLBACK_ON_FAILURE}; then + local ROLLBACK_ON_FAILURE="${GANTRY_ROLLBACK_ON_FAILURE:-"true"}" + local ROLLBACK_OPTIONS="${GANTRY_ROLLBACK_OPTIONS}" + local SERVICE_NAME="${1}" + local DOCKER_CONFIG="${2}" + if ! is_true "${ROLLBACK_ON_FAILURE}"; then return 0 fi log INFO "Rolling ${SERVICE_NAME} back." local ROLLBACK_MSG= + # SC2086: Double quote to prevent globbing and word splitting. + # shellcheck disable=SC2086 ROLLBACK_MSG=$(docker ${DOCKER_CONFIG} service update ${ROLLBACK_OPTIONS} --rollback "${SERVICE_NAME}" 2>&1) local RETURN_VALUE=$? if [ ${RETURN_VALUE} -ne 0 ]; then - log ERROR "Failed to roll back ${SERVICE_NAME}. $(echo ${ROLLBACK_MSG})" + log ERROR "Failed to roll back ${SERVICE_NAME}. ${ROLLBACK_MSG}" fi return ${RETURN_VALUE} } update_single_service() { - local UPDATE_JOBS=${GANTRY_UPDATE_JOBS} - local UPDATE_TIMEOUT_SECONDS=${GANTRY_UPDATE_TIMEOUT_SECONDS:-300} - local UPDATE_OPTIONS=${GANTRY_UPDATE_OPTIONS} + local UPDATE_JOBS="${GANTRY_UPDATE_JOBS}" + local UPDATE_TIMEOUT_SECONDS="${GANTRY_UPDATE_TIMEOUT_SECONDS:-300}" + local UPDATE_OPTIONS="${GANTRY_UPDATE_OPTIONS}" if ! is_number "${UPDATE_TIMEOUT_SECONDS}"; then log ERROR "GANTRY_UPDATE_TIMEOUT_SECONDS must be a number. Got \"${GANTRY_UPDATE_TIMEOUT_SECONDS}\"." return 1; fi - local SERVICE_NAME=${1} + local SERVICE_NAME="${1}" local MODE= - if ! is_true ${UPDATE_JOBS} && MODE=$(service_is_job ${SERVICE_NAME}); then + if ! is_true "${UPDATE_JOBS}" && MODE=$(service_is_job "${SERVICE_NAME}"); then log DEBUG "Skip updating service ${SERVICE_NAME} that is a ${MODE}." return 0; fi - local DOCKER_CONFIG=$(get_config_from_service ${SERVICE_NAME}) + local DOCKER_CONFIG= + DOCKER_CONFIG=$(get_config_from_service "${SERVICE_NAME}") [ -n "${DOCKER_CONFIG}" ] && log DEBUG "Add option \"${DOCKER_CONFIG}\" to docker commands." local IMAGE= IMAGE=$(inspect_image "${SERVICE_NAME}" "${DOCKER_CONFIG}") @@ -382,35 +400,41 @@ update_single_service() { [ ${RETURN_VALUE} -ne 0 ] && return ${RETURN_VALUE} [ -z "${IMAGE}" ] && log INFO "No new images." && return 0 log INFO "Updating with image ${IMAGE}" - local ADDITIONAL_OPTION=$(get_service_update_additional_option ${SERVICE_NAME}) + local ADDITIONAL_OPTION= + ADDITIONAL_OPTION=$(get_service_update_additional_option "${SERVICE_NAME}") [ -n "${ADDITIONAL_OPTION}" ] && log DEBUG "Add option \"${ADDITIONAL_OPTION}\" to the docker service update command." + # SC2086: Double quote to prevent globbing and word splitting. + # shellcheck disable=SC2086 if ! UPDATE_MSG=$(timeout "${UPDATE_TIMEOUT_SECONDS}" docker ${DOCKER_CONFIG} service update ${ADDITIONAL_OPTION} ${UPDATE_OPTIONS} --image="${IMAGE}" "${SERVICE_NAME}" 2>&1); then - log ERROR "docker service update failed or timeout. $(echo ${UPDATE_MSG})" + log ERROR "docker service update failed or timeout. ${UPDATE_MSG}" rollback_service "${SERVICE_NAME}" "${DOCKER_CONFIG}" - add_servicess_update_failed ${SERVICE_NAME} + add_service_update_failed "${SERVICE_NAME}" return 1 fi - local PREVIOUS_IMAGE=$(docker service inspect -f '{{.PreviousSpec.TaskTemplate.ContainerSpec.Image}}' "${SERVICE_NAME}") - local CURRENT_IMAGE=$(docker service inspect -f '{{.Spec.TaskTemplate.ContainerSpec.Image}}' "${SERVICE_NAME}") + local PREVIOUS_IMAGE= + local CURRENT_IMAGE= + PREVIOUS_IMAGE=$(docker service inspect -f '{{.PreviousSpec.TaskTemplate.ContainerSpec.Image}}' "${SERVICE_NAME}") + CURRENT_IMAGE=$(docker service inspect -f '{{.Spec.TaskTemplate.ContainerSpec.Image}}' "${SERVICE_NAME}") if [ "${PREVIOUS_IMAGE}" = "${CURRENT_IMAGE}" ]; then log INFO "No updates." return 0 fi - add_service_updated ${SERVICE_NAME} - add_image_to_remove ${PREVIOUS_IMAGE} + add_service_updated "${SERVICE_NAME}" + add_image_to_remove "${PREVIOUS_IMAGE}" log INFO "UPDATED." return 0 } get_services_filted() { - local SERVICES_FILTERS=${1} + local SERVICES_FILTERS="${1}" local SERVICES= local FILTERS= for F in ${SERVICES_FILTERS}; do FILTERS="${FILTERS} --filter ${F}" done - SERVICES=$(docker service ls --quiet ${FILTERS} --format '{{.Name}}' 2>&1) - if [ $? -ne 0 ]; then + # SC2086: Double quote to prevent globbing and word splitting. + # shellcheck disable=SC2086 + if ! SERVICES=$(docker service ls --quiet ${FILTERS} --format '{{.Name}}' 2>&1); then log ERROR "Failed to obtain services list with \"${FILTERS}\"." return 1 fi @@ -419,7 +443,7 @@ get_services_filted() { } gantry_initialize() { - local STACK=${1:-gantry} + local STACK="${1:-gantry}" GLOBAL_IMAGES_TO_REMOVE= GLOBAL_SERVICES_UPDATED= GLOBAL_SERVICES_UPDATE_FAILED= @@ -428,16 +452,18 @@ gantry_initialize() { } gantry_get_services_list() { - local SERVICES_EXCLUDED=${GANTRY_SERVICES_EXCLUDED} - local SERVICES_EXCLUDED_FILTERS=${GANTRY_SERVICES_EXCLUDED_FILTERS} - local SERVICES_FILTERS=${GANTRY_SERVICES_FILTERS} + local SERVICES_EXCLUDED="${GANTRY_SERVICES_EXCLUDED}" + local SERVICES_EXCLUDED_FILTERS="${GANTRY_SERVICES_EXCLUDED_FILTERS}" + local SERVICES_FILTERS="${GANTRY_SERVICES_FILTERS}" local SERVICES= - SERVICES=$(get_services_filted "${SERVICES_FILTERS}") - [ $? -ne 0 ] && return 1 + if ! SERVICES=$(get_services_filted "${SERVICES_FILTERS}"); then + return 1 + fi if [ -n "${SERVICES_EXCLUDED_FILTERS}" ]; then local SERVICES_FROM_EXCLUDED_FILTERS= - SERVICES_FROM_EXCLUDED_FILTERS=$(get_services_filted "${SERVICES_EXCLUDED_FILTERS}") - [ $? -ne 0 ] && return 1 + if ! SERVICES_FROM_EXCLUDED_FILTERS=$(get_services_filted "${SERVICES_EXCLUDED_FILTERS}"); then + return 1 + fi SERVICES_EXCLUDED="${SERVICES_EXCLUDED} ${SERVICES_FROM_EXCLUDED_FILTERS}" fi local LIST= @@ -447,7 +473,7 @@ gantry_get_services_list() { continue fi # Add self to the first of the list. - if service_is_self ${S}; then + if service_is_self "${S}"; then HAS_SELF=${S} continue fi @@ -457,16 +483,16 @@ gantry_get_services_list() { if [ -n "${HAS_SELF}" ]; then LIST="${HAS_SELF} ${LIST}" fi - echo ${LIST} + echo "${LIST}" } gantry_update_services_list() { - local LIST=${@} + local LIST="${*}" local ACCUMULATED_ERRORS=0 - local LOG_SCOPE_SAVED=${LOG_SCOPE} + local LOG_SCOPE_SAVED="${LOG_SCOPE}" for SERVICE in ${LIST}; do LOG_SCOPE="Updating service ${SERVICE}" - update_single_service ${SERVICE} + update_single_service "${SERVICE}" ACCUMULATED_ERRORS=$((ACCUMULATED_ERRORS + $?)) done LOG_SCOPE=${LOG_SCOPE_SAVED} @@ -474,7 +500,7 @@ gantry_update_services_list() { } gantry_finalize() { - local STACK=${1:-gantry} + local STACK="${1:-gantry}" report_services; remove_images "${STACK}_image-remover" } diff --git a/src/notification.sh b/src/notification.sh index 9305031..16ae2b8 100755 --- a/src/notification.sh +++ b/src/notification.sh @@ -16,17 +16,17 @@ # notify_via_apprise() { - local URL=${GANTRY_NOTIFICATION_APPRISE_URL} + local URL="${GANTRY_NOTIFICATION_APPRISE_URL}" local TITLE="${1}" local BODY="${2}" if [ -z "${URL}" ]; then return 0 fi - curl -X POST -H "Content-Type: application/json" --data "{\"title\": \"${TITLE}\", \"body\": \"${BODY}\"}" "$URL" + curl -X POST -H "Content-Type: application/json" --data "{\"title\": \"${TITLE}\", \"body\": \"${BODY}\"}" "${URL}" } notify_summary() { - local CUSTOMIZED_TITLE=${GANTRY_NOTIFICATION_TITLE} + local CUSTOMIZED_TITLE="${GANTRY_NOTIFICATION_TITLE}" local TITLE="${1}" local BODY="${2}" [ -n "${CUSTOMIZED_TITLE}" ] && TITLE="${TITLE} ${CUSTOMIZED_TITLE}"