420 lines
10 KiB
Bash
Executable File

#!/bin/sh
set -eu
usage() {
cat <<EOF
Usage: $BINNAME create|destroy|reset|clobber <name> [<name> ...]
$BINNAME list|clean
$BINNAME connect <name>
$BINNAME help|-h|--help
EOF
if [ "$#" -eq 2 ]; then
cat <<EOF
This script manages docker containers for the PVT course. Containers are
exposed via reverse proxy in apache and are accessible over the internet
at https://<name>.pvt.dsv.su.se.
Two containers are managed per <name> (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 <name> 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 <name> Destroys the container group with the given name.
It will remain in the registry and the jenkins volume saved.
reset <name> Resets the container group with the given name.
This is done by first destroying and then re-creating them.
Jenkins data is unaffected.
clobber <name> 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 <name> 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 "<name>\t<manager>\t<jenkins>".
$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=512M --memory-swap=512M \
--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-jdk17 >/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() {
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
# 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
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
# Disable proxy
rm "$SITES"/"$CONFNAME" || true
ARELOAD=true
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