#!/usr/bin/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 "${@}"
