Managing the development environments for different projects is a difficult task. With most GIT based development platforms, like Gitlab, Github, etc., containers are used to setup the continuous integration and deployment pipelines. So it is not far fetched, to use the containers for the local development enviornment, also. By doing this you get the following benefits:

  1. Local environment mirros always remote environment

  2. One person can manage development environment for the whole team

  3. Local computers do not get cluttered

  4. Different projects can have different versions of the same toolchain

  5. Old versions of a project can still be build without a hassle, even when the host operation system is incompatible with the older toolchain

This are only the most obvious of benefits a development team gets when using containers. On the other hand, it gets more complicated to use the containers with some IDEs, and sometime the containers have to be started with a special configuration. For example, debugging sometimes does not work correctly, out of the box.

Seemless integration

In the perfect world, a developer would not even recognize that a container is used. This seemless integration into the command line interface (CLI) can be done with two slightly different approches.

  1. By definition of alias/function in the bashrc

  2. By wrapper script, placed in the users binary search path

Definition of a bash function

yarn() { podman run --rm -it -v .:/project:z yarn:latest yarn "$@"; }

This approch is fast, but static. Every time a different version or options are needed the function definiton has to be changed. Also, the function can’t be used by the most IDEs, because these need binaries. But for tools, which are only called by the developer this could be a useful and fast approch.

By wrapper script

For this approch to work just place a script with the file name of the desired executable into .local/bin or into any other directory in the PATH variable. Again for yarn this could look like in the listing below.

#!/bin/bash

yarn() { podman run --rm -it -v .:/project:z yarn:latest yarn "$@"; }

yarn()

The above code will work, but is probably not really something you want to handle in production. Something a bit more sophisticated is needed. A script which lets you handle multiple environments depending on your needs. To get the fullest out of podman we need a script that can do the following things:

  1. Create/delete an environment

  2. Switch between environments

  3. Get current environment

  4. Add application to envrionment

  5. Remove application from environment

The example below works, but maybe needs some adjustments.

#!/bin/bash

CONFIG_DIR="$HOME/.config/devenv"
EXECUTION_DIR="$HOME/.local/bin"

environment_create() {
if [ -z $1 ]; then
    echo "Environment name is missing!"
    print_help
    exit 1
fi

if [ -d "${CONFIG_DIR}/$1" ]; then
    echo "Environment already exists!"
    exit 1
fi

echo "Create environment: $1"
mkdir -p "${CONFIG_DIR}/$1"

if [ ! -L "${CONFIG_DIR}/current" ]; then
    environment_switch $1
fi

exit 0
}

environment_delete() {
if [ -z $1 ]; then
    echo "Environment name is missing!"
    print_help
    exit 1
fi

if [ ! -d "${CONFIG_DIR}/$1" ]; then
    echo "Environment does not exist!"
    exit 1
fi

echo "Delete environment: $1"
rm -r "${CONFIG_DIR}/$1"

if [ ! -d readlink "${CONFIG_DIR}/current" ]; then
    rm "${CONFIG_DIR}/current"
fi
exit 0
}

symlinks_remove() {
if [ -L "${CONFIG_DIR}/current" ]; then
    apps=$(ls "${CONFIG_DIR}/current")
    for i in $apps; do
        if [ -L "${EXECUTION_DIR}/$i" ]; then
            rm "${EXECUTION_DIR}/$i"
        fi
    done
fi
}

symlinks_add() {
if [ -L "${CONFIG_DIR}/current" ]; then
    apps=$(ls "${CONFIG_DIR}/current")
    for i in $apps; do
        if [ ! -L "${EXECUTION_DIR}/$i" ]; then
            ln -s "${EXECUTION_DIR}/devenv-executor" "${EXECUTION_DIR}/$i"
        fi
    done
fi
}

environment_switch() {
if [ -z $1 ]; then
    echo "Environment name is missing!"
    print_help
    exit 1
fi

if [ ! -d "${CONFIG_DIR}/$1" ]; then
    echo "Environment does not exist!"
    exit 1
fi

echo "Set current environment to: $1"
if [ -L "${CONFIG_DIR}/current" ]; then
    symlinks_remove
    rm "${CONFIG_DIR}/current"
fi

ln -s "${CONFIG_DIR}/$1" "${CONFIG_DIR}/current"
symlinks_add
exit 0
}

