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

# Copyright 2021-2023 Nvidia Corporation.  All rights reserved.
# 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="4.0.1"

# 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 )

# default the login shell
LOGIN_SHELL="/bin/bash --login"

# If debugging a container, this will be set
OVERRIDE_ENTRYPOINT=""

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

# default tag to latest
DOCKER_IMAGE_TAG="latest"

# Options passed to Docker build, using --dockerarg
DOCKER_SPECIFIC_BUILD_ARGS=""

# List of example images
KNOWN_IMAGES="
 debian:11
 ubuntu:20.04
 arm32v5/debian:jessie
 arm32v7/debian:10
 arm64v8/debian:11
 ppc64le/debian:10
 redhat/ubi9
 opensuse/leap
 bci/bci-base-15.3
 fedora-33/fedora-33
"

# 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"


# Pull this in to set variables like ID
source /etc/os-release

case "$ID" in
    # Red Hat variants
    'fedora' | 'rhel' | 'opensuse-leap' | 'sles' )
        #
        # Red Hat uses a different package manager, so abstract that out.
        # Ex: sudo $PACKAGE_UPDATE  ; sudo $PACKAGE_INSTALL rsync
        #
        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 "
        ;;


    * )
        # Default to Debian variants
        OS_TYPE="Debian"
        PACKAGE_UPDATE=" apt-get update "
        PACKAGE_INSTALL=" apt-get install "
        ;;
esac

######################################################################
# 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 12th field being --platform, 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 -- --platform \
                             | sed -e 's/^.*with://g' \
                             | awk '{printf "%-20s %s\n", $12, $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)
