#!/usr/bin/env bash # Copyright: (c) 2023, Ansible Project # Apache License, Version 2.0 (see LICENSE.md or https://www.apache.org/licenses/LICENSE-2.0) # This entrypoint script papers over a number of problems that manifest under different container runtimes when # using ephemeral UIDs, then chain-execs to the requested init system and/or command. It is an implementation # detail for the convenience of Ansible execution environments built by ansible-builder. # # If we're running as a legit user that has an entry in /etc/passwd and a valid and writeable homedir, we're all good. # # If the current uid is not in /etc/passwd, we'll attempt to add it, but /etc/passwd is often not writable by GID 0. # `ansible-builder` defaults to making /etc/passwd writable by GID0 by default for maximum compatibility, but this is # not guaranteed. Some runtimes/wrappers (eg podman, cri-o) already create an /etc/passwd entry on the fly as-needed, # but they may set the homedir to something inaccessible (eg, `/`, WORKDIR). # # There are numerous cases where a missing or incorrect homedir in /etc/passwd are fatal. It breaks # `async` in ansible-core, things like `echo ~someuid`, and numerous other software packages that assume a valid POSIX # user configuration. # # If the homedir listed in /etc/passwd is not writeable by the current user (supposed to be primary GID0), we'll try # to make it writeable (except `/`), or select another writeable home directory from `$HOME`, `/runner`, or `/tmp` and # update $HOME (and /etc/passwd if possible) accordingly for the current process chain. # # This script is generally silent by default, but some likely-fatal cases will issue a brief warning to stderr. The # envvars described below can be set before container init to cause faster failures and/or get tracing output. # options: # EP_BASH_DEBUG=1 (enable set -x) # EP_DEBUG_TRACE=1 (enable debug trace to stderr) # EP_ON_ERROR=ignore/warn/fail (default ignore) set -eu if (( "${EP_BASH_DEBUG:=0}" == 1 )); then set -x fi : "${EP_DEBUG_TRACE:=0}" : "${EP_ON_ERROR:=warn}" : "${HOME:=}" CUR_UID=$(id -u) CUR_USERNAME=$(id -u -n 2> /dev/null || true) # whoami-free way to get current username, falls back to current uid DEFAULT_HOME="/runner" DEFAULT_SHELL="/bin/bash" if (( "$EP_DEBUG_TRACE" == 1 )); then function log_debug() { echo "EP_DEBUG: $1" 1>&2; } else function log_debug() { :; } fi log_debug "entrypoint.sh started" case "$EP_ON_ERROR" in "fail") function maybe_fail() { echo "EP_FAIL: $1" 1>&2; exit 1; } ;; "warn") function maybe_fail() { echo "EP_WARN: $1" 1>&2; } ;; *) function maybe_fail() { log_debug "EP_FAIL (ignored): $1"; } ;; esac function is_dir_writable() { [ -d "$1" ] && [ -w "$1" ] && [ -x "$1" ] } function ensure_current_uid_in_passwd() { log_debug "is current uid ${CUR_UID} in /etc/passwd?" if ! getent passwd "${CUR_USERNAME}" &> /dev/null ; then if [ -w "/etc/passwd" ]; then log_debug "appending missing uid ${CUR_UID} into /etc/passwd" # use the default homedir; we may have to rewrite it to another value later if it's inaccessible echo "${CUR_UID}:x:${CUR_UID}:0:container user ${CUR_UID}:${DEFAULT_HOME}:${DEFAULT_SHELL}" >> /etc/passwd else maybe_fail "uid ${CUR_UID} is missing from /etc/passwd, which is not writable; this error is likely fatal" fi else log_debug "current uid is already in /etc/passwd" fi } function ensure_writeable_homedir() { if (is_dir_writable "${CANDIDATE_HOME}") ; then log_debug "candidate homedir ${CANDIDATE_HOME} is valid and writeable" else if [ "${CANDIDATE_HOME}" == "/" ]; then log_debug "skipping attempt to fix permissions on / as homedir" return 1 fi log_debug "candidate homedir ${CANDIDATE_HOME} is missing or not writeable; attempt to fix" if ! (mkdir -p "${CANDIDATE_HOME}" >& /dev/null && chmod -R ug+rwx "${CANDIDATE_HOME}" >& /dev/null) ; then log_debug "candidate homedir ${CANDIDATE_HOME} cannot be made writeable" return 1 else log_debug "candidate homedir ${CANDIDATE_HOME} was successfully made writeable" fi fi # this might work; export it even if we end up not being able to update /etc/passwd # this ensures the envvar matches current reality for this session; future sessions should set automatically if /etc/passwd is accurate export HOME=${CANDIDATE_HOME} if [ "${CANDIDATE_HOME}" == "${PASSWD_HOME}" ] ; then log_debug "candidate homedir ${CANDIDATE_HOME} matches /etc/passwd" return 0 fi if ! [ -w /etc/passwd ]; then log_debug "candidate homedir ${CANDIDATE_HOME} is valid for ${CUR_USERNAME}, but /etc/passwd is not writable to update it" return 1 fi log_debug "resetting homedir for user ${CUR_USERNAME} to ${CANDIDATE_HOME} in /etc/passwd" # sed -i wants to create a tempfile next to the original, which won't work with /etc permissions in many cases, # so just do it in memory and overwrite the existing file if we succeeded NEWPW=$(sed -r "s;(^${CUR_USERNAME}:(.*:){4})(.*:);\1${CANDIDATE_HOME}:;g" /etc/passwd) echo "${NEWPW}" > /etc/passwd } ensure_current_uid_in_passwd log_debug "current value of HOME is ${HOME}" PASSWD_HOME=$(getent passwd "${CUR_USERNAME}" | cut -d: -f6) log_debug "user ${CUR_USERNAME} homedir from /etc/passwd is ${PASSWD_HOME}" CANDIDATE_HOMES=("${PASSWD_HOME}" "${HOME}" "${DEFAULT_HOME}" "/tmp") # we'll set this in the loop as soon as we find a writeable dir unset HOME for CANDIDATE_HOME in "${CANDIDATE_HOMES[@]}"; do if ensure_writeable_homedir ; then break fi done if ! [ -v HOME ] ; then maybe_fail "a valid homedir could not be set for ${CUR_USERNAME}; this is likely fatal" fi # chain exec whatever we were asked to run (ideally an init system) to keep any envvar state we've set log_debug "chain exec-ing requested command $*" exec "${@}"