application_create() {
if [ -z $1 ]; then
    echo "Application name not specified!"
    exit 1
fi

if [ -z $2 ]; then
    echo "Image name not specified!"
    exit 1
fi

echo "Create application entry: $1"
if [ ! -L "${CONFIG_DIR}/current" ]; then
    echo "No environment configured!"
    exit 1
fi

if [ -f "${CONFIG_DIR}/current/$1" ]; then
    echo "Application alread exists!"
    exit 1
fi

cat > ${CONFIG_DIR}/current/$1 << EOF
#!/bin/bash
podman run --rm -it ${p_value} $2 $1 "\$@"
EOF
chmod +x ${CONFIG_DIR}/current/$1

symlinks_add
}

application_delete() {
if [ -z $1 ]; then
    echo "Application name not specified!"
    exit 1
fi

symlinks_remove
if [ -f "${CONFIG_DIR}/current/$1" ]; then
    rm "${CONFIG_DIR}/current/$1"
fi
symlinks_add
}

executor_install() {
cat > "${EXECUTION_DIR}/devenv-executor" << EOF
#!/bin/bash
BASENAME="\$(basename \$0)"
CONFIG_DIR="${CONFIG_DIR}"
EXECUTION_DIR="${EXECUTION_DIR}"

if [ ! -f "\${CONFIG_DIR}/current/\${BASENAME}" ]; then
exit 1
fi

exec \${CONFIG_DIR}/current/\${BASENAME} "\$@"
EOF
chmod +x "${EXECUTION_DIR}/devenv-executor"
}

executor_uninstall() {
if [ ! -f "${EXECUTION_DIR}/devenv-executor" ]; then
    echo "Executor is not instaled!"
    exit 1
fi

rm "${EXECUTION_DIR}/devenv-executor"
exit 0
}

print_help() {
cat << EOF

Usage:
$(basename $0) -[CDS] <environment name>
$(basename $0) -c [-p=<podman args>] <application name> <image identifier>
$(basename $0) -d <application name>
$(basename $0) -i [<execution path>]
$(basename $0) -u

-C create environment
-D delete environment
-S switch environment
-c create application entry in current environment
-d delete application entry from current environment
-i install application executor to specified directory and add directory to path
-u uinstall application executor and remove directory from path
-p additional podman argument string

Only one of the options CDScdiu can be used at the same time.
EOF
}

check_flags() {
flag_count=$((C_flag + D_flag + S_flag + c_flag + d_flag + i_flag + u_flag))
if (( flag_count > 1 )); then
    echo "Only one of the options CDScdiu can be used at the same time."
    print_help
    exit 1
fi

if (( flag_count == 0 )); then
    echo "At least one of the options CDScdiu has to be specified."
    print_help
    exit 1
fi
}

parse_opts() {
while getopts ":CDScdiup:" flag;do
    case $flag in
        C)
            C_flag=1;;
        D)
            D_flag=1;;
        S)
            S_flag=1;;
        c)
            c_flag=1;;
        d)
            d_flag=1;;
        i)
            i_flag=1;;
        u)
            u_flag=1;;
        p)
            p_flag=1
            p_value="${OPTARG}"
            echo $p_value
            ;;
        \?)
            echo "Invalid option -$OPTARG"
            print_help
            exit 1;;
    esac
done
OPTIONS_PARSED=$((OPTIND-1))
}

parse_opts "$@"
shift ${OPTIONS_PARSED}
check_flags

if [ ! -z $C_flag ]; then
environment_create "$@"
fi

if [ ! -z $D_flag ]; then
environment_delete "$@"
fi

if [ ! -z $S_flag ]; then
environment_switch "$@"
fi

if [ ! -z $c_flag ]; then
application_create "$@"
fi

if [ ! -z $d_flag ]; then
application_delete "$@"
fi

if [ ! -z $i_flag ]; then
executor_install "$@"
fi

if [ ! -z $u_flag ]; then
executor_install "$@"
fi