diff --git a/precise/host/hooks/install.d/60-docker.sh b/precise/host/hooks/install.d/60-docker.sh index 6e2411c..d8d5acb 100755 --- a/precise/host/hooks/install.d/60-docker.sh +++ b/precise/host/hooks/install.d/60-docker.sh @@ -3,33 +3,61 @@ ## Set NO_DOCKER_RESTART to prevent restarting docker after upgrade. ## Set ALLOW_DOCKER_CHANGE to allow change your docker version. ## Set TARGET_DOCKER_VERSION to force a version (use cautiously!) +exname=${0##*/} +if [ "$exname" == "bash" ]; then + ## probably launched through ``charm`` + if [ -n "$CHARM_NAME" ]; then + exname="charm/${CHARM_NAME}" + else + echo "Error: cannot determine script name" >&2 + echo " Please either run this script directly or through \`\`charm\`\`" >&2 + exit 1 + fi +fi +for bin in apt-get grep dpkg file lsb_release; do + if ! type -p "$bin" >/dev/null; then + echo "Error: \`\`$bin\`\` not found" >&2 + exit 1 + fi +done + +STATIC_DOCKER_URL="https://download.docker.com/linux/static/stable/x86_64" distro=$(lsb_release -is) release=$(lsb_release -rs) +unsupported= +target_version=${TARGET_DOCKER_VERSION:-24} -if [ -z "$TARGET_DOCKER_VERSION" ]; then - case "$distro" in - Debian) - case "$release" in - 10|11|12) target_version=24;; - *) target_version=19;; - esac - ;; - Ubuntu) - case "$release" in - 22.04) target_version=24;; - *) target_version=19;; - esac - ;; - *) target_version=17;; - esac -else - target_version="$TARGET_DOCKER_VERSION" +declare -A distro_supported_version=( + [Debian]="9 10 11 12" + [Ubuntu]="20.04 22.04" +) + + +if [ "$distro" == "Debian" ]; then + release=${release%%.*} fi +if [ -z "${distro_supported_version[$distro]}" ] || + [[ " ${distro_supported_version[$distro]} " != *" $release "* ]]; then + echo "Warning: possibly unsupported distribution/version $distro $release" >&2 + echo " Supported distributions and versions:" >&2 + for dist in "${!distro_supported_version[@]}"; do + echo -n " $dist: " >&2 + ## comma separated list + versions="${distro_supported_version[$dist]}" + versions="${versions// /, }" + echo "$versions" >&2 + done + unsupported=1 +fi + + +version_gt() { test "$(echo -e "$@" | tr " " "\n" | sort -V | head -n 1)" != "$1"; } + just_installed= -if ! type -p docker >/dev/null; then +if ! docker_bin=$(type -p docker); then echo "Installing docker..." type -p curl >/dev/null || apt-get install -y curl /dev/null; then just_installed=1 fi +is_static_docker() { + local docker_bin=$1 + # check if this is a link + [[ -L "$docker_bin" ]] && docker_bin=$(readlink -f "$docker_bin") + + ## XXXvlab: We need to handle a special case that happens on some systems + ## having installed 0k-docker package that divert the docker + ## binary to a shell script that then calls docker + + ## check if this a shell script (using file) + if file "$docker_bin" | grep -q shell; then + echo "INFO: using a shell script for docker in '$docker_bin'" >&2 + + ## is the script mentioning "/usr/bin/docker" ? + if grep -q '/usr/bin/docker' "$docker_bin"; then + echo "INFO: docker script is calling '/usr/bin/docker'" >&2 + docker_bin=/usr/bin/docker + else + echo "ERROR: cannot determine docker binary from script '$docker_bin'" >&2 + return 1 + fi + fi + + [[ "$(ldd "$docker_bin")" == *not\ a\ dynamic\ executable* ]] +} + +is_static_docker_install=0 +if is_static_docker "$docker_bin"; then + echo "INFO: docker version is static" >&2 + is_static_docker_install=1 +fi + read docker_client_version docker_server_version < <( docker version --format '{{.Client.Version}} {{.Server.Version}}' ) @@ -51,63 +111,282 @@ if [ "$docker_client_version" != "$docker_server_version" ]; then mismatch_client_server=1 fi -distrib_available_version="$(apt-cache madison docker-ce | - cut -f 2 -d \| | - grep "$target_version" | - head -n 1 | xargs echo)" +distrib_available_versions=$(apt-cache madison docker-ce | cut -f 2 -d "|" | sort -V) + +deb_version_to_docker_version() { + local deb_version=$1 + local docker_version + docker_version="${deb_version%%~*}" + docker_version="${docker_version%-*}" + docker_version="${docker_version#*:}" + echo "$docker_version" +} + +declare -A docker_version_to_deb_version=() +for deb_version in $distrib_available_versions; do + docker_version=$(deb_version_to_docker_version "$deb_version") + docker_version_to_deb_version["$docker_version"]="$deb_version" +done + +## expand docker version to one that is available in deb on the repository +candidate_deb_target_version=$( + echo "${!docker_version_to_deb_version[@]}" | + tr ' ' '\n' | grep "^${target_version}[^\d]" | sort -V | tail -n 1 +) + +if [ -z "$candidate_deb_target_version" ]; then + ## No deb package seems to satisfy the target version + + # Fetch the HTML content and filter out the versions + available_docker_static_versions=$( + curl -sL "$STATIC_DOCKER_URL" | grep -oP 'docker-\d+\.\d+\.\d+\.tgz' | + cut -d'-' -f2 | cut -d'.' -f1,2,3 | sort -uV + ) + candidate_target_version=$( + echo "$available_docker_static_versions" | + grep "^${target_version}" | sort -V | tail -n 1 + ) + if [ -z "$candidate_target_version" ]; then + echo "ERROR: no docker version matching '${target_version}' available" >&2 + echo " Available versions for $distro $release:" >&2 + all_versions=( + $( + { + echo "${!docker_version_to_deb_version[@]}" | tr ' ' '\n' + echo "$available_docker_static_versions" + } | cut -f 1,2 -d. | sort -uV + ) + ) + for version in "${all_versions[@]}"; do + candidate_deb_target_version=$( + echo "${!docker_version_to_deb_version[@]}" | + tr ' ' '\n' | grep "^${version}[^\d]" | sort -V | tail -n 1 + ) + if [ -n "$candidate_deb_target_version" ]; then + msg=" with deb" + else + msg=" with static version" + fi + printf " - %-10s %s\n" "$version" "$msg" >&2 + done + echo >&2 + echo " 1) You may need to contact your system administrator to upgrade this script." >&2 + echo " 2) Meanwhile, if you know what you are doing, you can set yourself a target version" >&2 + echo " by setting TARGET_DOCKER_VERSION." >&2 + exit 1 + fi + echo "INFO: target docker version $target_version is not supported by distrib" >&2 + echo " Binary static version $candidate_target_version of docker will be used" >&2 + + max_docker_version_available= + for docker_version in ${!docker_version_to_deb_version[@]}; do + if version_gt "$docker_version" "$target_version"; then + continue + fi + if version_gt "$docker_version" "$max_docker_version_available"; then + max_docker_version_available="$docker_version" + fi + done + if [ -z "$max_docker_version_available" ]; then + ## use lowest version available + echo "INFO: no inferior docker version available in deb repository" \ + "to satisfy target version $target_version" >&2 + max_docker_version_available=$( + echo "${!docker_version_to_deb_version[@]}" | + tr ' ' '\n' | + sort -V | + head -n 1 + ) + if [ -z "$max_docker_version_available" ]; then + echo "ERROR: no docker version available in deb repository" >&2 + exit 1 + fi + echo "INFO: using lowest docker version available in deb repository" >&2 + fi + + echo "INFO: closest docker version supported by $distro $release:" \ + "$(deb_version_to_docker_version "$max_docker_version_available")" >&2 + + target_deb_version="${docker_version_to_deb_version[$max_docker_version_available]}" + target_version=$candidate_target_version + echo "INFO: target maximal deb version: '$target_deb_version' (docker version: $( + deb_version_to_docker_version "$target_deb_version" +))" >&2 + need_static=1 +else + ## a deb package is available to satisfy the target version + + target_deb_version="${docker_version_to_deb_version[$candidate_deb_target_version]}" + target_version=$candidate_deb_target_version + echo "INFO: target docker deb version: '$target_deb_version' " \ + "(docker version: $candidate_deb_target_version)" >&2 + need_static=0 +fi + +current_docker_ce_package_version=$( + dpkg -s docker-ce | grep '^Version:' | cut -f 2 -d ' ' +) +current_docker_ce_cli_package_version=$( + dpkg -s docker-ce-cli | grep '^Version:' | cut -f 2 -d ' ' +) + +## Install deb version if required -cleaned_distrib_available_version="${distrib_available_version%%~*}" -cleaned_distrib_available_version="${distrib_available_version%-*}" -cleaned_distrib_available_version="${cleaned_distrib_available_version#*:}" if [ -z "$mismatch_client_server" ] && - [[ "$docker_client_version" == "$cleaned_distrib_available_version" ]]; then - ## current and target versions match, no upgrade needed - echo "INFO: recommended target docker version $docker_client_version already installed" >&2 - echo " To set a different target version, set TARGET_DOCKER_VERSION" >&2 - exit 0 + [[ "$current_docker_ce_cli_package_version" == "$target_deb_version" ]] && + [[ "$current_docker_ce_package_version" == "$target_deb_version" ]]; then + if [[ "$need_static" == 0 ]] && [[ "$is_static_docker_install" == 0 ]]; then + ## current and target versions match, no upgrade needed + echo "INFO: recommended target deb docker version $target_version already installed" >&2 + echo " To set a different target version, set TARGET_DOCKER_VERSION" >&2 + exit 0 + fi + ## Check static version and current version matches the target_version + if [[ "$need_static" == 1 ]] && [[ "$is_static_docker_install" == 1 ]] && + [[ "$target_version" == "$docker_server_version" ]]; then + ## current and target versions match, no upgrade needed + echo "INFO: recommended target deb and static docker version $target_version already installed" >&2 + echo " To set a different target version, set TARGET_DOCKER_VERSION" >&2 + exit 0 + fi fi ## Should we skip upgrade? if [ -z "$just_installed" ] && [ -z "$ALLOW_DOCKER_CHANGE" ]; then - if [ -n "$distrib_available_version" ]; then - if [ -z "$mismatch_client_server" ]; then - ## get current version for docker-ce debian package installed - current_debian_package_version="$(dpkg -s docker-ce | grep '^Version:' | cut -f 2 -d ' ')" - echo "INFO: docker recommended version available (current: $current_debian_package_version => recommended: $distrib_available_version)" >&2 + echo "WARN: You are not on the recommended setup (target docker version: $target_version)" >&2 + if [[ "$current_docker_ce_package_version" != "$target_deb_version" ]]; then + echo "INFO: docker-ce recommended deb version available" >&2 + echo " current: $current_docker_ce_package_version" >&2 + echo " recommended: $target_deb_version" >&2 + fi + if [[ "$current_docker_ce_cli_package_version" != "$target_deb_version" ]]; then + echo "INFO: docker-ce-cli recommended deb version available" >&2 + echo " current: $current_docker_ce_cli_package_version" >&2 + echo " recommended: $target_deb_version" >&2 + fi + if [[ "$need_static" != "$is_static_docker_install" ]]; then + if [[ "$need_static" == 1 ]]; then + echo "INFO: docker static version recommended to achieved version $target_version" >&2 else - current_docker_ce_package_version="$(dpkg -s docker-ce | grep '^Version:' | cut -f 2 -d ' ')" - current_docker_ce_cli_package_version="$(dpkg -s docker-ce-cli | grep '^Version:' | cut -f 2 -d ' ')" - echo "WARNING: docker recommended version available (recommended: $distrib_available_version)" >&2 - echo " And your client ($current_docker_ce_cli_package_version) and server ($current_docker_ce_package_version) versions differ." >&2 + echo "INFO: docker deb version recommended, fulfilled by deb version $target_deb_version" >&2 fi - echo " To change version, run this script with ALLOW_DOCKER_CHANGE=1" >&2 - else - echo "WARNING: your docker version does match target recommended version" >&2 - echo " And your distribution does not package target version." >&2 - echo " - Your distribution: $distro $release" >&2 - echo " - Current docker version: $docker_client_version" >&2 - echo " - Target recommended version: ${target_version}*" >&2 fi + echo "INFO: To change version, run this script with ALLOW_DOCKER_CHANGE=1" >&2 exit 0 fi -if [ -z "$distrib_available_version" ]; then - echo "ERROR: no docker version matching '${target_version}*' available for your distribution" >&2 - echo " Your distribution: $distro $release" >&2 - echo " Available versions:" >&2 - apt-cache madison docker-ce | cut -f 2 -d \| | sed -r 's/^/ -/g' >&2 - echo - echo " 1) You may need to contact your system administrator to upgrade this script." >&2 - echo " 2) Meanwhile, if you know what you are doing, you can set yourself a target version" >&2 - echo " by setting TARGET_DOCKER_VERSION." >&2 - exit 1 +install_static_version() { + local docker_version="$1" + local url="$STATIC_DOCKER_URL/docker-${docker_version}.tgz" + local service + + ## ensure shadowlocal is set up + + if ! [ -e /etc/profile.d/shadowlocal.sh ]; then + cat < /etc/profile.d/shadowlocal.sh || return 1 +## add /opt/merge/bin to PATH if not already there +if [[ ":$PATH:" != *":/opt/merge/bin:"* ]]; then + export PATH="/opt/merge/bin:\$PATH" fi +EOF + fi + + if ! [ -e /etc/systemd/system.conf.d/shadowlocal.sh ]; then + mkdir -p /etc/systemd/system.conf.d + cat < /etc/systemd/system.conf.d/shadowlocal.sh || return 1 +[Manager] +DefaultEnvironment=PATH=/opt/merge/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +EOF + systemctl daemon-reload + fi + + + ## install in /opt/apps/docker -apt-get install -y --allow-downgrades \ - docker-ce="$distrib_available_version" \ - docker-ce-cli="$distrib_available_version" &2 + mkdir -p "${base_install}"/bin && + ( + set -o pipefail + curl -sSL "$url" | tar -xz -C "${base_install}"/bin --strip-components=1 + ) || return 1 + + for service in docker:dockerd containerd:containerd; do + bin=${service#*:} + service=${service%%:*} + ## get the original ExecStart line + if ! orig_exec_start=$(grep -m 1 '^ExecStart=' /lib/systemd/system/${service}.service); then + echo "ERROR: cannot find ExecStart in /lib/systemd/system/${service}.service" >&2 + return 1 + fi + ## override the ExecStart line by changing only the binary + new_exec_start="$(echo "$orig_exec_start" | sed -r 's|ExecStart=[^ ]+|/opt/merge/bin/'"${bin}"'|')" + mkdir -p "${base_install}/etc/systemd/system/${service}.service.d" + mkdir -p "/etc/systemd/system/${service}.service.d" + cat < "${base_install}/etc/systemd/system/${service}.service.d/docker-static.conf" +[Service] +ExecStart= +ExecStart=$new_exec_start +EOF + ## override systemd config + ln -sf /opt/merge/etc/systemd/system/docker.service.d/docker-static.conf \ + /etc/systemd/system/docker.service.d/docker-static.conf + done + ## Create symlinks to /opt/merge/bin with /opt/apps/docker/bin/* + shadowlocal add docker || return 1 ## requires kal-scripts + + if [ -z "$NO_DOCKER_RESTART" ]; then + systemctl daemon-reload && + service containerd restart && + service docker restart + fi +} + +uninstall_static_version() { + local base_install=/opt/apps/docker + if [ -d "${base_install}" ]; then + rm -rfv "${base_install}" || return 1 + fi + find -L "/opt/merge/bin" -maxdepth 1 -type l -ilname "/opt/apps/docker"/\* -exec echo rm -v {} \; || return 1 + for service in docker containerd; do + rm -vf "/etc/systemd/system/${service}.service.d/docker-static.conf" || return 1 + done +} + +## if currently static and it doesn't need it anymore, uninstall +if [[ "$is_static_docker_install" == 1 ]] && [[ "$need_static" == 0 ]]; then + echo "INFO: uninstalling static docker version" >&2 + uninstall_static_version || exit 1 +fi + +## if current docker-ce debian version is not the target debian version, install it +if [[ "$current_docker_ce_package_version" != "$target_deb_version" ]]; then + echo "INFO: installing docker-ce deb version $target_deb_version" >&2 + apt-get install -y --allow-downgrades \ + docker-ce="$target_deb_version" &2 + apt-get install -y --allow-downgrades \ + docker-ce-cli="$target_deb_version" &2 + install_static_version "$target_version" || exit 1 +fi