# /bin/bash  syntax
# SCRIPT_PURPOSE: A function library for Dedicated User Environment (DUE)

# Copyright 2019,2020 Cumulus Networks, Inc.  All rights reserved.
#
#  SPDX-License-Identifier:     MIT

######################################################################
# Init variables
######################################################################

# Enable extended pattern matching
shopt -s extglob

# Documentation and changelog builds get the official version from here.
DUE_VERSION="3.0.0"

# Debian packages required for DUE to run
REQUIRED_PACKAGES=" docker.io rsync bsdutils git"

# Packages to support processor emulation (ARM builds, etc)
RECOMMENDED_PACKAGES=" binfmt-support qemu qemu-user-static "

# Get enough information to add the current user account to a container
USER_NAME=$(whoami)
USER_ID=$( id -u )
USER_GROUP_ID=$( id -g )
# Get the name of the group if it has to be created in the container
USER_GROUP_NAME=$( getent group "$USER_GROUP_ID" | cut -d: -f1 )
HOST_HOME_DIR=$( realpath ~/)
# default the login shell
LOGIN_SHELL="/bin/bash --login"

# Default to restricting results to containers that have been set up.
FILTER_SHOW_ONLY_DUE="TRUE"

# default tag to latest
DOCKER_IMAGE_TAG="latest"

# List of example images
KNOWN_IMAGES="
 debian:10
 debian:11
 ubuntu:18.04
 ubuntu:20.04
 arm32v5/debian:jessie
 arm32v7/debian:10
 arm64v8/debian:10
 arm64v8/debian:11
 ppc64le/debian:10
"

# Max number of containers per user.
# Sometimes people need a reminder to clean up.
# The config file will override this, provided a read works.

DUE_USER_CONTAINER_LIMIT=10

# expected directory for template patch files
SPECIFIC_TEMPLATES_DIR="templates"

#  Template directories may have sub directories that add additional
# files to the resulting image. These are specified usin sub-type
TEMPLATE_SUB_TYPE="sub-type"

# If the common-template directory is local to the directory where
# DUE is being run it will be used rather than the version that is
# installed in the system.
COMMON_TEMPLATE_DIR="${SPECIFIC_TEMPLATES_DIR}/common-templates"

# Default installed location for template files
SYSTEM_TEMPLATE_INSTALL_DIR="/usr/share/due"


if [ "$TOP_LEVEL_DIR" = "" ];then
    TEMPLATE_INSTALL_PATH="$SYSTEM_TEMPLATE_INSTALL_DIR"
else
    # Look to cwd for these files
    TEMPLATE_INSTALL_PATH="$TOP_LEVEL_DIR"
fi
COMMON_TEMPLATE_PATH=${TEMPLATE_INSTALL_PATH}/$COMMON_TEMPLATE_DIR

SPECIFIC_TEMPLATE_PATH=${TEMPLATE_INSTALL_PATH}/${SPECIFIC_TEMPLATES_DIR}
# User can override the template path, so at build time, USE_TEMPLATE_PATH
# is the variable that gets referenced.
USE_TEMPLATE_PATH="$COMMON_TEMPLATE_PATH"

# if --use-template sets this, it will ID the container
DUE_IMAGE_TYPE_LABEL="default-type"

# Put build steps here
BUILD_MERGE_DIR="$(pwd)/due-build-merge"

#
# Red Hat uses a different package manager, so abstract that out.
# Ex: sudo $PACKAGE_UPDATE  ; sudo $PACKAGE_INSTALL rsync
#
if [ -e /etc/redhat-release ];then
    OS_TYPE="RedHat"
    PACKAGE_UPDATE=" dnf check-update"
    PACKAGE_INSTALL=" dnf install "
    #
    # Defaults for Red Hat systems have nosuid set when Docker runs,
    # preventing the mount of the user's home directory.
    # There may be a better work around, but for now,
    #  default to running --privileged
    #
    DOCKER_SPECIFIC_ARGS=" --privileged "
else
    OS_TYPE="Debian"
    PACKAGE_UPDATE=" apt-get update "
    PACKAGE_INSTALL=" apt-get install "
fi
######################################################################
# Common utility functions
######################################################################

# Takes: Path to directory
# list a directory with an indent to make it easier to pick out of
# a stream of text going by.
function fxnListDirContents()
{
    fxnPP "Contents of: $1"
    ls -l "$1" | sed -e 's/^/      /g'
}


# List what can be built.
# If $1 = empty - then print everything
# If $1 = "JustDirs" then don't print example commands
# Else, use $1 as output filter to limit responses
function fxnListContainersForCreation()
{
    local foundImages
    local foundExamples
    local outputFormat="$1"

    if [ "$outputFormat" != "JustDirs" ];then
        echo "Example build environments:"
        echo "Image type             Create it with:"
        echo "----------------------+---------------------------------------------------------"

        # To simplify parsing, this counts on the example line in the README.md files being formatted
        # in exactly the same way for all, with everything up to the 'with:' getting deleted,
        # and the last field being --use-template, as that gets prepended to the example string
        # via awk to specify the type of container being created.
        foundExamples=$( find "${SPECIFIC_TEMPLATE_PATH}" \
                              -name README.md \
                              -exec grep "^Create" {} \; \
                             | grep -- --from \
                             | sed -e 's/^.*with://g' \
                             | awk '{printf "%-20s %s\n", $NF, $0}' | sort )
        echo ""

        if [ "$outputFormat" != "" ];then
            echo "Filtering output to contain [ $outputFormat ]"
            echo ""
            echo "$foundExamples" | grep "$outputFormat"
        else
            echo "$foundExamples"
        fi
        echo ""

    fi
    # filter any template directories
    # Directories should always have this lib file, so
    # it shouldn't confuse with anything else.
    foundImages=$( find "$BUILD_MERGE_DIR" \
                        -maxdepth 2  \
                        -name install-config-common.lib 2>/dev/null \
                       | sed -e 's#\/install-config-common.lib##g' )

    if [ "$foundImages" = "" ];then
        foundImages="No locally built images found in $BUILD_MERGE_DIR"
    else
        echo ""
        echo "Configured directories to build from."
        echo "Use $0 --create --build-dir <path>"
        echo "--------------------------------------------------------------------------------"
        echo "$foundImages"
    fi

}

# Print progress
function fxnPP()
{
    printf "==== %-66s ====\n"  "$1"
}

# A universal error checking function. Invoke as:
# fxnEC <command line> || exit 1
# Example:  fxnEC cp ./foo /home/bar || exit 1
function fxnEC ()
{

    # actually run the command
    "$@"

    # save the status so it doesn't get overwritten
    status=$?
    # Print calling chain (BASH_SOURCE) and lines called from (BASH_LINENO) for better debug
    if [ $status -ne 0 ];then
        echo "ERROR [ $status ] in ${BASH_SOURCE[1]##*/}, line #${BASH_LINENO[0]}, calls [ ${BASH_LINENO[*]} ] command: \"$*\"" 1>&2
    fi

    return $status
}

#
# Standardized error messaging
# Takes: error message to print as first argument
function fxnERR
{
    echo ""
    # Print script name, and line original macro was on.
    printf "ERROR at ${BASH_SOURCE[1]##*/} line ${BASH_LINENO[1]} :  %s\n" "$1"
    echo ""
}

# print a header to make information stand out
function fxnHeader
{
    echo ""
    echo " __________________________________________________________________________ "
    echo "|"
    echo "| $1"
    echo "|__________________________________________________________________________ "
    echo ""

}

# Print a standard warning message
function fxnWarn()
{
    echo ""
    echo "Warning: $1"
    echo ""
}
######################################################################
# Docker container manipulation functions
######################################################################



#
# Delete an existing Docker image
# Takes: value of $DELETE_TERM
# Does:
#  This creates a separate script the user has to run to do the delete,
#   to make sure they haven't mistyped anything
# Example: delete docker containers named 'none'
#          fxnDodDelete "none"
#
# List of targets to delete, for user review
DELETE_SCRIPT="delete_these_docker_images.sh"

function fxnDeleteImage()
{
    if [ "$DELETE_TERM" = "" ];then
        echo "Existing images"
        ${DOCKER_APP} images | sed -e 's/^[ \t]*//g' 
        echo ""
        echo "Pass a string to wildcard delete."
    fi

    echo "#/bin/bash" > "$DELETE_SCRIPT"
    echo "# Created by $0 $INVOKED_WITH on $(date)" >> "$DELETE_SCRIPT"
    chmod a+x "$DELETE_SCRIPT"

    ${DOCKER_APP} images  | sed -e 's/^[ \t]*//g' \
        | grep "$DELETE_TERM" \
        | awk '{printf "# Delete: %s Tag: %s Image ID %s, Created %s %s %s, Size %s \n REPLACE_DOCKER_APP rmi --force %s \n", $1, $2, $3, $4, $5, $6, $7, $3}' \
              >> "$DELETE_SCRIPT"
	# Set the applicatio 
	sed -i "s/REPLACE_DOCKER_APP/$DOCKER_APP/g" "$DELETE_SCRIPT"
	
    echo " ________________________________________________________________________"
    echo "|                                                                       "
    echo "| Run [ ./$DELETE_SCRIPT ]                                "
    echo "|   to delete the following images that matched \"$DELETE_TERM\"                   "
    echo "|________________________________________________________________________"

    echo ""
    grep "Delete" "$DELETE_SCRIPT" | sed -e 's/# Delete:/   /g'

    # echo all the text in one pass
    {
        echo ""
        # show what's left when the script is run
        echo "$DOCKER_APP images | sed -e 's/^[ \t]*//g' "
        echo ""
        # clean up after ourselves
        echo "rm ./$DELETE_SCRIPT"

        echo ""
    } >> "$DELETE_SCRIPT"
    if [ "$DO_DELETE_NOW" = "TRUE" ];then
        ./"$DELETE_SCRIPT"
    fi
} #fxnDeleteImage

