Skip to content

Instantly share code, notes, and snippets.

@kherge
Last active July 1, 2021 22:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kherge/af65ff987f7cd542644e5694540d1861 to your computer and use it in GitHub Desktop.
Save kherge/af65ff987f7cd542644e5694540d1861 to your computer and use it in GitHub Desktop.
Containerized Development Environment Manager

Containerized Development Environment Manager

Uses Docker CLI to create a development environment as a container.

kherrera@machine Desktop % ./dev shell
[+] Building 1.5s (11/11) FINISHED                                                                                       
 => [internal] load build definition from Dockerfile                                                                0.0s
 => => transferring dockerfile: 882B                                                                                0.0s
 => [internal] load .dockerignore                                                                                   0.0s
 => => transferring context: 2B                                                                                     0.0s
 => [internal] load metadata for docker.io/library/ubuntu:21.04                                                     1.3s
 => [auth] library/ubuntu:pull token for registry-1.docker.io                                                       0.0s
 => [1/6] FROM docker.io/library/ubuntu:21.04@sha256:a30456233740024b9d297f5bcaa7439446a97bc59b25cadcdae829c334827  0.0s
 => CACHED [2/6] RUN apt-get update &&     apt-get dist-upgrade -y &&     apt-get autoclean &&     apt-get autorem  0.0s
 => CACHED [3/6] RUN apt-get install -y     build-essential libssl-dev pkg-config     curl git gnupg2 sudo unzip v  0.0s
 => CACHED [4/6] RUN /bin/bash -c '[ "$(grep -F :20: < /etc/group)" != "" ] ||     addgroup --gid 20 "staff"'       0.0s
 => CACHED [5/6] RUN adduser --uid 501 --gid 20 "kherrera" &&     chown -R "kherrera:staff" "/home/kherrera" &&     0.0s
 => CACHED [6/6] WORKDIR /home/kherrera                                                                             0.0s
 => exporting to image                                                                                              0.0s
 => => exporting layers                                                                                             0.0s
 => => writing image sha256:35e3507030a86946720d6c3697b7f03c1e2847992ad8b7a8a0be3f4d103f4818                        0.0s
 => => naming to docker.io/library/dev:1.0                                                                          0.0s

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

kherrera@88ccb4fd6a0e:~$ 

Features

  • Script is (or should be) POSIX compliant for portability.
  • Uses paths defined in the XDG Base Directory Specification.
  • Uses a volume for the user's home folder.
    • Matches the username, UID, and GID.

Installation

  1. Copy dev.sh to somewhere in your $PATH.
  2. Rename dev.sh to dev.
  3. Make it executable (e.g. chmod 755 dev).

Usage

Run dev without arguments to see a usage guide.