#        PLATFORM           - specify architecture that may be different from the host. (optional)
function fxnMakeNewDockerImage()
{
    local imageFrom="$1"
    local imageName="$2"
    local platformArg
    # if the architecture is different from the host
    local platformArch
    # If image should be pulled from remote registry
    local pullImage
    local imageID
    local imageTag
    # Store the build command to be run, so it can be printed out to the user
    local dockerBuildCommand=""

    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

    if [ "$3" != "" ];then
        # pass the platform specifying architecture specification along.
        # Ex: linux/aarch64
        platformArg=" --platform $3"
        platformArch=${3##*/}
    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 [ "$(command -v "${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
            # Do not have an image with that name
            fxnPP "No image [ $imageFrom ] found. Will pull."
            pullImage="TRUE"
        else
            fxnPP "Found existing image [ $imageFrom ]. Will not pull. Use --platform linux/amd64|aarch64 to override."
        fi
        if [ "$platformArg" != "" ];then
            # If the user specified a platform for the image, and it is not here.
            if [ "$imageArchitecture" != "$platformArch" ];then
                fxnPP "Specified $platformArg and there was no match for [ $imageFrom ]. Will pull."
                pullImage="TRUE"
            fi
        fi

        if [ "$pullImage" = "TRUE" ];then
            # Well, get a copy.
            fxnPP "[ $imageFrom ] not found. Pulling an image to check it's architecture before proceeding."
            fxnEC "${DOCKER_APP}" pull $platformArg "$imageFrom"  || exit 1

            # Use image id rather than name here. Red Hat images use the same name for arm64 and x86 images, so
            # if both are present, one will hide the other. Tag the image with the architecture to keep track.
            # the host's.

            # inspect handles finding the latest image in case there are mutliple, and will take an imageFrom
            #  string that may have more than one '/' in it, in case there are multiple.
            # However, it returns the ID with 'sha256:' stuck to the front.
            imageID=$( "${DOCKER_APP}"  inspect --format "{{.ID}}" "${imageFrom}" | sed -e 's/sha256://' )
            if [ "$imageID" = "" ];then
                fxnERR "Failed to properly id [ $imageFrom ]. Exiting."
                exit 1
            fi

            # May want to remove platformArg logic and just key off of Docker vs Podman
            if [ "$platformArg" != "" ];then
                if [ "${DOCKER_APP}" = 'docker' ];then
                    # Print only the fields to the left and right of the last '/' to strip registry references.
                    imageFrom=$( echo $imageFrom |  awk -F '/' '{print$(NF-1)"/"$NF}' )
                fi
            fi

            imageArchitecture=$( ${DOCKER_APP} inspect -f '{{.Architecture}}' "$imageID" )
            if [ "$platformArg" != "" ];then
                # Print only the fields to the left and right of the last '/' to strip registry references.
                #               imageFrom=$( echo $imageFrom |  awk -F '/' '{print$(NF-1)"/"$NF}' )
                # Sanitize the tag and source by changing any : to -, as having
                # multiple will break the auto-tagging below.
                imageFrom=${imageFrom//:/-}
                # Isolate the architecture
                imageTag=${platformArg##*/}
                # Tag to use
                imageTag=${imageTag//:/-}


                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

                # Tag it with the architecture
                imageTag="$imageArchitecture"

                # Tag the non-host architecture image, as it's name may not be different on a per-architecture basis.
                fxnEC "${DOCKER_APP}" tag $imageID ${imageFrom}:${imageTag} || exit 1
                fxnPP "Tagged new image with ${imageFrom}:${platformArg##*/}"
            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

            STATIC_QEMU_SOURCE="/usr/bin/qemu-${imageArchitecture}-static"
            if [ ! -e $STATIC_QEMU_SOURCE ];then
                fxnWarn "Host system must have qemu-${imageArchitecture}-static to run an ${imageArchitecture} container. Try: sudo $PACKAGE_INSTALL qemu-user-static."
                echo "Trying using the template's filesystem/usr/bin/"
                STATIC_QEMU_SOURCE="${USE_TEMPLATE_PATH}/filesystem/usr/local/bin/qemu-${imageArchitecture}-static"
                if [ ! -e "$STATIC_QEMU_SOURCE" ];then
                    fxnERR "Failed to find [ $STATIC_QEMU_SOURCE ] to run an ${imageArchitecture} container. Try copying it in under there."
                    # 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
            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
            # if no static qemu already there, copy the system's in.
            if [ ! -e "${BUILD_MERGE_DIR}/$imageName/filesystem/usr/bin/qemu-${imageArchitecture}-static" ];then
                fxnEC cp -a "$STATIC_QEMU_SOURCE" "${BUILD_MERGE_DIR}"/"$imageName"/filesystem/usr/bin/qemu-${imageArchitecture}-static || exit 1
            fi
            # 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" \
               --dockerarg "$DOCKER_SPECIFIC_BUILD_ARGS"
        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

        # Sanity check the code doing most of the work
        # Podman/Docker should return success
        DockerAppPath=$( which "$DOCKER_APP" )
        if [ "$DockerAppPath" = "" ];then
            fxnERR "Failed to find path for [ $DOCKER_APP ]. Is it installed?"
            exit 1
        fi
        if [ "$DOCKER_APP" = 'docker' ];then
            # Check if the Docker daemon is running.
            if [ ! -e /var/run/docker.sock ];then
                fxnERR "Is Docker running?"
                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.
        # NOTE: it seems that adding --platform linux/arm64 here _should_
        #  'request a specific platform' and avoid the subsequent:
        #      "[Warning] The requested image's platform (linux/arm64) does not match the
        #      detected host platform (linux/amd64) and no specific platform was requested"
        #  messages when building for arm64 on an amd64 system, but it does not.
        #  As the resulting containers run fine, the solution is to ignore the warning, for now.
        dockerBuildCommand="${DOCKER_APP} build \
              --no-cache=true \
              $DOCKER_SPECIFIC_BUILD_ARGS \
              --tag ${CONFIGURED_NAME_AND_TAG} \
              --file=./Dockerfile.create \
              ."
        # Print the build command, turning all whitespace in to one space.
        fxnHeader "Building with: $( echo $dockerBuildCommand | sed -e 's/[[:space:]]\+/ /g')"

        # Run it.
        fxnEC $dockerBuildCommand ||  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.
#         This should have been set on the command line
function fxnDoExport()
{
    local exportFilter="$1"
    local exportImageName
    local nameTag

    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//\//-}"

    if [ "$DOCKER_APP" = "podman" ];then
        # Trim leading localhost and the - swapped for / above if podman
        exportImageName=$( echo "$exportImageName" | sed -e 's#^localhost-##g' )
    fi

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

    # As names can have spaces, parsing gets tricky, so get it pre-parsed from the docker app.
    # List images:
    # - in name:tag format
    # - delimit the ID to search for with #
    # - delete any leading whitespace (Podman adds it, Docker does not)
    # - delete any leading localhost/ (Podman adds it, Docker does not)
    # - delete everything after #
    #    ..to get rid of the ID to search for now that the name:tag has been found.
    nameTag=$( "${DOCKER_APP}" images  --format 'table  {{.Repository}}:{{.Tag}}#{{.ID}}' \
                   | grep "${SELECTED_CONTAINER_ID}" \
                   | sed -e 's/^[ \t]*//' \
                         -e 's#^localhost/##g' \
                   | sed -e 's/#.*//g' )
    if [ "$nameTag" = "" ];then
        fxnERR "Failed to find name and tag for image ID [ ${SELECTED_CONTAINER_ID} ]. Exiting."
        exit 1
    fi

    # Now export the saved image
    fxnPP "Saving selected image with [ $nameTag ] as [ ${exportImageName}.tar ]"

    # Use the ID associated with the image
    # Use 'save' rather than 'export' to save all layers.
    #  Saving with $SELECTED_CONTAINER_ID does not keep name and tag
    #  Ex:  fxnEC ${DOCKER_APP} save  $SELECTED_CONTAINER_ID > ${exportImageName}.tar
    #  So save with name and tag to preserve them.
    fxnEC "${DOCKER_APP}" save  "$nameTag" -o "${exportImageName}.tar"

    # Note: it is absurdly difficult to get rid of 'localhost/' in short names
    # (i.e. not the full URL the container came from) on containers exported by
    #  Podman to Docker. If no Docker registry is specified:
    #   Docker defaults do Docker.io (and hides it)
    #   Podman defaults to localhost/
    # Point being that DUE containers exported from Podman will always show with
    #  localhost/ on Docker systems.
    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
        #
        if [ "$DOCKER_APP" = "podman" ];then
            fxnPP "Listing local images. This may take a bit with Podman..."
        fi
        # 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
            echo ""
            echo "Error: failed to list Docker images."

            if [ "$DOCKER_APP" = "docker" ];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 ""
            fi
            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.
        # ..but new versions do. Disable this for now, but leave it to check against older Podman versions.
        #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 containers were found. "
            if [ "$DOCKER_APP" = "podman" ];then
                # As Podman isolates containers on a per-user basis, only the user's containers will show.
                echo "  Note that podman does not support loging in to other user's containers."
            else
                # Docker will show all the containers on a system, so less filtering may help.
                echo "  However, the results were filtered by [ $userIs ]. Add --all to the command to see all Docker images."
            fi
            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 [ "$OVERRIDE_LOGIN" = "TRUE" ];then
                # if a login shell was explicitly passed,
                executeProgram="$LOGIN_SHELL"
            else

                # 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"

                if [ "$DUEMountHostDirsUP" != "<no value>" ];then
                    for (( count=DUEMountHostDirsUP ; count > 0 ; count-- ))
                    do
                        # Back up a directory
                        MOUNT_CURRENT_WORKING_DIRECTORY="$( dirname "$MOUNT_CURRENT_WORKING_DIRECTORY" )"
                    done
                fi

                # DUE images have the container-create-user.sh script, so configure for that.
                if [ "$DEBUG_LOGIN" = "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.
                # Only add it if you find it, as Podman systems probably won't have this.
                dockerGID=$( getent group 'docker' | awk -F ':' '{print$3}' )
                if [ "$dockerGID" != "" ];then
                    executeProgram+=" --docker-group-id $dockerGID "
                fi

                # 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
        else
            # Not a DUE image
            if [ "$DEBUG_LOGIN" = "TRUE" ];then
                executeProgram=""
                OVERRIDE_ENTRYPOINT=" --entrypoint $LOGIN_SHELL "
            fi
        fi
        # Generate a unique hostname
        # Replace any '/' or ':' 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
            # User is not supplying a different home directory, so
            # use what the default of DUE_ENV_DEFAULT_HOMEDIR
            # ( either ~/ or what was pulled from ~/.conf/due/due.conf )
            HOMEDIR_PATH+="${DUE_ENV_DEFAULT_HOMEDIR}:/home/$USER_NAME"
        else
            echo "$USE_HOMEDIR" | grep -q "/"
            if [ $? = 0 ];then
                HOMEDIR_PATH+=" ${USE_HOMEDIR}:/home/$USER_NAME"
            fi
            # not having an absolute path = use container home dir.
        fi

        # Add the homedir to mount
        ADDITIONAL_MOUNTS+=" --volume $HOMEDIR_PATH"
        if [ "$CURRENT_WORKING_DIRECTORY" != "" ];then

            if [[ "$CURRENT_WORKING_DIRECTORY" != "$USE_HOMEDIR"* ]];then
                # Let this fall through and get caught in the duplicate check, which has verbose
                # solutions.
                echo "Warning: Current working directory [ $CURRENT_WORKING_DIRECTORY ]"
                echo " is not under home directory [ $USE_HOMEDIR ]."
                echo " Use --mount-dir or --home-dir options to resolve paths if needed."
            fi

            # If cwd is the home directory, don't specify it as a volume mount again.
            # Especially because --home-dir may have been used to specify a different home
            # directory, and the launch will die trying to mount:
            #  /foo:/home/usr
            #   and
            #  /bar:/home/usr
            # at the same time...
            if [[ "$MOUNT_CURRENT_WORKING_DIRECTORY" != "/home/$USER_NAME" ]];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.
            fi
            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 "             Use --home-dir to specify a different host home directory to use in the container."
            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

        if [ "$DOCKER_APP" = "docker" ];then
            # And specify the OS/Architecture for Docker to avoid warnings about --platform type not matching contianer.
            platformType=$( ${DOCKER_APP} inspect -f '{{.Os}}/{{.Architecture}}' "${SELECTED_CONTAINER_NAME}":"${SELECTED_CONTAINER_TAG}"  2>/dev/null )
            if [ "$platformType" != "" ];then
                # Only pass this if a value was returned.
                # It might not be on a partially built container being debugged.
                ARCHITECTURE_ARGS=" --platform $platformType"
            fi

        elif [ "$DOCKER_APP" = "podman" ];then
            # Determine platform type to pass along so that warnings aren't thrown when an amd64 system runs arm64 code...
            # And specify just the architecture for podman to avoid warnings about --platform type not matching contianer.
            imageArchitecture=$( ${DOCKER_APP} inspect -f '{{.Architecture}}' "${SELECTED_CONTAINER_NAME}":"${SELECTED_CONTAINER_TAG}"  2>/dev/null )
            if [ "$imageArchitecture" != "" ];then
                # Only pass this if a value was returned.
                # It might not be on a partially built container being debugged.
                ARCHITECTURE_ARGS=" --arch $imageArchitecture"
            fi


            # For Podman, log in to the container as root,
            # Do not remap userids and namespace
            # then configure the host user in the container using container-create-user.sh
            DOCKER_SPECIFIC_ARGS+=" --user 0:0 --userns keep-id "

            # Podman versions prior to 4 have an issue running containers by ID, so supply name & tag instead.
            PODMAN_VERSION=$( podman -v | sed -e 's/podman version //g' -e 's/\..*//g' )
            if [ "$PODMAN_VERSION" -lt 4 ];then
                SELECTED_CONTAINER_ID="${SELECTED_CONTAINER_NAME}:${SELECTED_CONTAINER_TAG} "
            fi
        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
        # If overriding the container's entry point (for debug), set --entrypoint
        # If different from host, specify image OS/architecture as needed by Docker/Podman.
        # 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 \
                ${OVERRIDE_ENTRYPOINT} \
                $ARCHITECTURE_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 \\"
        echo "     $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
        local LoginCommand=""
        if [ "$USER_NAME" != "" ];then
            userNameArg=" --user $USER_NAME "
        fi

        if [ "$DEBUG_LOGIN" = "TRUE" ];then
            # If this is debug, make few assumptions about the container's ability to handle
            # logins. Just start a specified shell.
            LoginCommand="${DOCKER_APP} exec $doInteractive  $userNameArg ${SELECTED_CONTAINER_ID} $LOGIN_SHELL"
        else
            # Log into container as root, then set ID to user. This executes .bashrc configs and
            # feels more like a real login.
            LoginCommand="${DOCKER_APP} exec $doInteractive --user root ${SELECTED_CONTAINER_ID} $executeProgram --username $USER_NAME --userid $USER_ID  --groupid $USER_GROUP_ID --groupname $USER_GROUP_NAME"
        fi
        formatLoginCMD=$( echo $LoginCommand  | sed -e 's/                / /g' | sed -e 's# --#\\\n     --#g' )
        echo "Logging in to running  [ $SELECTED_CONTAINER_ID ] with: "
        echo " ---------------------------------------------------------------------------"
        echo " ------ : Logging in to running [ $SELECTED_CONTAINER_ID ] with this command:       -- "
        echo "   $formatLoginCMD"
        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
        $LoginCommand
    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_ETC_CONFIG_FILE_PATH="/etc/due/due.conf"

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


# Configuration file to hold user specific overrides.
# There are two use cases.
#  If code is running locally, and DUE has not been installed (no /etc/due/due.conf)
#  Due will copy the due.conf file from the source  checkout to the user's .config
# If DUE has been installed, it will copy the /etc/due/due.conf to the user's .config
#  directory, which will provide any system specific customizations.
function fxnCreateConfigFile()
{

    # If no user local config directory, create it
    if [ ! -e  "$DUE_HOME_CONFIG_DIR" ];then
        fxnPP "Making directory $DUE_HOME_CONFIG_DIR"
        fxnEC mkdir -p "$DUE_HOME_CONFIG_DIR" || exit 1


        if [ ! -e "$DUE_ETC_CONFIG_FILE_PATH" ];then
            # DUE has not been installed as a package.
            # Assume execution from a downloaded source directory
            fxnPP "Copying ./$DUE_ETC_CONFIG_FILE_PATH $DUE_USER_CONFIG_PATH"
            fxnEC cp ./"$DUE_ETC_CONFIG_FILE_PATH" "$DUE_USER_CONFIG_PATH" || exit 1
        else
            # Use the conf file from /etc/due
            cp "$DUE_ETC_CONFIG_FILE_PATH" "$DUE_USER_CONFIG_PATH"
            # Pix or it didn't happen.
            echo "ls -l $DUE_HOME_CONFIG_DIR"
            ls -l $DUE_HOME_CONFIG_DIR
        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.
#
# Set additional default arguments to Docker/Podman when running a container
# based off of the DUEImageType label value found in the image.
# This is set in the template's Dockerfile.config.
#  Example: LABEL DUEImageType=no-op-image

# This can be overridden on a per system or per user basis by sourcing the
# /etc/due/due.conf
# or the user's  ~/.config/due/due.conf file, below.

function fxnSetContainerSpecificArgs()
{
    echo "No default container specific arguments were applied."
    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 host /tmp directory as container's /tmp
            CONTAINER_SPECIFIC_ARGS+=" --volume /tmp/:/tmp/"
            ;;
    esac
}

#
# Source local settings.
#

# Default to not letting users do custom configuration.
DUE_ALLOW_USER_CONFIG="FALSE"

if [ -e "$DUE_ETC_CONFIG_FILE_PATH" ];then
    # Use the installed system-wide due.conf
    source $DUE_ETC_CONFIG_FILE_PATH
else
    # The lack of a config file under /etc implies that DUE was not already
    # installed, so this is likely running locally from a source checkout.
    # In which case, allow the use of a user local config.
    #
    DUE_ALLOW_USER_CONFIG="TRUE"
fi


# The config under /etc will set DUE_ALLOW_USER_CONFIG to TRUE if
# users are allowed to use their own config files.
if [ "$DUE_ALLOW_USER_CONFIG" = "TRUE" ];then
    if [ -e "$DUE_USER_CONFIG_PATH" ];then
        # If ~/.config/due/due.conf, let the user know.
        DUE_CONFIG_SOURCING_SUMMARY+="==== Sourcing due.conf  from: [ $DUE_USER_CONFIG_PATH ]\n"
        source "$DUE_USER_CONFIG_PATH"
    fi
fi

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

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