#
# PreProcess functions create copies of the specified file in the directory that
#  the container will be made from, then replace the REPLACE_* terms with valid
#  values, specific to the container.

# Takes: Path to where the file will be written out
#        Optional argument to indicate non native architecture container: use qemu
# Does:  Generates a Dockerfile.create file in the container creation directory.
function fxnPreProcessDockerFile()
{
    local destDir="$1"
    local useQEMU="$2"
    local domainName
    # If there is an existing copy, do not overwrite.
    # user may have custom modifications.

    if [ -e  "$destDir"/Dockerfile.create ];then
        fxnPP "Not creating $destDir/Dockerfile.create as it exists"
    else

        domainName=$( dnsdomainname )
        if [ "$domainName" = "" ];then
            # Sometimes this comes back empty.
            domainName="no-domain-name"
        fi
        fxnPP "Creating $destDir/Dockerfile.create from [ $USE_TEMPLATE_PATH ]"
        cp "${USE_TEMPLATE_PATH}"/Dockerfile.template "$destDir"/Dockerfile.create

        # now edit the copy
        if [ "$FROM_IMAGE_TYPE" != "" ];then
            sed -i "s#REPLACE_IMAGE_FROM\$#$FROM_IMAGE_TYPE#g"                          "$destDir"/Dockerfile.create
            sed -i "s#REPLACE_DUE_IMAGE_TYPE_LABEL\$#$DUE_IMAGE_TYPE_LABEL#g"           "$destDir"/Dockerfile.create
            # if the maintainer/organization is still set to the default, change it to the current user.
            sed -i "s/yourmaintainer@your-organization.org/$(whoami)@${domainName}/" "$destDir"/Dockerfile.create

            # Embed description of the image
            sed -i "s#REPLACE_IMAGE_DESCRIPTION#\"$IMAGE_DESCRIPTION\"#g" "$destDir"/Dockerfile.create

            # Log the version of DUE that created this container.
            sed -i "s/REPLACE_DUE_CREATION_VERSION/${DUE_VERSION}/"                     "$destDir"/Dockerfile.create
            # If qemu is to be installed, uncomment the copy of it
            if [ "$useQEMU" != "" ];then
                fxnPP "Configuring Dockerfile to copy in qemu as the first step."
                sed -i "s/#REPLACE_DUE_INSTALL_QEMU//#g" "$destDir"/Dockerfile.create
            fi
        fi
    fi
}

# Takes: Path to where the file will be written out
#
# This file holds things a container user might want in their bashrc.
# For example, the current due-bashrc.template supports setting the
# PS1 prompt and a function that will print the active branch of
# a git directory.
#
# Due edits the container's /etc/bashrc.bashrc to source this file on
# user login.
# If the user wants any other .bashrc configuration, they'll have to
# source it manually.
function fxnPreProcessDueBashrc()
{
    local destDir="$1"

    # If there is an existing copy, do not overwrite.
    # user may have custom modifications.
    if [ -e  "$destDir"/etc/due-bashrc ];then
        fxnPP "Not creating $destDir/etc/due-bashrc as it exists"
    else
        fxnPP "Creating $destDir/etc/due-bashrc from [ $USE_TEMPLATE_PATH ]"
        fxnEC cp "${USE_TEMPLATE_PATH}"/filesystem/etc/due-bashrc.template "$destDir"/due-bashrc || exit 1
        # Default the sourcing of the /etc/due-bashrc file
        USE_DUE_BASHRC="TRUE"
        if [ "$USE_DUE_BASHRC" = "TRUE" ];then
            # if enabling the bashrc to run
            sed -i "s/REPLACE_ENABLE_DUE_BASHRC/TRUE/g" "$destDir"/due-bashrc
        fi

        # If NEW_PROMPT is set, put it in here, otherwise leave empty
        sed -i "s/DUE_REPLACE_PROMPT/$NEW_PROMPT/g" "$destDir"/due-bashrc

    fi

}

# Have the container identify itself on login
# Takes: Path to where the file will be written out
function fxnPreProcessDockerLoginMessage()
{
    local destDir="$1"

    # If there is an existing copy, do not overwrite.
    # user may have custom modifications.
    if [ -e  "$destDir"/etc/DockerLoginMessage ];then
        fxnPP "Not creating $destDir/etc/DockerLoginMessage as it exists"
    else
        fxnPP "Creating $destDir/etc/DockerLoginMessage from [ $USE_TEMPLATE_PATH ]"
        fxnEC cp "${USE_TEMPLATE_PATH}"/filesystem/etc/DockerLoginMessage.template "$destDir"/DockerLoginMessage || exit 1

        # now edit the copy (/s may be in the string. Deliniate sed with #s)
        if [ "$IMAGE_DESCRIPTION" != "" ];then
            sed -i "s#REPLACE_IMAGE_DESCRIPTION#$IMAGE_DESCRIPTION#g" "$destDir"/DockerLoginMessage
        fi
    fi
}

# Set up the script to be run BEFORE configuration
# Takes: Path to where the file will be written out
function fxnPreProcessPreInstallConfig()
{
    local destDir="$1"

    # If there is an existing copy, do not overwrite.
    # user may have custom modifications.
    if [ -e  "$destDir"/pre-install-config.sh ];then
        fxnPP "Not creating $destDir/pre-install-config.sh as it exists"
    else
        fxnPP "Creating $destDir/pre-install-config.sh from [ $USE_TEMPLATE_PATH ]"
        cp "${USE_TEMPLATE_PATH}"/pre-install-config.sh.template "$destDir"/pre-install-config.sh

        # now edit the copy

        if [ "$IMAGE_DESCRIPTION" != "" ];then
            sed -i "s/REPLACE_IMAGE_NAME/$NEW_IMAGE_NAME/g" "$destDir"/pre-install-config.sh
        fi
    fi
}


# Set up the script to be run AFTER configuration
# Takes: Path to where the file will be written out
function fxnPreProcessPostInstallConfig()
{
    local destDir="$1"

    # If there is an existing copy, do not overwrite.
    # user may have custom modifications.
    if [ -e  "$destDir"/post-install-config.sh ];then
        fxnPP "Not creating $destDir/post-install-config.sh as it exists"
    else
        # the script just invokes a function defined in install-config-common.lib, so there's nothing
        # to replace at the moment, but if there was, it would be here
        fxnPP "Creating $destDir/post-install-config.sh from [ $USE_TEMPLATE_PATH ]"

        fxnEC cp "${USE_TEMPLATE_PATH}"/post-install-config.sh.template "$destDir"/post-install-config.sh || exit 1
        # Make it executable as the template versions should not run and do not have their executable bits set.
        # Also, Lintian will flag this.
        chmod a+x "$destDir"/post-install-config.sh

        #
        # Any variable replacement via sed would happen here
        #
    fi
}

# Set up any of the common script utilities
# Takes: Path to where the file will be written out
function fxnPreProcessInstallConfigLib()
{
    local destDir="$1"

    # If there is an existing copy, do not overwrite.
    # user may have custom modifications.
    if [ -e  "$destDir"/install-config-common.lib ];then
        fxnPP "Not creating $destDir/install-config-common.lib as it exists"
    else
        fxnPP "Creating $destDir/install-config-common-lib from [ $USE_TEMPLATE_PATH ]"
        fxnEC cp "${USE_TEMPLATE_PATH}"/install-config-common-lib.template "$destDir"/install-config-common.lib || exit 1

        if [ "$IMAGE_DESCRIPTION" != "" ];then
            sed -i "s/REPLACE_IMAGE_NAME/$NEW_IMAGE_NAME/g" "$destDir"/install-config-common.lib
        fi
    fi
}



