Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve messages, Add tests for login registries, Add alpine patch number #66

Merged
merged 11 commits into from
Nov 6, 2024
1 change: 1 addition & 0 deletions .github/workflows/on-pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ jobs:
- gantry_cleanup_images_spec.sh
- gantry_common_options_spec.sh
- gantry_filters_spec.sh
- gantry_login_negative_spec.sh
- gantry_login_spec.sh
- gantry_manifest_spec.sh
- gantry_notify_spec.sh
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/on-push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ jobs:
- gantry_cleanup_images_spec.sh
- gantry_common_options_spec.sh
- gantry_filters_spec.sh
- gantry_login_negative_spec.sh
- gantry_login_spec.sh
- gantry_manifest_spec.sh
- gantry_notify_spec.sh
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM alpine:3.20
FROM alpine:3.20.3

LABEL org.opencontainers.image.title=gantry
LABEL org.opencontainers.image.description="Updating docker swarm services"
Expand Down
27 changes: 22 additions & 5 deletions src/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#

load_libraries() {
local LOCAL_LOG_LEVEL="${GANTRY_LOG_LEVEL:-""}"
_get_lib_dir() {
local LIB_DIR=
if [ -n "${GANTRY_LIB_DIR:-""}" ]; then
LIB_DIR="${GANTRY_LIB_DIR}"
Expand All @@ -29,10 +28,28 @@ load_libraries() {
elif [ -r "./lib-gantry.sh" ]; then
LIB_DIR="."
fi
echo "${LIB_DIR}"
}

_log_load_libraries() {
local LOG_LEVEL="${GANTRY_LOG_LEVEL:-""}"
local IMAGES_TO_REMOVE="${GANTRY_IMAGES_TO_REMOVE:-""}"
local LIB_DIR="${1}"
# log function is not available before loading the library.
if ! echo "${LOCAL_LOG_LEVEL}" | grep -q -i "NONE"; then
echo "Loading libraries from ${LIB_DIR}"
if echo "${LOG_LEVEL}" | grep -q -i "NONE"; then
return 0
fi
local TIMESTAMP=
if [ -z "${IMAGES_TO_REMOVE}" ]; then
TIMESTAMP="[$(date -Iseconds)] "
fi
echo "${TIMESTAMP}Loading libraries from ${LIB_DIR}" >&2
}

load_libraries() {
local LIB_DIR=
LIB_DIR=$(_get_lib_dir)
_log_load_libraries "${LIB_DIR}"
. "${LIB_DIR}/notification.sh"
. "${LIB_DIR}/docker_hub_rate.sh"
. "${LIB_DIR}/lib-common.sh"
Expand Down Expand Up @@ -155,7 +172,7 @@ main() {
if [ -n "${IMAGES_TO_REMOVE}" ]; then
# Image remover runs as a global job. The log will be collected via docker commands then formatted.
# Redefine the log function for the formater.
log() { echo "${@}"; }
log() { echo "${@}" >&2; }
gantry_remove_images "${IMAGES_TO_REMOVE}"
return $?
fi
Expand Down
59 changes: 53 additions & 6 deletions src/lib-common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -228,10 +228,15 @@ read_config() {
read_env() {
local VNAME="${1}"; shift
[ -z "${VNAME}" ] && return 1
if env | grep -q "${VNAME}="; then
eval "echo \"\${${VNAME}}\""
else
# "grep -q" will exit immediately when the first line of data matches, and leading to broken pipe errors.
# Add "cat > /dev/null" to avoid broken pipe errors.
local GREP_RETURN=
env | (grep -q "^${VNAME}="; local R=$?; cat >/dev/null; test "${R}" == "0";);
GREP_RETURN=$?
if [ "${GREP_RETURN}" != "0" ]; then
echo "${@}"
else
eval "echo \"\${${VNAME}}\""
fi
return 0
}
Expand Down Expand Up @@ -418,6 +423,46 @@ docker_version() {
echo "Docker version client ${cver} (API ${capi}) server ${sver} (API ${sapi})"
}

# echo the name of the current container.
# echo nothing if unable to find the name.
# return 1 when there is an error.
docker_current_container_name() {
local ALL_NETWORKS=
ALL_NETWORKS=$(docker network ls --format '{{.ID}}') || return 1;
[ -z "${ALL_NETWORKS}" ] && return 0;
local IPS=;
IPS=$(ip route | grep src | sed -n "s/.* src \(\S*\).*$/\1/p");
[ -z "${IPS}" ] && return 0;
local GWBRIDGE_NETWORK HOST_NETWORK;
GWBRIDGE_NETWORK=$(docker network ls --format '{{.ID}}' --filter 'name=^docker_gwbridge$') || return 1;
HOST_NETWORK=$(docker network ls --format '{{.ID}}' --filter 'name=^host$') || return 1;
local NID=;
for NID in ${ALL_NETWORKS}; do
# The output of gwbridge does not contain the container name. It looks like gateway_8f55496ce4f1/172.18.0.5/16.
[ "${NID}" = "${GWBRIDGE_NETWORK}" ] && continue;
# The output of host does not contain an IP.
[ "${NID}" = "${HOST_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;
# NAME_AND_IP will be in one of the following formats:
# '<container name>/<ip>/<mask>'
# '<container name>/' (when network mode is host)
local CNAME CIP
CNAME=$(echo "${NAME_AND_IP}/" | cut -d/ -f1);
CIP=$(echo "${NAME_AND_IP}/" | cut -d/ -f2);
# Unable to find the container IP when network mode is host.
[ -z "${CIP}" ] && continue;
for IP in ${IPS}; do
[ "${IP}" != "${CIP}" ] && continue;
echo "${CNAME}";
return 0;
done
done
done
}

docker_service_remove() {
local SERVICE_NAME="${1}"
if ! docker service inspect --format '{{.JobStatus}}' "${SERVICE_NAME}" >/dev/null 2>&1; then
Expand Down Expand Up @@ -493,13 +538,15 @@ docker_run() {
local RETRIES=0
local MAX_RETRIES=5
local SLEEP_SECONDS=10
while ! docker run "${@}" >/dev/null; do
local MSG=
while ! MSG=$(docker run "${@}" 2>&1); do
if [ ${RETRIES} -ge ${MAX_RETRIES} ]; then
echo "Failed to run docker. Reached the max retries ${MAX_RETRIES}." >&2
log ERROR "Failed to run docker. Reached the max retries ${MAX_RETRIES}. ${MSG}"
return 1
fi
RETRIES=$((RETRIES + 1))
sleep ${SLEEP_SECONDS}
echo "Retry docker run (${RETRIES})."
log WARN "Retry docker run (${RETRIES}). ${MSG}"
done
echo "${MSG}"
}
93 changes: 44 additions & 49 deletions src/lib-gantry.sh
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ _read_env_or_label() {
echo "${VALUE}"
}


_login_registry() {
local USER="${1}"
local PASSWORD="${2}"
Expand All @@ -85,24 +84,30 @@ _login_registry() {
fi
[ -z "${USER}" ] && log ERROR "USER is empty." && return 1
[ -z "${PASSWORD}" ] && log ERROR "PASSWORD is empty." && return 1
local DOCKER_CONFIG=
local CONFIG_MESSAGE=" ${HOST}"
local REGISTRY_MESSAGE="registry ${HOST}"
if [ -z "${HOST}" ]; then
log WARN "HOST is empty. Will login to the default registry."
CONFIG_MESSAGE=""
REGISTRY_MESSAGE="default registry"
fi
local DOCKER_CONFIG=
local CONFIG_TO_REPORT=
CONFIG_TO_REPORT=$(readlink -f ~/.docker)
local CONFIG_MESSAGE="with default configuration"
if [ -n "${CONFIG}" ]; then
DOCKER_CONFIG="--config ${CONFIG}"
CONFIG_MESSAGE="${CONFIG_MESSAGE} for config ${CONFIG}"
CONFIG_TO_REPORT="${CONFIG}"
CONFIG_MESSAGE="with configuration ${CONFIG}"
fi
local REGISTRY_CONFIG_MESSAGE="${REGISTRY_MESSAGE} ${CONFIG_MESSAGE}"
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}"
log ERROR "Failed to login to ${REGISTRY_CONFIG_MESSAGE}. ${LOGIN_MSG}"
return 1
fi
log INFO "Logged into registry${CONFIG_MESSAGE}. ${LOGIN_MSG}"
log INFO "Logged into ${REGISTRY_CONFIG_MESSAGE}. ${LOGIN_MSG}"
_static_variable_add_unique_to_list STATIC_VAR_SERVICES_DOCKER_CONFIGS "${CONFIG_TO_REPORT}"
return 0
}

Expand Down Expand Up @@ -407,15 +412,15 @@ _remove_images() {
docker_service_remove "${SERVICE_NAME}"
}

_report_services_list() {
_report_list() {
local PRE="${1}"; shift
local POST="${1}"; shift
local LIST="${*}"
local NUM=
NUM=$(_get_number_of_elements "${LIST}")
local TITLE=
[ -n "${PRE}" ] && TITLE="${PRE} "
TITLE="${TITLE}${NUM} service(s)"
TITLE="${TITLE}${NUM}"
[ -n "${POST}" ] && TITLE="${TITLE} ${POST}"
echo "${TITLE}:"
local ITEM=
Expand All @@ -424,7 +429,7 @@ _report_services_list() {
done
}

_report_services_from_static_variable() {
_report_from_static_variable() {
local VARIABLE_NAME="${1}"
local PRE="${2}"
local POST="${3}"
Expand All @@ -435,7 +440,20 @@ _report_services_from_static_variable() {
echo "${EMPTY}"
return 0
fi
_report_services_list "${PRE}" "${POST}" "${LIST}"
_report_list "${PRE}" "${POST}" "${LIST}"
}

_report_services_from_static_variable() {
local VARIABLE_NAME="${1}"
local PRE="${2}"
local POST="${3}"
local EMPTY="${4}"
if [ -z "${POST}" ]; then
POST="service(s)"
else
POST="service(s) ${POST}"
fi
_report_from_static_variable "${VARIABLE_NAME}" "${PRE}" "${POST}" "${EMPTY}"
}

_get_number_of_elements() {
Expand Down Expand Up @@ -529,43 +547,15 @@ _current_container_name() {
local NO_CURRENT_CONTAINER_NAME=
NO_CURRENT_CONTAINER_NAME=$(_static_variable_read_list STATIC_VAR_NO_CURRENT_CONTAINER_NAME)
[ -n "${NO_CURRENT_CONTAINER_NAME}" ] && return 0
local ALL_NETWORKS=
ALL_NETWORKS=$(docker network ls --format '{{.ID}}') || return 1;
[ -z "${ALL_NETWORKS}" ] && return 0;
local IPS=;
IPS=$(ip route | grep src | sed -n "s/.* src \(\S*\).*$/\1/p");
[ -z "${IPS}" ] && return 0;
local GWBRIDGE_NETWORK HOST_NETWORK;
GWBRIDGE_NETWORK=$(docker network ls --format '{{.ID}}' --filter 'name=^docker_gwbridge$') || return 1;
HOST_NETWORK=$(docker network ls --format '{{.ID}}' --filter 'name=^host$') || return 1;
local NID=;
for NID in ${ALL_NETWORKS}; do
# The output of gwbridge does not contain the container name. It looks like gateway_8f55496ce4f1/172.18.0.5/16.
[ "${NID}" = "${GWBRIDGE_NETWORK}" ] && continue;
# The output of host does not contain an IP.
[ "${NID}" = "${HOST_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;
# NAME_AND_IP will be in one of the following formats:
# '<container name>/<ip>/<mask>'
# '<container name>/' (when network mode is host)
local CNAME CIP
CNAME=$(echo "${NAME_AND_IP}/" | cut -d/ -f1);
CIP=$(echo "${NAME_AND_IP}/" | cut -d/ -f2);
# Unable to find the container IP when network mode is host.
[ -z "${CIP}" ] && continue;
for IP in ${IPS}; do
[ "${IP}" != "${CIP}" ] && continue;
_static_variable_add_unique_to_list STATIC_VAR_CURRENT_CONTAINER_NAME "${CNAME}"
echo "${CNAME}";
return 0;
done
done
done
# Explicitly set that we cannot find the name of current container.
_static_variable_add_unique_to_list STATIC_VAR_NO_CURRENT_CONTAINER_NAME "NO_CURRENT_CONTAINER_NAME"
local CNAME=
CNAME=$(docker_current_container_name) || return 1;
if [ -n "${CNAME}" ]; then
_static_variable_add_unique_to_list STATIC_VAR_CURRENT_CONTAINER_NAME "${CNAME}"
else
# Explicitly set that we cannot find the name of current container.
_static_variable_add_unique_to_list STATIC_VAR_NO_CURRENT_CONTAINER_NAME "NO_CURRENT_CONTAINER_NAME"
fi
echo "${CNAME}"
return 0;
}

Expand Down Expand Up @@ -659,6 +649,11 @@ _get_config_from_service() {
local AUTH_CONFIG=
AUTH_CONFIG=$(_get_label_from_service "${SERVICE_NAME}" "${AUTH_CONFIG_LABEL}")
[ -z "${AUTH_CONFIG}" ] && return 0
if [ ! -d "${AUTH_CONFIG}" ]; then
log WARN "${AUTH_CONFIG} is not a directory that contains Docker configuration files."
local MSG="configuration(s) set via GANTRY_REGISTRY_CONFIG* or GANTRY_REGISTRY_CONFIGS_FILE"
_report_from_static_variable STATIC_VAR_SERVICES_DOCKER_CONFIGS "There are" "${MSG}" "There are no ${MSG}." | log_lines WARN
fi
echo "--config ${AUTH_CONFIG}"
}

Expand Down Expand Up @@ -708,7 +703,7 @@ _get_image_info() {
return 1
fi
if [ "${RETURN_VALUE}" != "0" ]; then
log ERROR "Image ${IMAGE} does not exist or it is not available. ${MSG}"
log ERROR "Image ${IMAGE} does not exist or it is not available. Docker ${MANIFEST_CMD} returns: ${MSG}"
return 1
fi
echo "${MSG}"
Expand Down
17 changes: 13 additions & 4 deletions tests/gantry_cleanup_images_spec.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ Describe 'cleanup-images'
When run test_CLEANUP_IMAGES_false "${TEST_NAME}" "${SERVICE_NAME}"
The status should be success
The stdout should satisfy display_output
The stdout should satisfy spec_expect_no_message ".+"
The stderr should satisfy display_output
The stderr should satisfy spec_expect_no_message "${NOT_START_WITH_A_SQUARE_BRACKET}"
The stderr should satisfy spec_expect_no_message "${SKIP_UPDATING}.*${SERVICE_NAME}"
The stderr should satisfy spec_expect_message "${PERFORM_UPDATING}.*${SERVICE_NAME}.*${PERFORM_REASON_HAS_NEWER_IMAGE}"
The stderr should satisfy spec_expect_no_message "${NUM_SERVICES_SKIP_JOBS}"
Expand Down Expand Up @@ -79,7 +81,9 @@ Describe 'cleanup-images'
When run test_CLEANUP_IMAGES_OPTIONS_bad "${TEST_NAME}" "${SERVICE_NAME}"
The status should be success
The stdout should satisfy display_output
The stdout should satisfy spec_expect_no_message ".+"
The stderr should satisfy display_output
The stderr should satisfy spec_expect_no_message "${NOT_START_WITH_A_SQUARE_BRACKET}"
The stderr should satisfy spec_expect_no_message "${SKIP_UPDATING}.*${SERVICE_NAME}"
The stderr should satisfy spec_expect_message "${PERFORM_UPDATING}.*${SERVICE_NAME}.*${PERFORM_REASON_HAS_NEWER_IMAGE}"
The stderr should satisfy spec_expect_no_message "${NUM_SERVICES_SKIP_JOBS}"
Expand Down Expand Up @@ -122,7 +126,9 @@ Describe 'cleanup-images'
When run test_CLEANUP_IMAGES_OPTIONS_good "${TEST_NAME}" "${SERVICE_NAME}"
The status should be success
The stdout should satisfy display_output
The stdout should satisfy spec_expect_no_message ".+"
The stderr should satisfy display_output
The stderr should satisfy spec_expect_no_message "${NOT_START_WITH_A_SQUARE_BRACKET}"
The stderr should satisfy spec_expect_no_message "${SKIP_UPDATING}.*${SERVICE_NAME}"
The stderr should satisfy spec_expect_message "${PERFORM_UPDATING}.*${SERVICE_NAME}.*${PERFORM_REASON_HAS_NEWER_IMAGE}"
The stderr should satisfy spec_expect_no_message "${NUM_SERVICES_SKIP_JOBS}"
Expand Down Expand Up @@ -209,11 +215,14 @@ Describe 'cleanup-images'
When run test_IMAGES_TO_REMOVE_none_empty "${TEST_NAME}" "${SERVICE_NAME}" "${IMAGE_WITH_TAG}"
The status should be success
The stdout should satisfy display_output
The stdout should satisfy spec_expect_message "Removed exited container.*${SERVICE_NAME0}.*${IMAGE_WITH_TAG0}"
The stdout should satisfy spec_expect_message "${REMOVED_IMAGE}.*${IMAGE_WITH_TAG0}"
The stdout should satisfy spec_expect_message "${FAILED_TO_REMOVE_IMAGE}.*${IMAGE_WITH_TAG1}"
The stdout should satisfy spec_expect_message "There is no image.*${IMAGE_WITH_TAG2}"
The stdout should satisfy spec_expect_no_message ".+"
The stderr should satisfy display_output
# It should not use the log function from the lib-common, the messages do not start with "[".
The stderr should satisfy spec_expect_no_message "^((?:\x1b\[[0-9;]*[mG])?\[)"
The stderr should satisfy spec_expect_message "Removed exited container.*${SERVICE_NAME0}.*${IMAGE_WITH_TAG0}"
The stderr should satisfy spec_expect_message "${REMOVED_IMAGE}.*${IMAGE_WITH_TAG0}"
The stderr should satisfy spec_expect_message "${FAILED_TO_REMOVE_IMAGE}.*${IMAGE_WITH_TAG1}"
The stderr should satisfy spec_expect_message "There is no image.*${IMAGE_WITH_TAG2}"
End
End
End # Describe 'Single service'
Loading
Loading