#!/bin/bash # # Controls execution and configuration of TYPO3 in a container # ============================================================ # # For help, run this script without any arguments. # # Copyright F. Kasper 2019 # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # Constants REPO_SLUG=undecaf/typo3-dev T3_ROOTDIR=/var/www/localhost MARIADB_DATADIR=/bitnami/mariadb POSTGRESQL_DATADIR=/bitnami/postgresql # -------------------------------------------------------------------------- # Prints $USAGE and an optional error message to stdout or stderr # and exits with exit code 0 or 1, respectively. # # Arguments: # $1 (optional) error message: if specified then it is printed, and all # output is sent to stderr; otherwise $USAGE goes to stdout. # # Environment: # $USAGE usage information; how to preserve linefeeds in $USAGE: # USAGE=$(cat <&2 <<- EOT *** $1 *** EOT REDIR=">&2" EXIT_CODE=1 fi eval 'echo "$USAGE" '$REDIR exit $EXIT_CODE } # -------------------------------------------------------------------------- # Returns exit code 0 if the argument contains a '/'. # # Arguments: # $1 string to test # is_path() { local RE RE='.+/.+' [[ "$1" =~ $RE ]] } # -------------------------------------------------------------------------- # If the argument is a path, verify that at least the parent exists and that # it is an empty directory. # # Arguments: # $1 path of a working directory to bind-mount, or a volume name # # Calls: # is_path # verify_dir() { if is_path "$1"; then # At least the parent directory must exist [ -d $(dirname "$1") ] || usage "Working directory '$1' does not have a parent" # Mount point must be an empty directory [ -n "$(ls -A "$1" 2>/dev/null)" ] && usage "Working directory path '$1' is not an empty directory" fi } # -------------------------------------------------------------------------- # Canonicalizes a path and echoes it to stdout. # # Arguments: # $1 path to canonicalize # canonicalize() { ( cd "$1" pwd -P ) } # -------------------------------------------------------------------------- # Autocompletes a string and echoes it to stdout. Echoes the original # string if autocompletion could not be achieved. # # Arguments: # $1 string to autocomplete # $2, $3, ... # autocompleted strings # autocomplete() { local ARG="$1" shift for AC in $@; do if [ "${AC##$ARG}" != "$AC" ]; then echo "$AC" return fi done echo "$ARG" } # -------------------------------------------------------------------------- # Bind-mounts a working directory to a container volume so that files and # directories of that volume appear to be owned by the current user. # Does nothing if no path was given. # # Arguments: # $1 name of container volume # $2 working directory path # # Environment: # $T3_ENGINE # $MP_FORMAT # $SUDO_PREFIX # # Calls: # is_path # mount_volume() { if is_path "$2"; then if ! which bindfs &>/dev/null; then unset USAGE usage "*** bindfs not installed, see https://bindfs.org/ for information ***" fi local BIND_MP local VOL_MP local VOL_UID local VOL_GID # Locate the volume mount point of the container engine VOL_MP=$($T3_ENGINE volume inspect --format "$MP_FORMAT" "$1") \ || usage "Volume '$1' not found" # Acquire sudo authorization for what follows sudo --prompt $'\n*** sudo authorization is required for bind-mounting a working directory: ' --validate # Determine UID and GID of the volume owner # (options differ between Linux and BSD) VOL_UID=$($SUDO_PREFIX stat --format '%u' $VOL_MP 2>/dev/null) || VOL_UID=$($SUDO_PREFIX stat -f '%u' $VOL_MP) VOL_GID=$($SUDO_PREFIX stat --format '%g' $VOL_MP 2>/dev/null) || VOL_GID=$($SUDO_PREFIX stat -f '%g' $VOL_MP) # Create the directory if necessary mkdir -p "$2" BIND_MP=$(canonicalize "$2") sudo bindfs \ --map=$VOL_UID/$(id -u):@$VOL_GID/@$(id -g) \ $VOL_MP \ "$BIND_MP" \ && echo "Working directory '$BIND_MP' bind-mounted to volume '$1'" fi } # -------------------------------------------------------------------------- # Unmounts whatever may be bind-mounted to a container volume mount point. # Does nothing if nothing is mounted there. # # Arguments: # $1 name of container volume # # Environment: # $T3_ENGINE # $MP_FORMAT # unmount_volume() { # Find the container volume mountpoint local VOL_MP local LINE local RE VOL_MP=$($T3_ENGINE volume inspect "$1" --format=$MP_FORMAT) # {{- ...}} and {{... -}} do no work here, therefore: VOL_MP=$(echo $VOL_MP) # Find the directory bind-mounted to the container volume mountpoint RE='^/.+( +[0-9]+){4}% +(/.+)$' if LINE=$(df -P -l | grep --fixed-string "$VOL_MP ") && [[ "$LINE" =~ $RE ]]; then sudo --prompt $'\n*** sudo authorization is required for unmounting a working directory: ' \ umount "${BASH_REMATCH[2]}" \ && echo "Working directory '${BASH_REMATCH[2]}' unmounted from volume '$1'" fi } # -------------------------------------------------------------------------- # Stops a container if possible and logs this to stdout. Also logs if the # container was removed after being stopped. Will remove the container if # $REMOVE_OPTION is set to anything non-empty. # # Arguments: # $1 container name # # Environment: # $T3_ENGINE # $REMOVE_OPTION # stop_container() { if $T3_ENGINE kill --signal SIGINT "$1" &>/dev/null && $T3_ENGINE stop --time 5 "$1" &>/dev/null; then echo "Container '$1' stopped" # Remove the container(s) if so requested test -n "$REMOVE_OPTION" && $T3_ENGINE container rm "$1" &>/dev/null # Wait until the container was removed eventually sleep 1 $T3_ENGINE container inspect "$1" &>/dev/null || echo "Container '$1' removed" fi } # -------------------------------------------------------------------------- # Determine command [ -n "$1" ] && MSG="Unknown command: '$1'" || MSG="Missing command" case "$1" in # Run TYPO3 in a container, optionally with a database in an extra container run) USAGE=$(cat </dev/null; echo $'\n*** Command failed or interrupted, rolling back ***'; $0 stop -e '$T3_ENGINE' -n '$T3_NAME' --rm; exit 1" ERR SIGINT # Database container required? if [ -z "$T3_DB_TYPE" ]; then # No, just run the TYPO3 container set -x -e $T3_ENGINE run \ --detach \ --name "$T3_NAME" \ --hostname "$T3_HOSTNAME" \ --volume "$T3_ROOT:$T3_ROOTDIR" \ --publish $T3_PORT:80 \ $ENV_OPTIONS \ $HOST_IP_OPTION \ $REMOVE_OPTION \ $@ \ $REPO_SLUG${T3_TAG:+:$T3_TAG} \ >/dev/null else # Share the network stack between database and TYPO3 container set -x -e "$ENGINE_NAME" run \ --detach \ --name "${T3_NAME}-db" \ --volume "${DB_DATA}:/bitnami/${T3_DB_TYPE}" \ --publish $T3_PORT:80 \ --publish $DB_PORT:$DB_CONTAINER_PORT \ $HOSTNAME_OPTION_DB \ $DB_CREDENTIALS \ $REMOVE_OPTION \ $@ \ bitnami/${T3_DB_TYPE} \ >/dev/null "$ENGINE_NAME" run \ --detach \ --name "$T3_NAME" \ --network container:"${T3_NAME}-db" \ --volume "$T3_ROOT:$T3_ROOTDIR" \ $HOSTNAME_OPTION_T3 \ $ENV_OPTIONS \ $HOST_IP_OPTION \ $REMOVE_OPTION \ $@ \ $REPO_SLUG${T3_TAG:+:$T3_TAG} \ >/dev/null fi { set +x; } 2>/dev/null # disable command echoing and do not echo this command # Eventually bind-mount container volumes in userspace mount_volume "$T3_ROOT" "$T3_ROOT_MP" mount_volume "$DB_DATA" "$DB_DATA_MP" ;; stop) # Collect volume names from container(s) VOLNAMES= for CONTNAME in "$T3_NAME"{,-db}; do # Note: prior to Podman v1.4.3, the volume name was contained in property .Source, not .Name VOLNAMES="$VOLNAMES $($T3_ENGINE container inspect \ --format='{{range .Mounts}}{{if eq .Destination "'$T3_ROOTDIR'" "'$MARIADB_DATADIR'" "'$POSTGRESQL_DATADIR'"}}{{printf "%s " .Name}}{{end}}{{end}}' \ "$CONTNAME" 2>/dev/null)" done # Try to unmount whatever may be mounted at the mountpoint of each volume for VOLNAME in $VOLNAMES; do unmount_volume "$VOLNAME" done # Stop and eventually remove the container(s) for CONTNAME in "$T3_NAME"{,-db}; do stop_container "$CONTNAME" done ;; env) set -x $T3_ENGINE exec "$T3_NAME" setenv $HOST_IP_ENV $@ ;; composer) set -x $T3_ENGINE exec "$T3_NAME" composer $@ ;; mount) test -n "$DIR_PATH" || usage "Option '--mount' is required" mount_volume $(basename "$DIR_PATH") "$DIR_PATH" ;; unmount) test -d "$DIR_PATH" || usage "Option '--unmount' is required" sudo --prompt $'\n*** sudo authorization is required for unmounting a working directory: ' \ umount "$DIR_PATH" \ && echo "Working directory '$DIR_PATH' unmounted from volume '$(basename "$DIR_PATH")'" ;; *) usage "Unknown command: '$CMD'" ;; esac