#
# Create a new Docker Image
# Takes: FROM_IMAGE_TYPE    - source image to build on
#        NEW_IMAGE_NAME     - Local name for image (my-stretch-build, for example)
function fxnMakeNewDockerImage()
{
    local imageFrom="$1"
    local imageName="$2"

    if [ "$2" = "" ];then
        # 3rd argument is optional
        fxnERR "Failed to pass enough arguments to fxnMakeNewDockerImage(). Exiting."
        exit 1
    fi

    if [ "$imageFrom" = "" ];then
        echo "ERROR: Must specify source image to use."
        # List all target directories as a hint.
        # common-template isn't a target directory
        #fxnListContainersForCreation
        echo "$KNOWN_IMAGES"
        exit 1
    fi

    # DOCKER_IMAGE_TAG will have been initialized to 'latest' by default,
    # or will have been overridden by the user.
    # Leaving this around in case there's a need to reference the base
    # container it was created from.
    # DOCKER_IMAGE_TAG="$( echo "$imageFrom" | tr ':' '-' | tr '/' '-' )"

    # Name of image and corresponding tag
    CONFIGURED_NAME_AND_TAG=due-${imageName}:${DOCKER_IMAGE_TAG}

    if [ "$(which ${DOCKER_APP})" = "" ];then
        if [ -e /.dockerenv ];then
            fxnERR "Docker or podman was not found, but you are already running in a container."
        else
            fxnERR "Docker or podman was not found! You should:"
            echo "1 - Install with:                      sudo $PACKAGE_UPDATE ; sudo $PACKAGE_INSTALL $REQUIRED_PACKAGES "
            echo "2 - Add yourself to the docker group:  sudo /usr/sbin/usermod -a -G docker $(whoami)"
            echo "3 - Consider the recommended packages: sudo $PACKAGE_INSTALL $RECOMMENDED_PACKAGES "
            echo "4 - Activate docker group membership:  log yourself out and log in again."
            echo "Exiting."
        fi
        exit 1
    fi

    #
    # Create a directory that will have a all the files that go into
    # the container, if it hasn't already been created.
    #
    if [ ! -e "${BUILD_MERGE_DIR}"/"${imageName}" ];then
        if [ "$MERGE_IN_TEMPLATE_PATH" != "" ];then
            # User has specified a template directory to pull in
            # This sets USE_TEMPLATE_PATH
            fxnGenerateTemplate "$MERGE_IN_TEMPLATE_PATH"
        fi

        #
        # Copy everything that isn't a template file from the template directory
        # to here. This allows the transport of user included files without
        # having to specifically handle them.
        # Template files, by their nature, will have to be dealt with on an
        # individual basis.
        #
        # --archive - recursively preserve everything
        # --verbose - show copy
        #
        fxnHeader "Creating directory to be used for container build at: [ $imageName ]"
        fxnPP "Copying non-template files from [ $USE_TEMPLATE_PATH ] to [ $imageName ]"
        if [ ! -e /usr/bin/rsync ];then
            fxnERR "rsync is not installed. Try: sudo $PACKAGE_UPDATE; sudo $PACKAGE_INSTALL rsync"
            exit 1
        fi
        fxnEC rsync \
              --archive \
              --verbose \
              --exclude=*.template \
              "${USE_TEMPLATE_PATH}"/ "${BUILD_MERGE_DIR}"/"${imageName}" || exit 1

        fxnListDirContents "${BUILD_MERGE_DIR}/${imageName}"

        #
        # Check for cross architecture containers
        # These can be run in emulation if they have QEMU as part of the container
        # Currently only supporting armel, but more can be added.

        # Map host architecture names to Docker names. Expect to add here as testable
        # cases show up.
        HOST_ARCH=$( uname -m )
        case $HOST_ARCH in
            "x86_64" )
                HOST_ARCH="amd64"
                ;;

            "aarch64" )
                HOST_ARCH="arm64"
                ;;

            "i686" )
                HOST_ARCH="i386"
                ;;

            * )
                fxnERR "Cannot map [ $HOST_ARCH ] to a Docker contianer type. Exiting."
                exit 1
                ;;
        esac

        # expect this to be amd64, arm, etc
        # Super convenient that qemu arm seems to handle all the variants (armhf, arm64, armel...)
        # So, has the image already been pulled?
        imageArchitecture=$( ${DOCKER_APP} inspect -f '{{.Architecture}}' "${imageFrom}" 2>/dev/null )
        if [ "$imageArchitecture" = "" ];then
            # Well, get a copy.
            fxnPP "[ $imageFrom ] not found. Pulling an image to check it's architecture before proceeding."
            fxnEC ${DOCKER_APP} pull "$imageFrom"  || exit 1
            imageArchitecture=$( ${DOCKER_APP} inspect -f '{{.Architecture}}' "${imageFrom}" )
            if [ "$imageArchitecture" = "" ];then
                # If the architecture can't be determined at this point, it is a bad sign,
                # but maybe not fatal. Continue and see what happens.
                fxnWarn "Failed to determine architecture of [ $imageFrom ]. Guessing same as host."
                imageArchitecture="$HOST_ARCH"
            fi
        fi

        #
        # Take advantage of a naming convention to figure out which qemu-<arch>-static to copy.
        #

        if [ "$HOST_ARCH" != "$imageArchitecture" ];then
            #
            # ...although the naming convention doesn't always match.
            # start stacking exceptions here for clarity.
            case $imageArchitecture in
                arm64 )
                    imageArchitecture="aarch64"
                    ;;
            esac

            fxnPP "Image architecture [ $imageArchitecture ] does not match host's [ $HOST_ARCH ]. Copying qemu-${imageArchitecture}-static in for emulation."
            # Have qemu-*-static in there so it can run
            fxnPP "Adding qemu-${imageArchitecture}-static to $imageName/filesystem/usr/bin/qemu-${imageArchitecture}-static."
            # qemu-user

            if [ ! -e /usr/bin/qemu-"${imageArchitecture}"-static ];then
                fxnERR "Host system must have qemu-${imageArchitecture}-static to run an ${imageArchitecture} container. Try: sudo $PACKAGE_INSTALL qemu-user-static."
                # Clean this up so that a re-run does not report success despite not having the emulation support.
                echo "Deleting partially built image at: ${BUILD_MERGE_DIR}/${imageName}"
                rm -rf "${BUILD_MERGE_DIR}/${imageName}"
                exit 1
            fi

            if [ ! -e /proc/sys/fs/binfmt_misc/qemu-${imageArchitecture} ];then
                fxnERR "Faied to find qemu-${imageArchitecture} under /proc/sys/fs/binfmt_misc. Packages may have to be installed or a restart is required."
                #               exit 1
            fi

            # Copy the host system's QEMU for image architecture to a location where it will be
            # installed in the container.
            if [ ! -e "${BUILD_MERGE_DIR}"/"$imageName"/filesystem/usr/bin ];then
                mkdir -p "${BUILD_MERGE_DIR}"/"$imageName"/filesystem/usr/bin
            fi
            fxnEC cp -a /usr/bin/qemu-${imageArchitecture}-static "${BUILD_MERGE_DIR}"/"$imageName"/filesystem/usr/bin/qemu-${imageArchitecture}-static || exit 1
            # Set this to pass to Dockerfile configuration so that qemu-static
            # will be present in the image _before_ Docker executes any commands in the image.
            QEMU_STATIC="qemu-${imageArchitecture}-static"

        fi

        #
        # Run preprocessing on all templates and put them in the
        # appropriate directory locations to configure the image
        fxnHeader "Replacing REPLACE strings in template files with supplied values."
        # Supply directory destination for output of processed template
        fxnPreProcessDockerLoginMessage "${BUILD_MERGE_DIR}"/"$imageName"/filesystem/etc

        # Changes to the bashrc
        fxnPreProcessDueBashrc          "${BUILD_MERGE_DIR}"/"$imageName"/filesystem/etc

        # Common functions used by pre/post install
        fxnPreProcessInstallConfigLib   "${BUILD_MERGE_DIR}"/"$imageName"

        # First script to run
        fxnPreProcessPreInstallConfig   "${BUILD_MERGE_DIR}"/"$imageName"

        # Last script to run
        fxnPreProcessPostInstallConfig  "${BUILD_MERGE_DIR}"/"$imageName"

        # Set parameters in the generated Docker file
        # If qemu needs to be installed, QEMU_STATIC will not be empty
        fxnPreProcessDockerFile         "${BUILD_MERGE_DIR}"/"$imageName" "$QEMU_STATIC"

        #
        # Put the current version of DUE in the incoming Dockerfile.config for
        # any future compatibility determinations
        #
        fxnPP "Embedding current version of DUE into the new image."
        {
            echo ""
            echo "#Version of DUE this was created with, for future compatibility"
            echo "LABEL DUECreationVersion=$DUE_VERSION"
            echo ""
        } >> "${BUILD_MERGE_DIR}"/"${imageName}"/Dockerfile.config

        #
        # Merge any information from Dockerfile.config into the Dockerfile.create
        # Put this right after DUE_INSERT_CONFIG
        sed -i "/# DUE_INSERT_CONFIG/ r ${BUILD_MERGE_DIR}/${imageName}/Dockerfile.config" "${BUILD_MERGE_DIR}"/"${imageName}"/Dockerfile.create

        fxnHeader "Created configuration directory [ $imageName ]"
        echo ""
        fxnPP "For additional configuration, modify the files in $imageName"
        echo ""

        DO_CREATE_IMAGE_NOW="TRUE"
        if [ "$DO_CREATE_IMAGE_NOW" = "TRUE" ];then
            # Create it all in one shot
            fxnHeader "Creating the new image with: $0 --create --build-dir ${BUILD_MERGE_DIR}/$imageName"
            $0 --create --build-dir "${BUILD_MERGE_DIR}/$imageName" --tag "$DOCKER_IMAGE_TAG"
        else
            fxnHeader " To create the new image, run: $0 --create --build-dir ${BUILD_MERGE_DIR}/$imageName"
        fi

    else
        # Directory was already there. Not overwriting it in case the user had
        # modifications.
        echo ""
        fxnPP "Confirmed directory $imageName exists."
        echo ""
        fxnPP "If there were changes in the /templates directory, delete this particular build with: "
        echo "      rm -r ${BUILD_MERGE_DIR}/$imageName ; rm -r ${BUILD_MERGE_DIR}/${templateName}"
        echo ""
        echo " Or delete all of ${BUILD_MERGE_DIR} with:"
        echo "     $0 --create --clean "
        echo ""
        fxnPP "To create the new image, run: $0 --create --build-dir ${BUILD_MERGE_DIR}/$imageName"
        echo ""
    fi

    #
    # Use the directory of files that was generated above to create the
    # new Docker image.
    #
    if [ "$DO_CREATE_NEW_IMAGE" = "TRUE" ];then

        # if this doesn't exist at this point, things have gone very wrong.
        fxnEC cd "${BUILD_MERGE_DIR}/$imageName" || exit 1

        if [ ! -e /var/run/docker.sock ];then
            fxnERR "Communication error with Docker!"
            result=$( dpkg -l | grep ${DOCKER_APP} )
            if [ "$result" != ""  ];then
                echo "Looks like ${DOCKER_APP} is installed:"
                echo "[ $result ]"
                fxnERR "Is it running?"
                exit 1
            else
                fxnERR "Has ${DOCKER_APP} been installed?"
                exit 1
            fi
        fi
        #
        # Create the image based off the contents of ./$imageName
        # This allows the user to test incremental changes without starting from scratch.
        fxnEC ${DOCKER_APP} build --no-cache=true --tag "${CONFIGURED_NAME_AND_TAG}" --file=./Dockerfile.create . || exit 1
    fi

    # Done.
    exit

} #fxnMakeNewDockerImage