#!/bin/sh
################################################################################
# Containerized Development Environment Manager #
################################################################################
# The path to the configuration folder.
CONFIG="$XDG_CONFIG_HOME"
if [ "$CONFIG" = '' ]; then
CONFIG="$HOME/.config"
fi
CONFIG="$CONFIG/dev"
if [ ! -d "$CONFIG" ]; then
if ! mkdir -p "$CONFIG"; then
exit 1
fi
fi
# The name of the container.
CONTAINER=dev
# The path to the data folder.
DATA="$XDG_DATA_HOME"
if [ "$DATA" = '' ]; then
DATA="$HOME/.local/share"
fi
DATA="$DATA/dev"
if [ ! -d "$DATA" ]; then
if ! mkdir -p "$DATA"; then
exit 1
fi
fi
# The name of this script.
EXE="$(basename "$0")"
# The name of the image.
IMAGE=dev
# The path to the preferred shell in the container.
PREFERRED_SHELL=/bin/bash
################################################################################
# Dockerfile #
################################################################################
# The current user's group ID.
GROUP_ID=$(id -g)
# The current user's group name.
GROUP_NAME="$(grep -F ":$(id -g):" < /etc/group | cut -d: -f1)"
# The current user's container password.
PASSWORD=dev
# The current user's ID.
USER_ID=$(id -u)
# The current user's name.
USER_NAME="$USER"
# The Dockerfile for the container.
DOCKERFILE="
FROM ubuntu:21.04
# Disable interactivity during build.
ARG DEBIAN_FRONTEND=noninteractive
# Update base installation.
RUN apt-get update && \\
apt-get dist-upgrade -y && \\
apt-get autoclean && \\
apt-get autoremove
# Install tools.
RUN apt-get install -y \\
build-essential libssl-dev pkg-config \\
curl git gnupg2 sudo unzip vim zip \\
htop
# Create a matching group, if necessary.
RUN /bin/bash -c '[ \"\$(grep -F :20: < /etc/group)\" != \"\" ] || \\
addgroup --gid $GROUP_ID \"$GROUP_NAME\"'
# Create a user with a matching UID & GID.
RUN adduser --uid $USER_ID --gid $GROUP_ID \"$USER_NAME\" && \
chown -R \"$USER_NAME:$GROUP_NAME\" \"/home/$USER_NAME\" && \
usermod --append --groups sudo \"$USER_NAME\" && \
(echo \"$USER_NAME:$PASSWORD\" | chpasswd)
# Change to the dev user.
USER $USER_NAME
WORKDIR /home/$USER_NAME
# Run forever.
CMD [\"sleep\", \"infinity\"]
"
# The versino of the Dockerfile above.
VERSION='1.0'
################################################################################
# Utilities #
################################################################################
###
# Retrieves the value of a configuration setting.
#
# @param $1 The name of the setting.
# @stderr If the setting could not be read.
# @stdout The value of the setting.
# @return `0` if there is a value, or `1` if not.
##
config_get()
{
FILE="$CONFIG/$1"
if [ -f "$FILE" ]; then
if ! cat "$FILE"; then
echo "$FILE: could not be read" >&2
exit 1
fi
return 0
fi
return 1
}
###
# Sets the value of a configuration setting.
#
# @param $1 The name of the setting.
# @param $2 The value of the setting.
# @stderr If the setting could not be written.
##
config_set()
{
FILE="$CONFIG/$1"
VALUE="$2"
if ! echo "$VALUE" > "$FILE"; then
echo "$FILE: could not be written" >&2
exit 1
fi
}
###
# Prints a message to STDERR if DEBUG=1.
#
# @param $@ The message to print.
##
debug()
{
if [ "$DEBUG" = '1' ]; then
echo "$@" >&2
fi
}
###
# Requires that a command successfully execute.
#
# @param $@ The command line arguments.
# @stderr The output of the command.
# @stdout The output of the command.
# @exit The same status as the command if not 0 (zero).
##
must()
{
"$@"
STATUS=$?
debug "exited [$STATUS]: $*"
if [ $STATUS -ne 0 ]; then
debug "Command failed, exiting."
exit $STATUS
fi
}
################################################################################
# Container, Image, and Volume Management #
################################################################################
###
# Attaches a shell to the running container.
#
# @exit The same status as Docker CLI exit status.
##
container_attach()
{
debug "Attaching shell to container, $CONTAINER..."
docker exec \
--interactive \
--tty \
--user "$USER_NAME" \
--workdir "/home/$USER_NAME" \
"$CONTAINER" "$PREFERRED_SHELL"
exit $?
}
###
# Creates the container.
##
container_create()
{
debug "Creating the container, $CONTAINER..."
must docker container create \
"--name=$CONTAINER" \
"--volume=$DATA:/home/$USER_NAME" \
"$IMAGE:$VERSION" > /dev/null
}
###
# Destroys the container if it exists.
##
container_destroy()
{
debug "Destroying the container, $CONTAINER..."
must docker container rm "$CONTAINER"
}
###
# Checks if the container exists.
#
# @return Returns 0 (zero) if it exists, or 1 (one) if not.
##
container_exists()
{
debug "Checking if the container, $CONTAINER, exists..."
if ! STATUS="$(container_status)"; then
exit 1
elif [ "$STATUS" = '' ]; then
debug "The container does not exist."
return 1
fi
debug "The container exists."
return 0
}
###
# Checks if the container is running.
#
# @return Returns 0 (zero) if it is running, or 1 (one) if not.
##
container_is_running()
{
debug "Checking of the container, $CONTAINER, is running..."
if ! STATUS="$(container_status)"; then
exit 1
elif [ "$STATUS" = 'running' ]; then
debug "The container is running."
return 0
fi
debug "The container is not running."
return 1
}
###
# Starts the container if it is not running.
##
container_start()
{
debug "Starting the container, $CONTAINER..."
must docker start "$CONTAINER" > /dev/null
}
###
# Fetches the status of the container.
##
container_status()
{
debug "Getting the status of the container, $CONTAINER..."
must docker ps --all --filter "name=$CONTAINER" --format "{{.State}}"
}
###
# Stops the container if it is running.
##
container_stop()
{
debug "Stopping the container, $CONTAINER..."
must docker container stop "$CONTAINER" > /dev/null
}
###
# Creates a new image, replacing an existing version if necessary.
##
image_create()
{
debug "Creating the image, $IMAGE:$VERSION..."
echo "$DOCKERFILE" | must docker build --tag "$IMAGE:$VERSION" -
# Because we're piping, we need to handle the subshell.
STATUS=$?
if [ $STATUS != 0 ]; then
exit $STATUS
fi
}
###
# Destroys the image (current version tag) if it exists.
##
image_destroy()
{
debug "Destroying the image, $IMAGE:$VERSION..."
must docker image rm "$IMAGE:$VERSION"
}
###
# Checks if the image exists.
#
# @return Returns 0 (zero) if it exists, or 1 (one) if not.
##
image_exists()
{
debug "Checking if the image, $IMAGE:$VERSION, exists..."
docker image inspect "$IMAGE:$VERSION" > /dev/null 2>&1
return $?
}
###
# Destroys the volume if it exists.
##
volume_destroy()
{
debug "Destroying the volume, $DATA..."
if [ -d "$DATA" ]; then
must rm -R "$DATA"
fi
}
###
# Checks if the volume exists.
#
# @return Returns 0 (zero) if it exists, or 1 (one) if not.
##
volume_exists()
{
debug "Checking if the image, $IMAGE:$VERSION, exists..."
if [ -d "$DATA" ]; then
return 0
fi
return 1
}
################################################################################
# Commands #
################################################################################
###
# Tears down the environment.
#
# @param $@ The command line arguments.
##
do_clean()
{
# Process arguments.
DELETE_CONTAINER=0
DELETE_IMAGE=0
DELETE_VOLUME=0
shift
while getopts :civ OPTION; do
case "$OPTION" in
c) DELETE_CONTAINER=1;;
i) DELETE_IMAGE=1;;
v) DELETE_VOLUME=1;;
*)
echo "$EXE: $OPTARG: invalid option" >&2
echo >&2
do_usage
esac
done
# Make sure work is specified.
if [ $DELETE_CONTAINER -eq 0 ] && \
[ $DELETE_IMAGE -eq 0 ] && \
[ $DELETE_VOLUME -eq 0 ]; then
echo "No work to be done."
return
fi
# Confirm.
printf "The process is irreversible. Are you sure? [y/N] "
read -r REPLY
if [ "$REPLY" != 'y' ]; then
return
fi
# Start nuking.
if [ $DELETE_CONTAINER -eq 1 ]; then
echo "Deleting the container..."
if container_exists; then
if container_is_running; then
container_stop
fi
container_destroy
fi
fi
if [ $DELETE_VOLUME -eq 1 ]; then
echo "Deleting the volume..."
if volume_exists; then
if container_is_running; then
echo "The container must be stopped first." >&2
exit 1
fi
volume_destroy
fi
fi
if [ $DELETE_IMAGE -eq 1 ]; then
echo "Deleting the image..."
if image_exists; then
if container_exists; then
echo "The container must be deleted too." >&2
exit 1
fi
image_destroy
fi
fi
}
###
# Displays the usage guide.
##
do_usage()
{
echo "Usage: $EXE COMMAND [OPTIONS]"
echo "Manages a containerized development environment."
echo
echo "COMMAND"
echo
echo " clean Tears down the environment."
echo " help Displays this help message."
echo " shell Starts the container and attaches a shell."
echo " start Starts the container."
echo " stop Stops the container."
echo
echo "OPTIONS"
echo
echo " clean"
echo
echo " -c Deletes the container."
echo " -i Deletes the image."
echo " -v Deletes the volume."
echo
exit 3
}
###
# Starts the container and attaches a shell.
##
do_shell()
{
SILENT=1 do_start
container_attach
}
###
# Starts the container.
##
do_start()
{
if ! image_exists; then
image_create
fi
if ! container_exists; then
container_create
fi
if container_is_running && [ "$SILENT" != '1' ]; then
echo "Container is already running."
else
container_start
fi
}
###
# Stops the container.
##
do_stop()
{
if container_is_running; then
container_stop
else
echo "Container is already stopped."
fi
}
################################################################################
# Interface #
################################################################################
if ! command -v docker > /dev/null; then
echo "$EXE: docker is required" >&2
exit 1
fi
case "$1" in
""|help) do_usage;;
clean) do_clean "$@";;
shell) do_shell;;
start) do_start;;
stop) do_stop;;
*)
echo "$EXE: $1: invalid command" >&2
exit 1
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment