420 lines
10 KiB
Bash
Executable File
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
|