#
# Save a running container as a new docker image.
# Docker image file is saved to disk and not stored locally.
# If the user wants to run it, they need to --import it.
#
#  User gets to pick from running containers.
#
# Uses: SAVE_CONTAINER_IMAGE_NAME  Name of resulting image.
#         This should have been set on the command line
function fxnSaveContainer()
{
    # Prepend due- here so that it is part of the name when/if
    # it gets imported back in.
    local exportImageName="due-$1"

    # choose from running containers
    fxnSelectContainer "running"
    # check for file overwrite
    if [ -e "${exportImageName}".tar ];then
        fxnERR "File: [ ${exportImageName}.tar ] already exists. Not overwriting.Exiting."
        exit 1
    fi

    # commit saves the container as an image, so all layers are captured.
    fxnPP "Saving container $SELECTED_CONTAINER_ID as image  ${exportImageName}"
    fxnEC ${DOCKER_APP} commit  "$SELECTED_CONTAINER_ID"  "${exportImageName}"

    # Now export the saved image
    fxnPP "Saving selected image as [ ${exportImageName}.tar ]"
    fxnEC ${DOCKER_APP} save "${exportImageName}" > "${exportImageName}".tar

    # Delete the local save. If you want to keep it as an image, rather
    # than a tar file, import it.
    fxnEC ${DOCKER_APP} rmi --force "$exportImageName"

    fxnPP "Exported container is in $(pwd) "
    ls -l | grep  "${exportImageName}".tar
    echo ""

}

# Export a copy of an already available Docker image as a file.
# This can be used with fxnDoImport to move an image between two machines.
# Uses: EXPORT_IMAGE_NAME  Name of resulting image.
#xo         This should have been set on the command line
function fxnDoExport()
{
    local exportFilter="$1"
    local exportImageName
    # choose from running containers

    fxnSelectContainer "export" $exportFilter

    # fxnSelectContainer will have set this.
	# If the name has a / in it, replace that with a -
	# Podman names do this, and we don't want it interpreted
	#  as a subdirectory.
    exportImageName="${SELECTED_CONTAINER_NAME//\//-}"	

    # check for file overwrite
    if [ -e ${exportImageName}.tar ];then
        fxnERR "File: [ ${exportImageName}.tar ] already exists. Not overwriting.Exiting."
        exit 1
    fi

    # Now export the saved image
    fxnPP "Saving selected image as [ ${exportImageName}.tar ]"
	# Use the ID associated with the image
	# Use 'save' rather than 'export' to save all layers.
	#    fxnEC ${DOCKER_APP} save  $SELECTED_CONTAINER_ID > ${exportImageName}.tar
    fxnEC ${DOCKER_APP} save  $SELECTED_CONTAINER_ID -o ${exportImageName}.tar	foo:bar

    fxnPP "Exported container is in $(pwd) "
    ls -l | grep  ${exportImageName}.tar
    echo ""

}

