#!/bin/sh set -eu usage() { cat < [ ...] $BINNAME list|clean $BINNAME connect $BINNAME help|-h|--help EOF if [ "$#" -eq 2 ]; then cat <.pvt.dsv.su.se. Two containers are managed per (a container group). One is a tomcat to deploy applications to, the other is a jenkins instance to manage CI. Both are always handled together. The tomcat container has no persistent data whatsoever, while the jenkins container has an associated docker volume for persistence. The script automatically handles proxy and SSL certificate management as necessary in addition to the container management functions. Commands: create Creates a new container group with the given name. If the name exists in the registry, the associated passwords are used. If it doesn't exist in the registry, new passwords are generated and saved to the registry. If the docker volume to be used with jenkins exists it will be re-used as is. destroy Destroys the container group with the given name. It will remain in the registry and the jenkins volume saved. reset Resets the container group with the given name. This is done by first destroying and then re-creating them. Jenkins data is unaffected. clobber Fully destroys the container group with the given name, including jenkins volume data. Otherwise acts the same as destroy. list Lists known container groups along with their passwords. clean Purges containers that aren't currently running. This also deletes them from the registry, unlike the 'destroy' action. Jenkins volumes not in use are also removed. connect Starts a bash process in the container with the given name and connects the terminal to it. help Displays this help text. The create, destroy and reset commands can be used with multiple names separated by spaces, in which case the relevant action is preformed for all listed names. Files and directories: $BASEDIR/containers.list The registry file that keeps track of containers and manager passwords. The format is "\t\t". $BASEDIR/proxy.conf The template file used to set up the apache vhost for each container. $BASEDIR/base/ The base dockerfile and supporting files are located here. $BASEDIR/target/ The dockerfile to create the final image for a given container and supporting files are located here. EOF fi exit "$1" } registry_read() { local name="$1" local field="$2" case "$field" in manager ) field=2 ;; jenkins ) field=3 ;; * ) echo "Error: reading unknown field '$field'" >&2 exit 2 ;; esac local pass='' local existing="$(grep "^$name\s" "$REGISTRY")" if [ -n "$existing" ]; then pass="$(echo "$existing" | cut -f"$field")" fi echo "$pass" } registry_write() { local name="$1" local field="$2" local value="$3" local manager_pass='' local jenkins_pass='' local existing="$(grep "^$name\s" "$REGISTRY")" if [ -n "$existing" ]; then manager_pass="$(echo "$existing" | cut -f2)" jenkins_pass="$(echo "$existing" | cut -f3)" fi case "$field" in manager ) manager_pass="$value" ;; jenkins ) jenkins_pass="$value" ;; * ) echo "Error: writing unknown field '$field'" >&2 exit 2 ;; esac grep -v "^$name\s" "$REGISTRY" > "$REGISTRY.new" || true printf "$name\t$manager_pass\t$jenkins_pass\n" >> "$REGISTRY.new" mv "$REGISTRY.new" "$REGISTRY" } docker_clone() { local volume="$1" local jenkins_pass="$2" docker volume create --name "$volume" >/dev/null docker run --rm -it \ -v "jenkins-base":/from -v "$volume":/to \ alpine ash -c "cd /from; cp -a . /to" >/dev/null # Set the passed password in the clone local adminconf=$(find "$VOLUMES/$volume/_data/users" -name "config.xml") local hash=$(python3 /opt/pvt/hash.py "$jenkins_pass") xmlstarlet -q ed -L \ -u /user/properties/hudson.security.HudsonPrivateSecurityRealm_-Details/passwordHash \ -v "$hash" \ "$adminconf" # Set the correct URL for jenkins xmlstarlet -q ed -L \ -u /jenkins.model.JenkinsLocationConfiguration/jenkinsUrl \ -v "https://$FQDN/jenkins" \ "$VOLUMES/$volume/_data/jenkins.model.JenkinsLocationConfiguration.xml" } create_tomcat() { local name="$1" local tag="pvt:$name" # Get password from registry or generate if not found local manager_pass="$(registry_read "$name" manager)" if [ -z "$manager_pass" ]; then manager_pass=$(pwgen 20 1) registry_write "$name" manager "$manager_pass" fi # Build images and start container docker build --tag pvt:base base >/dev/null docker build --tag "$tag" --build-arg pass="$manager_pass" target docker run -tid --restart unless-stopped \ --memory=2G --memory-swap=2G \ --name "$name" "$tag" >/dev/null } destroy_tomcat() { local name="$1" docker stop "$name" >/dev/null docker rm "$name" >/dev/null } create_jenkins() { local name="$1" local jenkins_name="$name-jenkins" # Get password from registry or generate if not found local jenkins_pass="$(registry_read "$name" jenkins)" if [ -z "$jenkins_pass" ]; then jenkins_pass=$(pwgen 20 1) registry_write "$name" jenkins "$jenkins_pass" fi # Init jenkins homedir if it doesn't exist if ! docker volume ls | awk '{print $2}' | grep -q "^$jenkins_name$"; then docker_clone "$jenkins_name" "$jenkins_pass" fi # Start group jenkins docker run -d --restart unless-stopped \ -v "$jenkins_name":/var/jenkins_home \ --env JENKINS_OPTS="--prefix=/jenkins" \ --memory=1G --memory-swap=1G \ --name "$jenkins_name" \ jenkins/jenkins:lts >/dev/null } destroy_jenkins() { local jenkins="$1-jenkins" local purge="$2" # Kill jenkins docker stop "$jenkins" >/dev/null docker rm "$jenkins" >/dev/null if [ -n "$purge" ]; then docker volume rm "$jenkins" >/dev/null fi } create_proxy() { local name="$1" # Determine container IP local ip="$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$name")" local jip="$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$name-jenkins")" # Set up proxy sed -r \ -e "s/%FQDN%/$FQDN/g" \ -e "s/%BACKEND%/$ip/g" \ -e "s/%JENKINS%/$jip/g" \ proxy.conf > "$SITES"/"$CONFNAME" ARELOAD=true } destroy_proxy() { # Disable proxy rm "$SITES"/"$CONFNAME" || true ARELOAD=true } create() { local name="$1" shift local no_proxy="" while [ "$#" -gt 0 ]; do case "$1" in no-proxy ) no_proxy="$1" shift ;; * ) echo "Invalid argument: $1" exit 3 ;; esac done create_tomcat "$name" create_jenkins "$name" # Update vhosts if ! grep -q "^${FQDN}$" "$VHOSTS"; then echo "$FQDN" >> "$VHOSTS" NEEDCERT=true fi if [ -z "$no_proxy" ]; then create_proxy "$name" fi } destroy() { local name="$1" shift local purge="" local keep_proxy="" while [ "$#" -gt 0 ]; do case "$1" in purge ) purge="$1" shift ;; keep-proxy ) keep_proxy="$1" shift ;; * ) echo "Invalid argument: $1" exit 3 ;; esac done if [ -z "$keep_proxy" ]; then destroy_proxy fi destroy_tomcat "$name" destroy_jenkins "$name" "$purge" } reset() { local name="$1" destroy "$name" create "$name" } resetsoft() { local name="$1" destroy "$name" keep-proxy create "$name" no-proxy } clobber() { local name="$1" destroy "$name" purge } connect() { local name="$1" if [ "$#" -gt 1 ]; then shift exec docker exec "$name" "$@" else exec docker exec -ti "$name" bash fi } list() { running="$(docker container ls --format '{{.Names}}')" ( printf "#Name\tManager pass\tJenkins pass\tStatus\n" while read name manager jenkins; do printf "${name}\t${manager}\t${jenkins}" if echo "$running" | grep -q "^${name}$"; then printf "\trunning" fi printf '\n' done < "$REGISTRY" ) | column -s"$(printf '\t')" -t } clean() { # List running running="$(docker container ls --format '{{.Names}}')" # Collect unused names prune="" while read name junk; do if echo "$running" | grep -q "^${name}$"; then continue fi prune="$prune $name" done < "$REGISTRY" for name in $prune; do # Delete from registry sed -ri "/^${name}\s/d" "$REGISTRY" # Delete from vhosts sed -ri "/^${name}.pvt.dsv.su.se$/d" "$VHOSTS" done } BINNAME="$(basename $0)" BASEDIR="$(dirname "$(readlink -f "$0")")" cd "$BASEDIR" if [ "$#" -lt 1 ]; then usage 0 fi SITES="/etc/apache2/sites-enabled" VHOSTS="/etc/vhosts" VOLUMES="/var/lib/docker/volumes" REGISTRY="containers.list" NEEDCERT=false ARELOAD=false ACTION="$1" shift case "$ACTION" in create|destroy|reset|resetsoft|clobber ) while [ "$#" -gt 0 ]; do NAME="$1" shift FQDN="${NAME}.pvt.dsv.su.se" CONFNAME="proxy-$NAME.conf" $ACTION "$NAME" done ;; connect ) connect "$@" ;; list|clean ) $ACTION ;; help|-h|--help) usage 0 long ;; * ) usage 1 ;; esac # Update SSL cert if needed if [ "$NEEDCERT" = true ]; then /adm/scripts/lecert get >/dev/null ARELOAD=true fi if [ "$ARELOAD" = true ]; then service apache2 reload fi