blob: 50bf432f2fd1e02c60f83edfca239068287b5e3c [file] [log] [blame]
#!/usr/bin/env bash
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
DOCKERMODE=false
DOCKERCMD=$(command -v docker)
DOCKER_ID=${RANDOM}
DOCKER_DESTRUCTIVE=true
DOCKERFILE_DEFAULT="${BINDIR}/test-patch-docker/Dockerfile"
DOCKERFAIL="fallback,continue,fail"
DOCKERSUPPORT=false
DOCKER_ENABLE_PRIVILEGED=true
DOCKER_CLEANUP_CMD=false
DOCKER_MEMORY="4g"
declare -a DOCKER_EXTRAARGS
####
#### IMPORTANT
####
#### If these times are updated, the documentation needs to
#### be changed too!
# created, stopped, exited, running, for 24 hours
DOCKER_CONTAINER_PURGE=("86400" "86400" "86400" "86400" )
# keep images for 1 week
DOCKER_IMAGE_PURGE=604800
## @description Docker-specific usage
## @stability stable
## @audience private
## @replaceable no
function docker_usage
{
if [[ "${DOCKER_CLEANUP_CMD}" == false ]]; then
yetus_add_option "--docker" "Spawn a docker container"
fi
yetus_add_option "--dockercmd=<file>" "Command to use as docker executable (default: '${DOCKERCMD}')"
if [[ "${DOCKER_CLEANUP_CMD}" == false ]]; then
yetus_add_option "--dockerfile=<file>" "Dockerfile fragment to use as the base (default: '${DOCKERFILE_DEFAULT}')"
yetus_add_option "--dockeronfail=<list>" "If Docker fails, determine fallback method order (default: ${DOCKERFAIL})"
yetus_add_option "--dockerprivd=<bool>" "Run docker in privileged mode (default: '${DOCKER_ENABLE_PRIVILEGED}')"
fi
yetus_add_option "--dockerdelrep" "In Docker mode, only report image/container deletions, not act on them"
if [[ "${DOCKER_CLEANUP_CMD}" == false ]]; then
yetus_add_option "--dockermemlimit=<num>" "Limit a Docker container's memory usage (default: ${DOCKER_MEMORY})"
fi
}
## @description Docker-specific argument parsing
## @stability stable
## @audience private
## @replaceable no
## @params arguments
function docker_parse_args
{
declare i
for i in "$@"; do
case ${i} in
--docker)
DOCKERSUPPORT=true
;;
--dockercmd=*)
#shellcheck disable=SC2034
DOCKERCMD=${i#*=}
;;
--dockerdelrep)
DOCKER_DESTRUCTIVE=false
;;
--dockerfile=*)
DOCKERFILE=${i#*=}
;;
--dockermemlimit=*)
DOCKER_MEMORY=${i#*=}
;;
--dockermode)
DOCKERMODE=true
;;
--dockeronfail=*)
DOCKERFAIL=${i#*=}
;;
--dockerprivd=*)
DOCKER_ENABLE_PRIVILEGED=${i#*=}
;;
esac
done
}
## @description Docker initialization pre- and post- re-exec
## @stability stable
## @audience private
## @replaceable no
function docker_initialize
{
declare dockvers
# --docker and --dockermode are mutually
# exclusive. --docker is used by the user to
# re-exec test-patch in Docker mode.
# --dockermode is used by launch-test-patch (which is
# run as the Docker EXEC in the Dockerfile,
# see elsewhere for more info) to tell test-patch that
# it has been restarted already. launch-test-patch
# also strips --docker from the command line so that we
# don't end up in a loop if the docker image
# also has the docker command in it
# we are already in docker mode
if [[ "${DOCKERMODE}" == true ]]; then
# DOCKER_VERSION is set by our creator.
add_footer_table "Docker" "${DOCKER_VERSION}"
return
fi
# docker mode hasn't been requested
if [[ "${DOCKERSUPPORT}" != true ]]; then
return
fi
# turn DOCKERFAIL into a string composed of numbers
# to ease interpretation: 123, 213, 321, ... whatever
# some of these combos are non-sensical but that's ok.
# we'll treat non-sense as effectively errors.
DOCKERFAIL=${DOCKERFAIL//,/ }
DOCKERFAIL=${DOCKERFAIL//fallback/1}
DOCKERFAIL=${DOCKERFAIL//continue/2}
DOCKERFAIL=${DOCKERFAIL//fail/3}
DOCKERFAIL=${DOCKERFAIL//[[:blank:]]/}
if ! docker_exeverify; then
if [[ "${DOCKERFAIL}" =~ ^12
|| "${DOCKERFAIL}" =~ ^2 ]]; then
add_vote_table 0 docker "Docker command '${DOCKERCMD}' not found/broken. Disabling docker."
DOCKERSUPPORT=false
else
add_vote_table -1 docker "Docker command '${DOCKERCMD}' not found/broken."
bugsystem_finalreport 1
cleanup_and_exit 1
fi
fi
dockvers=$(docker_version Client)
if [[ "${dockvers}" =~ ^0
|| "${dockvers}" =~ ^1\.[0-5]$ || "${dockvers}" =~ ^1\.[0-5]\. ]]; then
if [[ "${DOCKERFAIL}" =~ ^12
|| "${DOCKERFAIL}" =~ ^2 ]]; then
add_vote_table 0 docker "Docker command '${DOCKERCMD}' is too old (${dockvers} < 1.6.0). Disabling docker."
DOCKERSUPPORT=false
else
add_vote_table -1 docker "Docker command '${DOCKERCMD}' is too old (${dockvers} < 1.6.0). Disabling docker."
bugsystem_finalreport 1
cleanup_and_exit 1
fi
fi
}
## @description Verify dockerfile exists
## @audience private
## @stability evolving
## @replaceable no
## @return exits on failure if configured
function docker_fileverify
{
if [[ ${DOCKERMODE} = false &&
${DOCKERSUPPORT} = true ]]; then
if [[ -n "${DOCKERFILE}" ]]; then
pushd "${STARTINGDIR}" >/dev/null
if [[ -f ${DOCKERFILE} ]]; then
DOCKERFILE=$(yetus_abs "${DOCKERFILE}")
else
if [[ "${DOCKERFAIL}" =~ ^1 ]]; then
yetus_error "ERROR: Dockerfile '${DOCKERFILE}' not found, falling back to built-in."
add_vote_table 0 docker "Dockerfile '${DOCKERFILE}' not found, falling back to built-in."
DOCKERFILE=${DOCKERFILE_DEFAULT}
elif [[ "${DOCKERFAIL}" =~ ^2 ]]; then
yetus_error "ERROR: Dockerfile '${DOCKERFILE}' not found, disabling docker."
add_vote_table 0 docker "Dockerfile '${DOCKERFILE}' not found, disabling docker."
DOCKERSUPPORT=false
else
yetus_error "ERROR: Dockerfile '${DOCKERFILE}' not found."
add_vote_table -1 docker "Dockerfile '${DOCKERFILE}' not found."
bugsystem_finalreport 1
cleanup_and_exit 1
fi
fi
popd >/dev/null
else
DOCKERFILE=${DOCKERFILE_DEFAULT}
fi
fi
}
## @description Verify docker exists
## @audience private
## @stability evolving
## @replaceable no
## @return 1 if docker is broken
## @return 0 if docker is working
function docker_exeverify
{
if ! verify_command "Docker" "${DOCKERCMD}"; then
return 1
fi
if ! ${DOCKERCMD} info >/dev/null 2>&1; then
yetus_error "Docker is not functioning properly. Daemon down/unreachable?"
return 1
fi
return 0
}
## @description Run docker with some arguments, and
## @description optionally send to debug.
## @description some destructive commands require
## @description DOCKER_DESTRUCTIVE to be set to true
## @audience private
## @stability evolving
## @replaceable no
## @param args
function dockercmd
{
declare subcmd=$1
shift
yetus_debug "dockercmd: ${DOCKERCMD} ${subcmd} $*"
if [[ ${subcmd} == rm
|| ${subcmd} == rmi
|| ${subcmd} == stop
|| ${subcmd} == kill ]]; then
if [[ "${DOCKER_DESTRUCTIVE}" == false ]]; then
yetus_error "Safemode: not running ${DOCKERCMD} ${subcmd} $*"
return
fi
fi
"${DOCKERCMD}" "${subcmd}" "$@"
}
## @description Convet docker's time format to ctime
## @audience private
## @stability evolving
## @replaceable no
## @param time
function dockerdate_to_ctime
{
declare mytime=$1
if [[ "${mytime}" = "0001-01-01T00:00:00Z" ]]; then
mytime="1970-01-01T00:00:00"
fi
# believe it or not, date is not even close to standardized...
if [[ $(uname -s) == Linux ]]; then
# GNU date
date -d "${mytime}" "+%s"
else
# BSD date; docker gives us two different format because fun
if ! date -j -f "%FT%T%z" "${mytime}" "+%s" 2>/dev/null; then
date -j -f "%FT%T" "${mytime}" "+%s"
fi
fi
}
## @description Stop and delete all defunct containers
## @audience private
## @stability evolving
## @replaceable no
function docker_container_maintenance
{
declare line
declare id
declare name
declare status
declare tmptime
declare createtime
declare starttime
declare stoptime
declare remove
declare difftime
declare data
if [[ "${ROBOT}" = false ]]; then
return
fi
big_console_header "Docker Container Maintenance"
dockercmd ps -a
data=$(dockercmd ps -qa)
if [[ -z "${data}" ]]; then
return
fi
while read -r line; do
id=$(echo "${line}" | cut -f1 -d, )
status=$(echo "${line}" | cut -f2 -d, )
tmptime=$(echo "${line}" | cut -f3 -d, | cut -f1 -d. )
createtime=$(dockerdate_to_ctime "${tmptime}")
tmptime=$(echo "${line}" | cut -f4 -d, | cut -f1 -d. )
starttime=$(dockerdate_to_ctime "${tmptime}")
tmptime=$(echo "${line}" | cut -f5 -d, | cut -f1 -d. )
stoptime=$(dockerdate_to_ctime "${tmptime}")
name=$(echo "${line}" | cut -f6 -d, )
curtime=$("${AWK}" 'BEGIN {srand(); print srand()}')
remove=false
case ${status} in
created)
((difftime = curtime - createtime))
if [[ ${difftime} -gt ${DOCKER_CONTAINER_PURGE[0]} ]]; then
remove=true
fi
;;
stopped)
((difftime = curtime - stoptime))
if [[ ${difftime} -gt ${DOCKER_CONTAINER_PURGE[1]} ]]; then
remove=true
fi
;;
exited | dead)
((difftime = curtime - stoptime))
if [[ ${difftime} -gt ${DOCKER_CONTAINER_PURGE[2]} ]]; then
remove=true
fi
;;
running)
((difftime = curtime - starttime))
if [[ ${difftime} -gt ${DOCKER_CONTAINER_PURGE[3]}
&& "${SENTINEL}" = true ]]; then
remove=true
echo "Attempting to kill docker container ${name} [${id}]"
dockercmd kill "${id}"
fi
;;
*)
;;
esac
if [[ "${remove}" == true ]]; then
echo "Attempting to remove docker container ${name} [${id}]"
dockercmd rm "${id}"
fi
done < <(
# shellcheck disable=SC2086
dockercmd inspect \
--format '{{.Id}},{{.State.Status}},{{.Created}},{{.State.StartedAt}},{{.State.FinishedAt}},{{.Name}}' \
${data})
}
## @description Delete images after ${DOCKER_IMAGE_PURGE}
## @audience private
## @stability evolving
## @replaceable no
function docker_image_maintenance_helper
{
declare id
declare tmptime
declare createtime
declare difftime
declare name
if [[ "${ROBOT}" = false ]]; then
return
fi
if [[ -z "$*" ]]; then
return
fi
for id in "$@"; do
tmptime=$(dockercmd inspect --format '{{.Created}}' "${id}" | cut -f1 -d. )
createtime=$(dockerdate_to_ctime "${tmptime}")
curtime=$(date "+%s")
((difftime = curtime - createtime))
if [[ ${difftime} -gt ${DOCKER_IMAGE_PURGE} ]]; then
echo "Attempting to remove docker image ${id}"
dockercmd rmi "${id}"
fi
done
}
## @description get sentinel-level docker images
## @audience private
## @stability evolving
## @replaceable no
## @param args
function docker_get_sentinel_images
{
#shellcheck disable=SC2016
dockercmd images \
| tail -n +2 \
| "${GREP}" -v hours \
| "${AWK}" '{print $1":"$2}' \
| "${GREP}" -v "<none>:<none>"
}
## @description Remove untagged/unused images
## @audience private
## @stability evolving
## @replaceable no
## @param args
function docker_image_maintenance
{
declare id
if [[ "${ROBOT}" = false ]]; then
return
fi
big_console_header "Removing old images"
dockercmd images
echo "Untagged images:"
#shellcheck disable=SC2046
docker_image_maintenance_helper $(dockercmd images --filter "dangling=true" -q --no-trunc)
echo "Apache Yetus images:"
# removing this by image id doesn't always work without a force
# in the situations that, for whatever reason, docker decided
# to use the same image. this was a rare problem with older
# releases of yetus. at some point, we should revisit this
# in the mean time, we're going to reconstruct the
# repostory:tag and send that to get removed.
#shellcheck disable=SC2046,SC2016
docker_image_maintenance_helper $(dockercmd images | ${GREP} -e ^yetus | grep tp- | ${AWK} '{print $1":"$2}')
#shellcheck disable=SC2046,SC2016
docker_image_maintenance_helper $(dockercmd images | ${GREP} -e ^yetus | ${GREP} -v hours | ${AWK} '{print $1":"$2}')
if [[ "${SENTINEL}" = false ]]; then
return
fi
echo "Other images:"
#shellcheck disable=SC2046
docker_image_maintenance_helper $(docker_get_sentinel_images)
}
## @description Perform pre-run maintenance to free up
## @description resources. With --jenkins, it is a lot
## @description more destructive.
## @audience private
## @stability evolving
## @replaceable no
## @param args
function docker_cleanup
{
docker_image_maintenance
docker_container_maintenance
}
## @description Determine the revision of a dockerfile
## @audience private
## @stability evolving
## @replaceable no
## @param args
function docker_getfilerev
{
${GREP} 'YETUS_PRIVATE: gitrev=' \
"${PATCH_DIR}/precommit/test-patch-docker/Dockerfile" \
| cut -f2 -d=
}
function docker_version
{
declare vertype=$1
declare val
declare ret
# new version command
val=$(dockercmd version --format "{{.${vertype}.Version}}" 2>/dev/null)
ret=$?
if [[ ${ret} != 0 ]];then
# old version command
val=$(dockercmd version | ${GREP} "${vertype} version" | cut -f2 -d: | tr -d ' ')
fi
echo "${val}"
}
## @description Start a test patch docker container
## @audience private
## @stability evolving
## @replaceable no
## @param args
function docker_run_image
{
declare dockerfilerev
declare baseimagename
declare patchimagename="yetus/${PROJECT_NAME}:tp-${DOCKER_ID}"
declare containername="yetus_tp-${DOCKER_ID}"
declare client
declare server
declare retval
declare elapsed
dockerfilerev=$(docker_getfilerev)
baseimagename="yetus/${PROJECT_NAME}:${dockerfilerev}"
# make a base image, if it isn't available
big_console_header "Building base image: ${baseimagename}"
start_clock
dockercmd build \
-t "${baseimagename}" \
"${PATCH_DIR}/precommit/test-patch-docker"
retval=$?
#shellcheck disable=SC2046
elapsed=$(clock_display $(stop_clock))
echo ""
echo "Total Elapsed time: ${elapsed}"
echo ""
if [[ ${retval} != 0 ]]; then
yetus_error "ERROR: Docker failed to build image."
add_vote_table -1 docker "Docker failed to build ${baseimagename}."
bugsystem_finalreport 1
cleanup_and_exit 1
fi
big_console_header "Building ${BUILDMODE} image: ${patchimagename}"
start_clock
# using the base image, make one that is patch specific
dockercmd build \
-t "${patchimagename}" \
- <<PatchSpecificDocker
FROM ${baseimagename}
LABEL org.apache.yetus=""
LABEL org.apache.yetus.testpatch.patch="tp-${DOCKER_ID}"
LABEL org.apache.yetus.testpatch.project=${PROJECT_NAME}
RUN groupadd --non-unique -g ${GROUP_ID} ${USER_NAME} || true
RUN useradd -g ${GROUP_ID} -u ${USER_ID} -m ${USER_NAME} || true
RUN chown -R ${USER_NAME} /home/${USER_NAME} || true
ENV HOME /home/${USER_NAME}
USER ${USER_NAME}
PatchSpecificDocker
retval=$?
#shellcheck disable=SC2046
elapsed=$(clock_display $(stop_clock))
echo ""
echo "Total Elapsed time: ${elapsed}"
echo ""
if [[ ${retval} != 0 ]]; then
yetus_error "ERROR: Docker failed to build image."
add_vote_table -1 docker "Docker failed to build ${patchimagename}."
bugsystem_finalreport 1
cleanup_and_exit 1
fi
if [[ "${DOCKER_ENABLE_PRIVILEGED}" = true ]]; then
DOCKER_EXTRAARGS+=("--privileged")
fi
if [[ -n "${CONSOLE_REPORT_FILE}" ]]; then
touch "${CONSOLE_REPORT_FILE}"
DOCKER_EXTRAARGS+=("-v" "${CONSOLE_REPORT_FILE}:/testptch/console.txt")
fi
if [[ -n "${DOCKER_MEMORY}" ]]; then
DOCKER_EXTRAARGS+=("-m" "${DOCKER_MEMORY}")
fi
client=$(docker_version Client)
server=$(docker_version Server)
dockerversion="Client=${client} Server=${server}"
# make the kernel prefer to kill us if we run out of RAM
DOCKER_EXTRAARGS+=("--oom-score-adj" "500")
DOCKER_EXTRAARGS+=("--cidfile=${PATCH_DIR}/cidfile")
DOCKER_EXTRAARGS+=(-v "${PWD}:/testptch/${PROJECT_NAME}")
DOCKER_EXTRAARGS+=(-u "${USER_NAME}")
DOCKER_EXTRAARGS+=(-w "/testptch/${PROJECT_NAME}")
DOCKER_EXTRAARGS+=("--env=BASEDIR=/testptch/${PROJECT_NAME}")
DOCKER_EXTRAARGS+=("--env=DOCKER_VERSION=${dockerversion} Image:${baseimagename}")
DOCKER_EXTRAARGS+=("--env=JAVA_HOME=${JAVA_HOME}")
DOCKER_EXTRAARGS+=("--env=PATCH_SYSTEM=${PATCH_SYSTEM}")
DOCKER_EXTRAARGS+=("--env=PROJECT_NAME=${PROJECT_NAME}")
DOCKER_EXTRAARGS+=("--env=TESTPATCHMODE=${TESTPATCHMODE}")
DOCKER_EXTRAARGS+=(--name "${containername}")
trap 'docker_signal_handler' SIGTERM
trap 'docker_signal_handler' SIGINT
if [[ ${PATCH_DIR} =~ ^/ ]]; then
dockercmd run --rm=true -i \
"${DOCKER_EXTRAARGS[@]}" \
-v "${PATCH_DIR}:/testptch/patchprocess" \
--env=PATCH_DIR=/testptch/patchprocess \
"${patchimagename}" &
else
dockercmd run --rm=true -i \
"${DOCKER_EXTRAARGS[@]}" \
--env=PATCH_DIR="${PATCH_DIR}" \
"${patchimagename}" &
fi
wait ${!}
cleanup_and_exit $?
}
## @description docker kill the container on SIGTERM
## @audience private
## @stability evolving
## @replaceable no
function docker_signal_handler
{
declare cid
cid=$(cat "${PATCH_DIR}/cidfile")
yetus_error "ERROR: Caught signal. Killing docker container:"
dockercmd kill "${cid}"
yetus_error "ERROR: Exiting."
cleanup_and_exit 143 # 128 + 15 -- SIGTERM
}
## @description Switch over to a Docker container
## @audience private
## @stability evolving
## @replaceable no
## @param args
function docker_handler
{
PATCH_DIR=$(relative_dir "${PATCH_DIR}")
docker_cleanup
determine_user
docker_run_image
}