#
# Import a snapshot of a container.
# Takes: name of file to import
#
function fxnDoImport()
{
    local fileName="$1"
    local imageName
    local dueName

    # Stored image name will not have .tar* at the end
    imageName=${fileName%%.tar*}

    # trim any leading ./ that might specify the file
    imageName=${imageName#*./}
    # trim a leading due- if it happens to be there, because...
    imageName=${imageName#due-}
    # ...all imported images get due- prepended.
    # ...and we don't want to double that up
    dueName="due-${imageName}"

    fxnPP "Importing [ $fileName ] as [ $dueName ]"

	#
	# Use 'load' as it goes with 'save' - gathering all layers
	# Do not mix 'import' and 'export' with 'load' and 'save'
	# The former do not preserve all filesystem layers, and
	# mixing the two creates failures that only show at run time.
	# 
	fxnEC ${DOCKER_APP} load --input $fileName  || exit 1
	#tar -c "$fileName"  | docker image import - "$dueName"	
	#${DOCKER_APP} import $fileName $dueName
    # Show the user the result
    ${DOCKER_APP} images | sed -e 's/^[ \t]*//g' | grep "$dueName"

    fxnPP "Imported: $fileName as $dueName"
    echo ""
}

#
# Takes: url of repository to browse, with optional port number
# Does:  list contents of registry, and supplies access hints.
#        This is mostly informational and a Docker syntax reference,
#        as I access these things infrequently enough to have just
#        forgotten the syntax by the next time I need to use it.
#
function fxnBrowseRegistry()
{
    local result
    local access="insecure"
    local fullURL="$1"
    local browseURL
    local port=${1##*:}
    local images
    local examplePushImage
    local examplePullImage

    # if no port supplied, set Docker registry default of 5000
    if [ "$port" = "$1" ];then
        fullURL="$1:5000"
    fi

    browseURL="${fullURL}/v2/_catalog"
    # --silent - do not print curl progress header
    # filter unprintable characters as it will cause Bash to error out
    browsedWith="curl --silent --request GET http://${browseURL}"
    result=$( $browsedWith | tr -cd '[:print:]' )
    if [ "$result" = ""  ];then
        access="secure"
        browsedWith="curl --silent --request GET http://${browseURL} "
        result=$($browsedWith  | tr -cd '[:print:]' )
        if [ "$result" = "" ];then
            access="possibly insecure"
            # note the use of httpS here
            browsedWith=" curl --insecure --silent --request GET https://${browseURL}"
            result=$( $browsedWith | tr -cd '[:print:]' )
            if [ "$result" = "" ];then
                echo "ERROR! Failed to browse ${browseURL}"
                exit 1
            fi
        fi
    fi

    echo "Contents of $access Docker registry:[ $browseURL ]"
    echo "Browsed with: $browsedWith "
    echo "------------------------------------------------------------"
    # use command line JSON parser jq to make it pretty.
    echo ""
    images=$(   echo "$result"  | jq '.repositories[]' | tr -d \" )
    examplePullImage=$(echo "$images" | head -n 1)
    examplePushImage=$(${DOCKER_APP} images --format "{{.Repository}}:{{.Tag}}" | head -n 1)
    echo "$images"
    echo ""
    echo "------------------------------------------------------------"
    echo " Access examples:"
    echo ""
    echo "  Pull registry image  [ $examplePullImage ] from $fullURL "
    echo "    ${DOCKER_APP} image pull ${fullURL}/${examplePullImage} "
    echo ""
    echo "  Push a local image like [ $examplePushImage ] to $fullURL"
    echo "    ${DOCKER_APP} image tag $examplePushImage ${fullURL}/${examplePushImage};  docker image push ${fullURL}/${examplePushImage}"
    echo ""

}

######################################################################
# Container Runtime functions
######################################################################

#
# Have a menu to select containers
#
# Takes:
#   First argument: "start",   if choosing from existing images
#                   "running", if accessing running container
#   Second argument: additional term to filter on, like container type.
#
# Container ID returned by fxnSelectContainer
SELECTED_CONTAINER_ID=""
# Container name returned by fxnSelectContainer
SELECTED_CONTAINER_NAME=""

function fxnSelectContainer()
{

    local theContainers=""
    local entryLine=""

    local doOperation="$1"
    local imageName="$2"
    local filterByType
    local filterByTag

    # If the image name has a :tag at the end
    # Ex: myImage:myTag
    if [[ "$imageName"  == *:* ]];then
        # Split this up on the : so that  filtering matches type, then tag
        # Delete characters after :
        filterByType=${imageName%%:*}
        # Delete characters before :
        filterByTag=${imageName##*:}
    else
        filterByType="$imageName"
    fi
    local userIs="${USER_NAME}"

    # Default to having the user choose the image/container
    local enableRunChoice="TRUE"
    echo "-"

    if [ "$doOperation" = "" ];then
        fxnERR "No operation specified. Exiting."
        exit 1
    fi

    # if the image to use was passed on the command line, then don't bother with this
    if [ "$SELECTED_CONTAINER_ID" != "" ];then
        echo " - Not selecting images as IMAGE ID [ $SELECTED_CONTAINER_ID ] was specified."
        return 0
    fi

    #
    # Starting a new container. The selection process will be from existing images.
    #

    if [ "$doOperation" = "start" ] || [ "$doOperation" = "export" ];then
        #
        # Run a container
        #
		# Podman will add leading spaces that throw off start-of-line pattern matching. Trim them.
        theContainers=$( ${DOCKER_APP} images --format 'table  {{.Repository}}\t{{.Tag}}\t{{.Size}}\t{{.ID}}' | sed -e 's/^[ \t]*//g' )
        if [ $? != 0 ];then
            # As this is frequently the first access of the system's Docker daemon,
            # Fail gracefully if it's not installed.
            echo ""
            echo "Error: the Docker daemon not seem to be running. Is docker.io installed?  Exiting DUE."
            echo ""
            exit 1
        fi

        #
        # filter out containers the user isn't interested in
        #
        if [ "$filterByType" != "" ];then
            # Pass filter type and REPOSITORY header.
            # Match string against start of line, and leave a space at the end to
            # terminate it in case it would match a longer string.
            theContainers=$( echo "$theContainers" | grep "^$filterByType \|REPOSITORY" )
            # If a tag is present, filter the results using that, too.
            if [ "$filterByTag" != "" ];then
                theContainers=$( echo "$theContainers" | grep " $filterByTag \|REPOSITORY")
            fi
        fi

        if [ "$FILTER_SHOW_ONLY_DUE" = "TRUE" ];then
            theContainers=$( echo "$theContainers" | grep "due\|REPOSITORY" )
        fi

        #
        # if there's only one entry after all this, run it by default.
        #
        if [ "$( echo "$theContainers" |  wc -l  )" = 2 ];then
            enableRunChoice="FALSE"
        else
            if [ "$doOperation" = "export" ];then
                # Image export uses this menu
                echo "- Select one of the following containers to export"
            else
                # otherwise this is to run an image
                echo "- Select one of the following containers to run:"
                echo "-  (Skip this menu by using '--run-image <image name>')"
            fi
        fi
    fi


    #
    # Logging in to a running container.
    #
    if [ "$doOperation" = "running" ];then
        #
        # select existing container
        #
		# Podman doesn't print the header returning from a query. 
		if [ "${DOCKER_APP}" = "podman" ];then
			theContainers="NAMES              IMAGE    CONTAINER ID  COMMAND 
"
		fi

        if [ "$VERBOSITY_LEVEL" -ne 0 ];then
            theContainers+=$(     ${DOCKER_APP} ps --format 'table {{.Names}}\t{{.Image}}\t{{.ID}}\t{{.RunningFor}}\t{{.Status}}\t{{.Command}}')
        else
            # just the basics
            theContainers+=$(     ${DOCKER_APP} ps --format 'table {{.Names}}\t{{.Image}}\t{{.ID}}\t{{.Command}}')
        fi

        if [ "$filterByType" != "" ];then
            # pass filter type and REPOSITORY header (or NAMES for podman)
            theContainers=$( echo "$theContainers" | grep "$filterByType\|REPOSITORY \|NAMES " )
        fi

        if [ "$FILTER_SHOW_ONLY_DUE" = "FALSE" ];then
            echo "- Select one of the following active containers. Omit '--all' to see containers filtered by [ $userIs ]"
        else
            echo "- Select one of the following active containers filtered by [ $filterByType ]. Use '--all' to see every available container."
        fi
    fi

    echo " ----------------------------------------------------------------------------"

    if [ "$theContainers" = "" ];then
        echo "  None."
        exit 0
    fi


	# separate the head from the body to prevent it getting numbered		
	theHeader=$( echo "$theContainers" | head -n 1 )

    # set -n - clip first line
    # cat -n - put a number at the start
    theBody=$(  echo "$theContainers" \
                    | sed -n '1!p' \
                    | cat -n \
                    | sed -e 's/^    //g' -e 's/\t/  /g' )
    if [ "$theBody"  = "" ];then
        echo ""
        if [ "$FILTER_SHOW_ONLY_DUE" = "TRUE" ];then
            echo "  No DUE Docker images were found. "
            echo "  Howwever, the results were filtered by [ $userIs ]. Add --all to the command to see all Docker images."
            exit 1
        fi
        if [ "$filterByType" = "" ];then
            echo "  No Docker images were found. Try running [ ./due --create --help ]? "
        else
            echo "  No Docker images starting with [ $filterByType ] found. "
        fi
        echo ""
        exit 0
    fi

    # Filter by container type
    if [ "$filterByType" != "" ];then
        theBody=$( echo "$theBody" | grep "$filterByType" )
    fi

    if [ "$enableRunChoice" = "TRUE" ];then
        # Print the menu for the user
        echo " #  $theHeader"
        #    echo "$theBody" | more
        echo "$theBody"
        echo ""
        echo -n "- Enter number or q to quit > "

        read -r ENTRY

        case $ENTRY in
            q|Q|x|X|"" )
                echo "Exiting"
                exit 0
                ;;

            h|help|-h|--help )
                echo ""
                echo "Got [ $ENTRY ], so...executing '$0 run --help'  and exiting."
                echo ""
                fxnHelpRun
                exit 0
                ;;

            # At this point, only numbers are valid values.
            # If it is not, then exit gracefully before trying
            # to parse whatever this was later on.
            ''|*[!0-9]* )
                echo ""
                echo "Invalid entry! Must be one of: 'q', 'help', or a number. Exiting."
                echo ""
                exit 1
                ;;
        esac


    else
        # default to first entry if the user is not choosing.
        # This should be whats left from a filter operation
        ENTRY=1
    fi
	
	# If dealing with image files, starting them or exporting them...
    if [ "$doOperation" = "start" ] || [ "$doOperation" = "export" ];then
        # use the pre-filtered var, theBody

        # print ENTRY line only
        entryLine=$( echo "$theBody"  | sed -n "${ENTRY}p"  )
        if [ "$entryLine" = "" ];then
            fxnERR "Invalid entry [ $ENTRY ]. Exiting."
            exit 1
        fi
        SELECTED_CONTAINER_ID=$( echo "$entryLine" |  awk '{print$5}' )

        # In Ubuntu, Docker insists on putting a space between the size and units
        # Ex: 531 MB, not 531MB. If our IMAGE ID has a 'B' in it, use the
        # next column over.
        echo "$SELECTED_CONTAINER_ID" | grep "B" > /dev/null
        if [ "$?" = "0" ];then
            SELECTED_CONTAINER_ID=$( echo "$entryLine" |  awk '{print$6}' )
        fi
        #Get the tag
        SELECTED_CONTAINER_TAG=$( echo "$entryLine" |  awk '{print$3}' )

        # Get the name
        SELECTED_CONTAINER_NAME=$( echo "$entryLine" |  awk '{print$2}' )

        if [ "$SELECTED_CONTAINER_NAME" = "<none>" ];then
            SELECTED_CONTAINER_NAME="none"
        fi

        if [ "$VERBOSITY_LEVEL" -ne 0 ];then
            fxnPrintContainerLabels "$SELECTED_CONTAINER_NAME" "$SELECTED_CONTAINER_TAG"
        fi

    else
        SELECTED_CONTAINER_ID=$( echo "$theBody" | sed -n "${ENTRY}p" | awk '{print$4}' )
        SELECTED_CONTAINER_NAME=$( echo "$theBody" | sed -n "${ENTRY}p" | awk '{print$2}' )
    fi
}


# Provide a list and login option for open containers.
# Containers configured by DUE will have a container-create-user.sh script
# that will create a user account on the fly.
# Otherwise it tries to create a session as root with a shell
#
# Takes:
#     $1 is either:
#        start  <- instantiate new container from image and log into it
#        login  <-  selected container will be logged in to
#
#     $2 is optional. Can be the name of an image to start, otherwise user chooses.
#
function fxnDoLogin()
{
    local userNameArg=""
    local entryLine=""

    local imname=""
    local doOperation="$1"
    local useImage="$2"
    # If there is only 1 of something, set this to empty
    local plural="s"
    # Default to assuming it's a preconfigured due image they're trying to run.
    local isDUEImage="TRUE"

    # Docker image label describing type of image
    local DUEImageType=""

    # Image name minus offending characters
    local sanitizedContainerName=""

    # Users may forget they have containers running.
    # Remind them.
    local usersContainers


    # Docker image label hinting where the mount point should be
    # on an independently run command.
    # Ex: since built Debian packages are one directory up from
    # where the build is invoked, the container should mount the
    # host system directory one level above where the build is.
    local DUEMountHostDirsUp
    # Add --debug as first argument to turn on debugging in container-create-user.sh
    local executeProgram="/usr/local/bin/container-create-user.sh "

    # Docker settings for user interaction. Not used with --command
    local doInteractive=" --interactive --tty "


    if [ "$doOperation" = "" ];then
        fxnERR "Failed to pass running|start in $0 fxnDoLogin"
        exit 1
    fi

    #
    # Come back with SELECTED_CONTAINER_NAME set
    # Note FILTER_SHOW_ONLY_DUE = FALSE will show all containers
    #
    fxnSelectContainer "$doOperation" "$useImage"

    # Does this image have DUEImageType as a label?

    if [ "$1" = "start" ];then
        # If we are selecting an image to run as a container
		#        DUEImageType=$( ${DOCKER_APP} inspect -f '{{.Config.Labels.DUEImageType}}' "${SELECTED_CONTAINER_NAME}":"${SELECTED_CONTAINER_TAG}" )
		        DUEImageType=$( ${DOCKER_APP} inspect -f '{{.Config.Labels.DUEImageType}}' "${SELECTED_CONTAINER_NAME}":"${SELECTED_CONTAINER_TAG}" )
    else
        # Logging in to an already running container
        DUEImageType=$( ${DOCKER_APP} container inspect -f '{{.Config.Labels.DUEImageType}}' "${SELECTED_CONTAINER_ID}")
    fi


    if [ "$DUEImageType" = "" ];then
        # Don't know what that was, other than 'not good'
        fxnWarn "Failed to determine anything about the image/container, but will try anyway."
        DUEImageType="<no value>"
    fi

    #
    # for debug purposes, make no assumption about the container.
    if [ "$IGNORE_IMAGE_TYPE" = "TRUE" ];then
        fxnPP "Not parsing any labels out of container, as --ignore-type was passed."
        DUEImageType="<no value>"
    fi
    if [ "$DUEImageType" = "<no value>" ];then
        isDUEImage="FALSE"
        # Default to sh login as container-create-user.sh probably doesn't exist
        echo ""
        echo "#############################################################################"
        echo "#                                                                           #"
        echo "#    NOTE: This container does not have any DUE labels.                     #"
        echo "#    You may have to run DUE again with:                                    #"
        echo "#      --login-shell to /bin/sh                                             #"
        echo "#      --username    to root                                                #"
        echo "#      --userid      to 0                                                   #"
        echo "#                                                                           #"
        echo "#############################################################################"
        echo ""
        executeProgram="$LOGIN_SHELL"

        # if user name/ID has been specified, then use Docker commands
        # to set it in the running container
        if [ "$USER_NAME" != "" ];then
            SET_DOCKER_USER_ARGS+=" --name $USER_NAME "
        fi

        if [ "$USER_ID" != "" ];then
            SET_DOCKER_USER_ARGS+=" --user $USER_ID "
        fi
    else
        isDUEImage="TRUE"
    fi

    # If we are starting a new container
    if [ "$1" = "start" ];then
        if [ "$FILTER_SHOW_ONLY_DUE" = "TRUE" ];then
            usersContainers=$( ${DOCKER_APP} ps | grep -c " ${USER_NAME}_"  )
            # include this container they're starting
            usersContainers=$(( usersContainers + 1 ))
            if [ "$usersContainers" = 1 ];then
                # If there is only 1, don't reference it as multiple.
                # It's a small thing, but it always irritates me
                plural=""
            fi
            if [ $(( usersContainers > DUE_USER_CONTAINER_LIMIT )) = 1 ];then
                echo ""
                echo " Error: You have [ $usersContainers ] DUE container${plural} running, exceeding the system limit of [ $DUE_USER_CONTAINER_LIMIT ] on $(hostname)"
                echo ""
                echo " Stop some with:  $DUE_NAME --manage --stop"
                echo ""
                exit 2
            fi
        fi

        #
        # Figure out the particulars of any directory mounts
        #

        if [ "$isDUEImage" = "TRUE" ];then
            # If the container has a hint about mounting host directories relative
            # to the current working directory, take that into account.
            DUEMountHostDirsUP=$( ${DOCKER_APP} inspect -f '{{.Config.Labels.DUEMountHostDirsUp}}' "${SELECTED_CONTAINER_NAME}":"${SELECTED_CONTAINER_TAG}" )

            MOUNT_CURRENT_WORKING_DIRECTORY="$CURRENT_WORKING_DIRECTORY"
            for (( count=DUEMountHostDirsUP ; count > 0 ; count-- ))
            do
                # Back up a directory
                MOUNT_CURRENT_WORKING_DIRECTORY="$( dirname "$MOUNT_CURRENT_WORKING_DIRECTORY" )"
            done


            # DUE images have the container-create-user.sh script, so configure for that.
            if [ "$DO_DEBUG" = "TRUE" ];then
                # If container-create-user.sh gets --debug as the first argument, it will set -x
                executeProgram=" $executeProgram --debug "
            fi

            # If running docker in the container, it has to match the host
            # system's group ID
            dockerGID=$( getent group 'docker' | awk -F ':' '{print$3}' )
            executeProgram+=" --docker-group-id $dockerGID "

            # specify user and ID
            executeProgram+=" --username $USER_NAME --userid $USER_ID --groupid $USER_GROUP_ID --groupname $USER_GROUP_NAME "

            # Set any CONTAINER_SPECIFIC_ARGS
            fxnSetContainerSpecificArgs "$DUEImageType"

        fi
        # Generate a unique hostname
        # Replace any / in name with - as Docker will complain.
        sanitizedContainerName=${SELECTED_CONTAINER_NAME/\//-}

        USE_HOSTNAME="${USER_NAME}_${sanitizedContainerName}_$(date +%s)"

        # Set the hostname to the container type. As this will
        # probably show up in the user prompt since DUE can't
        # override the .bashrc sourced from the user's home directory.
        HOSTNAME_ARGS=" --hostname $sanitizedContainerName "

        # If not otherwise specified
        # make sure the container mounts something local

        #
        # Local file systems the container can mount.
        #
        if [ "$USE_HOMEDIR" = "" ];then
            ADDITIONAL_MOUNTS+=" --volume $(realpath ~/):/home/$USER_NAME"
        else
            echo "$USE_HOMEDIR" | grep -q "/"
            if [ $? = 0 ];then
                ADDITIONAL_MOUNTS+=" --volume ${USE_HOMEDIR}:/home/$USER_NAME"
            fi
            # not having an absolute path = use container home dir.
        fi

        if [ "$CURRENT_WORKING_DIRECTORY" != "" ];then
            # If we are making the inside of the container have the
            # same path as the directory this was invoked in, do it here.
            ADDITIONAL_MOUNTS+=" --volume $MOUNT_CURRENT_WORKING_DIRECTORY:$MOUNT_CURRENT_WORKING_DIRECTORY"
            # Put the user in the same equivalent directory in the container.
            COMMAND_LIST=" cd $CURRENT_WORKING_DIRECTORY ; $COMMAND_LIST"
        fi

        #
        # Check for mounting different directories into the same place in the container
        # Put --volume one per line
        # Trim white space
        # Filter any identical requests (those won't bother Docker, but do break parsing here)
        # Split out the container mount points after :
        # Count any duplicates there
        # ..and print those duplicates
        duplicates=$( echo $ADDITIONAL_MOUNTS | sed -e 's/--volume/\n/g' | tr -d " " | uniq | awk -F ":" '{print$2}'| uniq -cd | awk '{print$2}')
        if [ "$duplicates" != "" ];then

            echo "Error! Multiple host directories mapping to [ $duplicates ] in the container."
            echo "Docker command line:"
            echo "$ADDITIONAL_MOUNTS" | sed -e 's/--volume/\n--volume/g'
            echo ""
            echo " Note: some containers (debian package builds) mount the host system above the current working directory."
            echo " Solutions:  Add a directory to change the path?"
            echo "             Move the current working directory elsewhere?"
            #
            # check for a redefine of the home directory and use of the original home directory.
            # If the user's home directory was remapped, there is probably a good reason.
            # (like it's on an NFS mount and weird permissions bugs present.)
            #
            echo "$duplicates" | grep -q "/home/$USER_NAME"
            if [ $? = 0 ];then
                if [ "$USE_HOMEDIR" != "" ];then
                    echo "             Note: your home directory is configured to be [ $USE_HOMEDIR ]"
                    echo "                   So you shouldn't use directories under  [ /home/$USER_NAME ]"
                fi
            fi
            echo " Exiting."
            exit 1
        fi

        #
        # Does the container specify any host directories it would like to mount?
        # This is handy for sharing host installed tools or libraries without
        # bloating the container, and every instance of it.
        # The container's Dockerfile.config would have a:
        #   LABEL DUEMountHostDirectories=/path1,/path2,/path3, etc to mount
        #
        MountHostDirectories=$( ${DOCKER_APP} inspect -f '{{.Config.Labels.DUEMountHostDirectories}}' "${SELECTED_CONTAINER_NAME}":"${SELECTED_CONTAINER_TAG}" )
        if [ "$MountHostDirectories" != "<no value>" ];then
            # Save the current field separator (probably return)
            saveIFS=$IFS
            # Because it is now a ','
            IFS=,
            # use a 'here string' to parse the string like a file
            read -ra theDirs <<< "$MountHostDirectories"

            for dir in "${theDirs[@]}"; do
                if [ -d "$dir" ];then
                    fxnPP "Mounting host directory [ $dir ] in container, as suggested by container."
                    ADDITIONAL_MOUNTS+=" --volume $dir:$dir "
                elif [ -S "$dir" ];then
                    # The Docker socket is a less obvious mount case...
                    fxnPP "Mounting host Socket [ $dir ] in container, as suggested by container."
                    ADDITIONAL_MOUNTS+=" --volume $dir:$dir "
                else

                    # host system doesn't have a directory the container wants to
                    # mount. Warn and proceed.
                    fxnWarn "Host system does not have directory [ $dir ] requested by container. Continuing..."
                fi
            done
            # restore the field separator to what it was.
            IFS=$saveIFS
        fi

        if [ "$CONTAINER_MOUNT_DIR" = "" ];then
            # Specify where the host directory is mounted in the container
            CONTAINER_MOUNT_DIR="$DEFAULT_CONTAINER_MOUNT_BUILD_DIR"
        fi

        if [ "$DO_RUN_BACKGROUND" = "TRUE" ];then
            # default to whatever the container's default is.
            executeProgram=""
            doInteractive=""
        fi

        #
        # If the invocation is not a login, but a command to run in the
        # container, handle that here.
        # Keep in mind:
        #  The invoking user's home directory will be mounted, for access
        #  to any configuration files, or ssh keys
        #
        #  The path to the directory the command was invoked in is
        #  created and mounted in the container. So the command will
        #  always start with a 'cd' to that directory.
        #

        if [ "$COMMAND_LIST" != "" ];then
            # The user the command will run as has already been set
            # Pass the actual command here
            if [ "$isDUEImage" = "TRUE" ];then
                # This container starts with due-, so it should have
                # container-create-user.sh installed, which will run the
                # command as the user.
                executeProgram+=" --command $COMMAND_LIST "
            else
                # container-create-user.sh may not be installed, best to
                # just run it however we log in
                # This is a command, not a log in, so add -c
                executeProgram+=" -c "
                # Quote this here so it gets passed to the -c coherently
                # when it runs below.
                cmdList="$COMMAND_LIST "
            fi

            # if this is a command, eliminate the --interactive and --tty options
            doInteractive=""
        fi

        # assign a name
        if [ "$SET_CONTAINER_NAME" != "" ];then
            # User is overriding defaults with --container-name
            imname="$SET_CONTAINER_NAME"
        else
            imname="${USE_HOSTNAME}"
        fi


        # Note that any variables here that are not set will evaluate to
        # blank space and not be parsed as arguments.

        # Skip network configuration for now
        #                --network=\"host\" \
            # Pass any arguments that were set to go directly to docker
        # Pass any arguments that may be container specific
        # be  interactive, or not
        # use --rm to delete the container on exit
        # Mount any additional host directories
        # Specify the user to Docker
        # Set container host name
        # Specify the container
        # Pass the command to execute
        runCMD="$DOCKER_SPECIFIC_ARGS \
                $CONTAINER_SPECIFIC_ARGS \
                $doInteractive \
                --rm \
                $ADDITIONAL_MOUNTS \
                $SET_DOCKER_USER_ARGS \
                --name $imname \
                $userNameArg \
                ${HOSTNAME_ARGS} \
                ${SELECTED_CONTAINER_ID} \
                 $executeProgram"

        # Remove spaces for readability when echoing.
        # put one argument per line for readability
        # and make sure there are \ so the command can be cut and pasted
        formatRunCMD=$( echo $runCMD  | sed -e 's/                / /g' | sed -e 's# --#\\ \n     --#g' )

        #
        # Let the user know what DUE is doing behind the scenes
        #

        echo " ----------------------------------------------------------------------------"
        echo " ------ : start and log in to [ $SELECTED_CONTAINER_ID ] with this command:      -- "
        echo "   ${DOCKER_APP} run $formatRunCMD"

        echo ""
        echo " ----------------------------------------------------------------------------"
        echo " ------ Host file systems are mounted as follows: ---------------------------"
        echo " -"
        # Put this on two lines and delete the starting space newline..
        echo "$ADDITIONAL_MOUNTS" | sed -e 's/--volume/\n -   --volume/g' | sed -e '/^ $/d'

        if [ "$usersContainers" != "" ];then
            # Only limit this for DUE containers.
            echo " - "
            echo " - You now have  [ $usersContainers ] DUE container${plural} running on $(hostname)."
            echo " - User limit is [ $DUE_USER_CONTAINER_LIMIT ]"
        fi
        echo " -"
        echo " ----------------------------------------------------------------------------"
        echo " -------- Type 'exit' to leave.  Docker Container Output Follows ------------"

        echo ""

        if [ "$cmdList" != "" ];then
            # even the empty "" here will trip up a shell login, but they are
            # needed if cmdList is set to anything. Hence two cases.
            ${DOCKER_APP} run $runCMD "$cmdList"
        else
            ${DOCKER_APP} run $runCMD
        fi

    else

        if [ "$USER_NAME" != "" ];then
            userNameArg=" --user $USER_NAME "
        fi
        echo "Logging in to running  [ $SELECTED_CONTAINER_ID ] with: "
        echo " ___________________________________________________________________________"
        echo "|"
        echo "|  ${DOCKER_APP} exec $doInteractive $userNameArg ${SELECTED_CONTAINER_ID}  $LOGIN_SHELL"
        echo "|___________________________________________________________________________"
        echo ""
        echo ""
        echo " ---------------------------------------------------------------------------"
        echo " -             Logging in to container. Type 'exit' to leave.              -"
        echo " ------------------- Docker Container Output Follows -----------------------"
        # all output from here on down is from the container session, rather than the
        # launch from DUE

        if [ "$DO_DEBUG" = "TRUE" ];then
            # If this is debug, make few assumptions about the container's ability to handle
            # logins. Just start a specified shell.
            ${DOCKER_APP} exec $doInteractive  $userNameArg "${SELECTED_CONTAINER_ID}" $LOGIN_SHELL
        else
            if [ "$OS_TYPE" = "RedHat" ];then
                ${DOCKER_APP} exec $doInteractive --user "$USER_NAME"  "${SELECTED_CONTAINER_ID}" /bin/bash --login
            else
                # Log into container as root, then set ID to user. This executes .bashrc configs and
                # feels more like a real login.
                ${DOCKER_APP} exec $doInteractive --user root "${SELECTED_CONTAINER_ID}" /bin/login -p -f "$USER_NAME"
            fi
        fi
    fi
    # User typed 'exit' or something blew up?
    return $?

}


#
# template functions.
# Templates are used to create the directory that a container is created from.
# They allow replacement of parameters (like those in the Dockerfile.create )
# to create a class of build containers.
# The ./templates/common-templates directory holds files that are used as a starting point.
#
# Creation flow:
#  Template File(s) -> Directory with name of image to create -> create image

function fxnGenerateTemplate()
{
    local name
    local templateName

    # Hold any intermediate directories that are split by having
    # /templates/ in the path, so their contents can be included too
    # ex: ./templates/foo-parent/templates/foo-child
    local subTypeDirPaths=""
    # Hold a path that contains /templates/
    local partial="$MERGE_IN_TEMPLATE_PATH"
    # While still finding /templates/ in the path
    local parseTemplates="TRUE"

    #   name="$( basename "$MERGE_IN_TEMPLATE_PATH" )"
    name="$NEW_IMAGE_NAME"
    templateName="${name}-template-merge"

    #
    # Create a work directory to hold all build output in
    #
    if [ ! -d "$BUILD_MERGE_DIR" ];then
        fxnHeader "Creating work directory $BUILD_MERGE_DIR"
        fxnEC mkdir -p "$BUILD_MERGE_DIR" || exit 1
    fi

    if [ -e "${BUILD_MERGE_DIR}"/"${templateName}" ];then
        fxnWarn " [ ${BUILD_MERGE_DIR}/$templateName ] directory already exists. Skipping generation."
        return 0
    fi
    fxnHeader "Copying $COMMON_TEMPLATE_PATH to ${BUILD_MERGE_DIR}/$templateName"
    # here -template will stick with the directories and get filtered

    # Copy over the common templates all containers have
    # --preserve Keep mode,owner,timestamp, etc
    fxnEC cp  --recursive --preserve "$COMMON_TEMPLATE_PATH" "${BUILD_MERGE_DIR}"/"$templateName" || exit 1
    fxnListDirContents "${BUILD_MERGE_DIR}/$templateName"


    while [ "$parseTemplates" = "TRUE" ];do
        # Since the deepest directory under /templates/ is parsed
        # first but its contents should be applied last, prepend
        # the shorter directory paths in the array, so that,
        # when applied, the files deepest in the tree override
        # the files above them.
        # Each directory in the path named 'sub-type' indicates a new
        # group of files to be copied in. So:
        #  due/templates/foo/sub-type/bar/sub-type/baz
        # Results in a final merged directory of files that are:
        #  common-templates+(files from foo)+(files from bar)+(files from baz)
        # ..written in that order, so that files from 'baz' will replace
        #  any identically named files provided by bar, foo, or common-templates.
        subTypeDirPaths="${partial} ${subTypeDirPaths}"
        partial=${partial%/${TEMPLATE_SUB_TYPE}/*}

        # Out of /sub-type/  yet?
        if [[ $partial != *${TEMPLATE_SUB_TYPE}* ]];then
            # No more embedded template directories.
            parseTemplates="FALSE"
            # Store the top level directory now that sub-directories
            # are done.
            subTypeDirPaths="${partial} ${subTypeDirPaths}"
        fi
    done

    for dir in ${subTypeDirPaths[@]}; do
        fxnHeader "Merging in [ $name ] specific files from ${dir} to ${BUILD_MERGE_DIR}/$templateName"
        # Copy the data a link points to with --copy-links
        # preserve permissions, but DON'T copy anything named 'templates'
        fxnEC rsync --recursive --perms --times --group --owner --copy-links \
              --exclude "/${TEMPLATE_SUB_TYPE}"  \
              "${dir}"/* "${BUILD_MERGE_DIR}"/"${templateName}"/ || exit 1

        fxnListDirContents "${BUILD_MERGE_DIR}/$templateName"
    done

    # Rather than pull files from the common-templates to create the
    # configuration directory for the docker image, use this merged one
    USE_TEMPLATE_PATH="${BUILD_MERGE_DIR}/${name}-template-merge"

}


#
# Configuration file code
#
# System wide config file
DUE_CONFIG_FILE_PATH="/etc/due/due.conf"

# Personal config file
DUE_HOME_CONFIG_DIR=~/.config/due
DUE_HOME_CONFIG_FILENAME=due.conf
DUE_HOME_CONFIG_ABSOLUTE_PATH=${DUE_HOME_CONFIG_DIR}/${DUE_HOME_CONFIG_FILENAME}


# Configuration file to hold user specific overrides
# This will copy the version installed in $DUE_CONFIG_FILE_PATH as a starting point,
# and put a copy in the user's real home directory.
function fxnCreateConfigFile()
{

    if [ ! -e "$DUE_HOME_CONFIG_ABSOLUTE_PATH" ];then
        fxnPP "Making directory $DUE_HOME_CONFIG_DIR"
        fxnEC mkdir -p "$DUE_HOME_CONFIG_DIR" || exit 1
    fi

    if [ -e "$DUE_HOME_CONFIG_ABSOLUTE_PATH" ];then
        fxnPP "$DUE_HOME_CONFIG_ABSOLUTE_PATH already exists. Doing nothing."
    else

        if [ ! -e "$DUE_CONFIG_FILE_PATH" ];then
            # DUE has not been installed as a package.
            # Assume execution from a downloaded source directory
            fxnPP "Copying ./$DUE_CONFIG_FILE_PATH $DUE_HOME_CONFIG_ABSOLUTE_PATH"
            fxnEC cp ./"$DUE_CONFIG_FILE_PATH" "$DUE_HOME_CONFIG_ABSOLUTE_PATH" || exit 1
        else
            fxnPP "Copying $DUE_CONFIG_FILE_PATH $DUE_HOME_CONFIG_ABSOLUTE_PATH"
            fxnEC cp "$DUE_CONFIG_FILE_PATH" "$DUE_HOME_CONFIG_ABSOLUTE_PATH" || exit 1
        fi
        # Pix or it didn't happen.
        echo "ls -l $DUE_HOME_CONFIG_DIR"
        ls -l $DUE_HOME_CONFIG_DIR
    fi

}

#
# Read any user set configuration, and provide config file under ~/.config
#

# Save for later if printing --verbose.
# Default to user's due.conf file
CONFIG_FILE_PATH="$DUE_HOME_CONFIG_ABSOLUTE_PATH"

function fxnReadConfigFile()
{
    # store variable name / value pairs
    local configVars
    local homeDir
    local maxContainers

    # If the user's local DUE config does not exist
    if [ ! -e "$DUE_HOME_CONFIG_ABSOLUTE_PATH" ];then
        if [ -e "$CONFIG_FILE_PATH" ];then
            # Use the system wide file if it exists ( via Debian package install)
            CONFIG_FILE_PATH="$DUE_HOME_CONFIG_ABSOLUTE_PATH"
        else
            # Create a local config to use.
            fxnPP "Creating DUE configuration file at $CONFIG_FILE_PATH"
            fxnEC fxnCreateConfigFile || exit 1
        fi
    fi

    if [ -e "$CONFIG_FILE_PATH" ];then
        # Read the file, filter out comments (#) and empty lines (\S)
        configVars=$( grep -v "#" "$CONFIG_FILE_PATH" | grep "\S" )

        # Parse the DUE_ENV_DEFAULT_HOMEDIR variable.
        homeDir=$(echo "$configVars" | grep DUE_ENV_DEFAULT_HOMEDIR | tail -n 1 )
        if [ "$homeDir" != "" ];then
            # This will set DUE_ENV_DEFAULT_HOMEDIR
            eval "$( echo $homeDir )"
        fi

        # Parse the DUE_USER_CONTAINER_LIMIT variable, and override the
        # default set at the top of this file.
        maxContainers=$(echo "$configVars" | grep DUE_USER_CONTAINER_LIMIT | tail -n 1 )
        if [ "$maxContainers" != "" ];then
            # Trim everything before the = to leave the number
            DUE_USER_CONTAINER_LIMIT=${maxContainers##*=}
        fi
    fi
}

# DUE embeds labels into the images it creates to get hints for how the containers should
# be run. Print all known labels here:
# Takes:
#   $1  - name of container
#   $2  - tag of container
function fxnPrintContainerLabels()
{
    local containerName="$1"
    local containerTag="$2"

    local DUEImageType
    local DUEMountHostDirsUP
    local DUEContainerVersion
    local DUECreationVersion

    # How does the container identify itself?
    DUEImageType=$( ${DOCKER_APP} inspect -f '{{.Config.Labels.DUEImageType}}' "${containerName}":"${containerTag}" )

    # Mount host file system this many directories up from current working directory
    DUEMountHostDirsUP=$( ${DOCKER_APP} inspect -f '{{.Config.Labels.DUEMountHostDirsUp}}' "${containerName}":"${containerTag}" )
    # Revision of container
    DUEContainerVersion=$( ${DOCKER_APP} inspect -f '{{.Config.Labels.DUEContainerVersion}}' "${containerName}":"${containerTag}" )
    # DUE revision that created it
    DUECreationVersion=$( ${DOCKER_APP} inspect -f '{{.Config.Labels.DUECreationVersion}}' "${containerName}":"${containerTag}" )
    # Host directories to try and mount in the container (for shared libraries, etc)
    DUEMountHostDirectories=$( ${DOCKER_APP} inspect -f '{{.Config.Labels.DUEMountHostDirectories}}' "${SELECTED_CONTAINER_NAME}":"${SELECTED_CONTAINER_TAG}" )

    echo " -----------------------------------------------------------------------"
    echo "| DUE Labels for:      ${containerName}:${containerTag}"
    echo "|"
    echo "| DUEImageType            $DUEImageType"
    echo "| DUEContainerVersion     $DUEContainerVersion"
    echo "| DUECreationVersion      $DUECreationVersion"
    echo "| DUEMountHostDirsUP      $DUEMountHostDirsUP"
    echo "| DUEMountHostDirectories $DUEMountHostDirectories"
    echo "|"

}

#
# Stop a running container.
# Note that if the container was started with DUE it will be removed, as
# DUE containers are invoked with --rm and clean up on exit.
# Takes: Optional pattern match as first argument
# Note: if --all was part of the invocation, containers for all users will be shown.

STOP_SCRIPT="stop_these_docker_containers.sh"

function fxnDoContainerStop()
{
    local filterByUser
    local containerInfo
    local stopTerm="$1"
    local countDown=5
    if [ "$FILTER_SHOW_ONLY_DUE" = "TRUE" ];then
        filterByUser="${USER_NAME}"
    fi

    if [ "$stopTerm" = "" ];then
        fxnSelectContainer "running" "$filterByUser"
        echo "Stopping container in 5 seconds. Use Ctrl-C to cancel."
        containerInfo=$(${DOCKER_APP} ps | grep "$SELECTED_CONTAINER_ID")
        echo "$containerInfo"
        while [ $countDown -ne 0 ]
        do
            echo -n "${countDown}..."
            ((countDown--))
            sleep 1
        done
        echo ""
        echo "Stopping container."
        fxnEC ${DOCKER_APP} stop "$SELECTED_CONTAINER_ID" || exit 1
        # Leave some evidence in the system log in case this was not what was intended.
        /usr/bin/logger "due: $(whoami) stopped container [ $containerInfo ]"
        echo "Stopped."
    else

        header=$( ${DOCKER_APP} ps | head -n 1 ) 
        terms=$( ${DOCKER_APP} ps | grep "$stopTerm" )
        if [ "$filterByUser" != "" ];then
            terms=$( echo "$terms" | grep "$filterByUser" )
        fi

        if [ "$terms" = "" ];then
            echo "Found no containers that had  : $stopTerm $filterByUser"
            echo "Exiting."
            exit 0
        fi
        # make sure there is something to act on before continuing.
        numToDelete=$( echo "$terms" | wc -l )

        # put a # after the container ID to turn the rest of the info in to a comment
        # prepend the ID with 'docker stop' to create an executable command.
        #echo "$terms" | sed -e 's/   / # /' | sed -e 's/^/docker stop /g' >> $STOP_SCRIPT
		toDelete=$( echo "$terms" | sed -e 's/ / # /' | sed -e "s/^/${DOCKER_APP} stop /g" )
#        toDelete=$( echo "$terms" | sed -e 's/   / # /' | sed -e "s/^/${DOCKER_APP} stop /g" )

        # Create the stop script to be very explicit about what will happen
        {
            echo "#/bin/bash"
            echo "# Created by $0 $INVOKED_WITH on $(date)"
            echo "# Will stop the following containers:"
            echo "#            $header"
            echo "$toDelete"

            # Leave some evidence in the system log in case this was not what was intended.
            echo "/usr/bin/logger \"due: $(whoami) stopped multiple containers.\""

            # Clean up after ourselves
            echo "rm ./$STOP_SCRIPT"
        } > $STOP_SCRIPT

        chmod a+x "$STOP_SCRIPT"

        echo " ________________________________________________________________________"
        echo "|                                                                       "
        echo "| Run [ ./$STOP_SCRIPT ]                                "
        echo "|   to delete the [ $numToDelete ] containers that matched \"$stopTerm\"        "
        echo "|________________________________________________________________________"
        echo ""

    fi

}

# Takes: String identifying the type of DUE container
# Does:  Sets CONTIANER_SPECIFIC_ARGS, if needed.
#
# This sets any additional arguments to pass to Docker when starting a
# container, such as --privileged or an additional mount point.
#
# This is left at the end of the file for easy merging, as local
# branches may have deployment specific changes that do not apply
# to upstream.
function fxnSetContainerSpecificArgs()
{
    local imageType="$1"

    # uses the value set by the DUEImageType label from
    # template directory's Dockerfile.config
    case $imageType in
        "no-op-image" )
        # Example: Mount current directory under /build in the container.
        # Useful if this is invoked by an automated build system
        # that builds outside the home directory hierarchy
        # CONTAINER_SPECIFIC_ARGS="  --volume $(dirname $(pwd)):/build "
        ;;

    esac

}

######################################################################
# Editor hints for sh syntax interpretation
# Append as necessary.
######################################################################

# For Emacs
# Local Variables:
# mode: sh
# End:
