You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
635 lines
16 KiB
635 lines
16 KiB
#!/bin/bash
|
|
|
|
. /etc/shlib
|
|
|
|
|
|
include pretty
|
|
|
|
depends shyaml docker
|
|
|
|
if [ -r /etc/default/charm ]; then
|
|
. /etc/default/charm
|
|
fi
|
|
|
|
if [ -r /etc/default/$exname ]; then
|
|
. /etc/default/$exname
|
|
fi
|
|
|
|
usage="$exname CHARM"'
|
|
|
|
Deploy and manage a swarm of containers to provide services based on
|
|
a ``compose.yml`` definition and charms from a ``charm-store``.
|
|
'
|
|
|
|
export DEFAULT_COMPOSE_FILE
|
|
|
|
##
|
|
## Functions
|
|
##
|
|
|
|
export APACHE_CONFIG_LOCATION=$CONFIGSTORE/apache/etc/apache2/sites-enabled
|
|
|
|
apache_ssl_proxy_config () {
|
|
local DOMAIN=$1 TARGET=$2
|
|
|
|
cat <<EOF
|
|
<IfModule mod_ssl.c>
|
|
|
|
<VirtualHost *:443>
|
|
ServerAdmin ${ADMIN_MAIL:-contact@$DOMAIN}
|
|
ServerName ${DOMAIN}
|
|
|
|
ServerSignature Off
|
|
CustomLog /var/log/apache2/s-${DOMAIN}_access.log combined
|
|
ErrorLog /var/log/apache2/s-${DOMAIN}_error.log
|
|
ErrorLog syslog:local2
|
|
|
|
<IfModule mod_proxy.c>
|
|
ProxyRequests Off
|
|
<Proxy *>
|
|
Order deny,allow
|
|
Allow from all
|
|
</Proxy>
|
|
ProxyVia On
|
|
ProxyPass / http://$TARGET/ retry=0
|
|
<Location / >
|
|
ProxyPassReverse /
|
|
</Location>
|
|
</IfModule>
|
|
|
|
## Forbid any cache, this is only usefull on dev server.
|
|
#Header set Cache-Control "no-cache"
|
|
#Header set Access-Control-Allow-Origin "*"
|
|
#Header set Access-Control-Allow-Methods "POST, GET, OPTIONS"
|
|
#Header set Access-Control-Allow-Headers "origin, content-type, accept"
|
|
|
|
RequestHeader set "X-Forwarded-Proto" "https"
|
|
|
|
## Fix IE problem (httpapache proxy dav error 408/409)
|
|
SetEnv proxy-nokeepalive 1
|
|
#ServerSignature On
|
|
SSLProxyEngine On
|
|
SSLEngine On
|
|
|
|
## Full stance
|
|
SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
|
|
SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
|
|
|
|
SSLVerifyClient None
|
|
|
|
</VirtualHost>
|
|
|
|
</IfModule>
|
|
EOF
|
|
|
|
}
|
|
export -f apache_ssl_proxy_config
|
|
|
|
apache_ssl_config() {
|
|
local DOMAIN=$1
|
|
|
|
cat <<EOF
|
|
<IfModule mod_ssl.c>
|
|
|
|
<VirtualHost *:443>
|
|
ServerAdmin ${ADMIN_MAIL:-contact@$DOMAIN}
|
|
ServerName ${DOMAIN}
|
|
|
|
ServerSignature Off
|
|
CustomLog /var/log/apache2/s-${DOMAIN}_access.log combined
|
|
ErrorLog /var/log/apache2/s-${DOMAIN}_error.log
|
|
ErrorLog syslog:local2
|
|
|
|
DocumentRoot /var/www/${DOMAIN}
|
|
|
|
<Directory />
|
|
Options FollowSymLinks
|
|
AllowOverride None
|
|
</Directory>
|
|
|
|
<Directory /var/www/${DOMAIN}>
|
|
Options Indexes FollowSymLinks MultiViews
|
|
AllowOverride all
|
|
Order allow,deny
|
|
allow from all
|
|
</Directory>
|
|
|
|
SSLEngine On
|
|
|
|
## Full stance
|
|
SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
|
|
SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
|
|
SSLVerifyClient None
|
|
|
|
</VirtualHost>
|
|
|
|
</IfModule>
|
|
EOF
|
|
|
|
}
|
|
export -f apache_ssl_config
|
|
|
|
apache_ssl_add () {
|
|
local DOMAIN=$1
|
|
[ -e "$APACHE_CONFIG_LOCATION/$DOMAIN.conf" ] && return 0
|
|
mkdir -p "$APACHE_CONFIG_LOCATION"
|
|
apache_ssl_config $DOMAIN > $APACHE_CONFIG_LOCATION/$DOMAIN.conf
|
|
echo "Added $DOMAIN apache config." >&2
|
|
|
|
}
|
|
export -f apache_ssl_add
|
|
apache_ssl_proxy_add () {
|
|
local DOMAIN=$1 TARGET=$2
|
|
[ -e "$APACHE_CONFIG_LOCATION/$DOMAIN.conf" ] && return 0
|
|
|
|
mkdir -p "$APACHE_CONFIG_LOCATION"
|
|
apache_ssl_proxy_config $DOMAIN $TARGET > $APACHE_CONFIG_LOCATION/$DOMAIN.conf
|
|
echo "Added $DOMAIN as a proxy to $TARGET." >&2
|
|
}
|
|
export -f apache_ssl_proxy_add
|
|
|
|
gen_password() {
|
|
python -c 'import random; \
|
|
xx = "azertyuiopqsdfghjklmwxcvbn1234567890AZERTYUIOPQSDFGHJKLMWXCVBN+_-"; \
|
|
print "".join([xx[random.randint(0, len(xx)-1)] for x in range(0, 14)])'
|
|
}
|
|
export -f gen_password
|
|
|
|
|
|
file_put() {
|
|
local TARGET="$1"
|
|
mkdir -p "$(dirname "$TARGET")" &&
|
|
cat - > "$TARGET"
|
|
}
|
|
export -f file_put
|
|
|
|
|
|
apache_data_dir() {
|
|
local DOMAIN=$1 DATA_COMMA_SEPARATED=$2
|
|
|
|
export APACHE_DOCKER_IMAGE=$(service_base_docker_image apache)
|
|
|
|
DOCKER_SITE_PATH=/var/www/$DOMAIN
|
|
BASE=$DATASTORE/apache
|
|
DST=$BASE/$DOCKER_SITE_PATH
|
|
DATA=()
|
|
while IFS="," read -ra ADDR; do
|
|
for dir in "${ADDR[@]}"; do
|
|
mkdir -p "$DST/$dir"
|
|
DATA+=($dir)
|
|
done
|
|
done <<< "$DATA_COMMA_SEPARATED"
|
|
|
|
if [ -z "$APACHE_DOCKER_GID" ] &&
|
|
! grep "^export APACHE_DOCKER_GID=" /etc/compose.local.conf >/dev/null 2>&1; then
|
|
echo "Adding APACHE_DOCKER_GID to '/etc/compose.local.conf'."
|
|
|
|
export APACHE_DOCKER_GID=$(docker run "$APACHE_DOCKER_IMAGE" id -g www-data)
|
|
|
|
cat <<EOF >> /etc/compose.local.conf
|
|
export APACHE_DOCKER_GID=$APACHE_DOCKER_GID
|
|
EOF
|
|
|
|
fi
|
|
|
|
dirs=()
|
|
for d in "${DATA[@]}"; do
|
|
dirs+=("$DST/$d")
|
|
done
|
|
|
|
chgrp www-data "${dirs[@]}" -R && chmod 775 "${dirs[@]}" -R
|
|
}
|
|
export -f apache_data_dir
|
|
|
|
|
|
export _DOCKER_COMPOSE_DEF=""
|
|
get_compose_def() {
|
|
local local_compose
|
|
|
|
if [ "$_DOCKER_COMPOSE_DEF" ]; then
|
|
echo "$_DOCKER_COMPOSE_DEF"
|
|
return 0
|
|
fi
|
|
|
|
##
|
|
## Adding sub services configurations
|
|
##
|
|
|
|
additional_services=
|
|
if [ -z "$*" ]; then
|
|
info "No service provided, using \$DEFAULT_SERVICES variable. Target services: $DEFAULT_SERVICES"
|
|
additional_services=$DEFAULT_SERVICES
|
|
fi
|
|
declare -A loaded
|
|
for target_service in "$@" $additional_services; do
|
|
|
|
services=$(get_ordered_service_dependencies "$target_service") || return 1
|
|
for service in $services; do
|
|
|
|
if [ "${loaded[$service]}" ]; then
|
|
continue
|
|
fi
|
|
loaded[$service]=1
|
|
export _DOCKER_COMPOSE_DEF="$_DOCKER_COMPOSE_DEF
|
|
$service:
|
|
$(get_service_def "$service" | sed -r 's/^/ /g')"
|
|
|
|
done
|
|
done
|
|
echo "$_DOCKER_COMPOSE_DEF"
|
|
}
|
|
export -f get_compose_def
|
|
|
|
get_service_def() {
|
|
local service="$1"
|
|
|
|
if [ -z "$service" ]; then
|
|
echo "Please specify a service." >&2
|
|
return 1
|
|
fi
|
|
|
|
if [ -d "$CHARM_STORE/$service" ]; then
|
|
compose_file="$CHARM_STORE/$service/compose.yml"
|
|
local_compose=""
|
|
if [ -e "$compose_file" ]; then
|
|
debug "Found compose.yml in $service directory. Including in 'docker-compose.conf'."
|
|
local_compose="$(cat "$compose_file")"
|
|
fi
|
|
metadata_file="$CHARM_STORE/$service/metadata.yml"
|
|
if [ -e "$metadata_file" ]; then
|
|
debug "Found metadata.yml in $service directory. Including in 'docker-compose.conf'."
|
|
docker_compose_entry=$(get_docker_compose_entry_from_metadata "$service" < "$metadata_file") || return 1
|
|
local_compose="$local_compose
|
|
$docker_compose_entry"
|
|
fi
|
|
echo "$local_compose"
|
|
return 0
|
|
fi
|
|
|
|
err "service '$DARKYELLOW$service$NORMAL' not found." >&2
|
|
return 1
|
|
}
|
|
export -f get_service_def
|
|
|
|
service_base_docker_image() {
|
|
local service="$1"
|
|
service_def="$(get_service_def "$service")" || return 1
|
|
|
|
service_image=$(echo "$service_def" | shyaml get-value image 2>/dev/null)
|
|
if [ "$?" != 0 ]; then
|
|
service_build=$(echo "$service_def" | shyaml get-value build)
|
|
if [ "$?" != 0 ]; then
|
|
echo "Service '$service' has no 'image' nor 'build' parameter." >&2
|
|
return 1
|
|
fi
|
|
service_dockerfile="$COMPOSE_YML_PATH/$service_build/Dockerfile"
|
|
if ! [ -e "$service_dockerfile" ]; then
|
|
echo "No Dockerfile found in '$service_dockerfile' location." >&2
|
|
return 1
|
|
fi
|
|
|
|
grep '^FROM' "$service_dockerfile" | xargs echo | cut -f 2 -d " "
|
|
else
|
|
echo "$service_image"
|
|
fi
|
|
|
|
}
|
|
export -f service_base_docker_image
|
|
|
|
|
|
read-0() {
|
|
local eof
|
|
eof=
|
|
while [ "$1" ]; do
|
|
IFS='' read -r -d '' "$1" || eof=true
|
|
shift
|
|
done
|
|
test "$eof" != true
|
|
}
|
|
export -f read-0
|
|
|
|
array_values_to_stdin() {
|
|
local e
|
|
if [ "$#" -ne "1" ]; then
|
|
print_syntax_warning "$FUNCNAME: need one argument."
|
|
return 1
|
|
fi
|
|
var="$1"
|
|
eval "for e in \"\${$var[@]}\"; do echo -en \"\$e\\0\"; done"
|
|
}
|
|
|
|
array_keys_to_stdin() {
|
|
local e
|
|
if [ "$#" -ne "1" ]; then
|
|
print_syntax_warning "$FUNCNAME: need one argument."
|
|
return 1
|
|
fi
|
|
var="$1"
|
|
eval "for e in \"\${!$var[@]}\"; do echo -en \"\$e\\0\"; done"
|
|
}
|
|
|
|
array_kv_to_stdin() {
|
|
local e
|
|
if [ "$#" -ne "1" ]; then
|
|
print_syntax_warning "$FUNCNAME: need one argument."
|
|
return 1
|
|
fi
|
|
var="$1"
|
|
eval "for e in \"\${!$var[@]}\"; do echo -n \"\$e\"; echo -en '\0'; echo -n \"\${$var[\$e]}\"; echo -en '\0'; done"
|
|
}
|
|
|
|
|
|
array_pop() {
|
|
local narr="$1" nres="$2"
|
|
for key in $(eval "echo \${!$narr[@]}"); do
|
|
eval "$nres=\${$narr[\"\$key\"]}"
|
|
eval "unset $narr[\"\$key\"]"
|
|
return 0
|
|
done
|
|
}
|
|
export -f array_pop
|
|
|
|
array_member() {
|
|
local src elt
|
|
src="$1"
|
|
elt="$2"
|
|
while read-0 key; do
|
|
if [ "$(eval "echo -n \"\${$src[\$key]}\"")" == "$elt" ]; then
|
|
return 0
|
|
fi
|
|
done < <(array_keys_to_stdin "$src")
|
|
return 1
|
|
}
|
|
export -f array_member
|
|
|
|
|
|
get_service_deps() {
|
|
local service="$1"
|
|
service_def=$(get_service_def "$service") || return 1
|
|
echo "$service_def" | shyaml get-values links 2>/dev/null
|
|
return 0
|
|
}
|
|
export -f get_service_deps
|
|
|
|
## a service is not always a container.
|
|
## XXXvlab: a service name should not be a container name neither... see this later.
|
|
# get_container_name() {
|
|
# local service="$1"
|
|
# get_service_def "$service" | shyaml get-values links 2>/dev/null
|
|
# if [ "$(get_md_service_def "$service" | shyaml get-value subordinate 2>/dev/null)" != "true" ]; then
|
|
# echo "$service"
|
|
# return 0
|
|
# fi
|
|
|
|
# }
|
|
|
|
_rec_get_depth() {
|
|
local elt=$1
|
|
|
|
if [ "${depths[$elt]}" ]; then
|
|
return 0
|
|
fi
|
|
deps=$(get_service_deps "$elt") || return 1
|
|
if [ -z "$deps" ]; then
|
|
depths[$elt]=0
|
|
fi
|
|
|
|
max=0
|
|
for dep in $deps; do
|
|
_rec_get_depth "$dep" || return 1
|
|
if (( "${depths[$dep]}" > "$max" )); then
|
|
max="${depths[$dep]}"
|
|
fi
|
|
done
|
|
depths[$elt]=$((max + 1))
|
|
}
|
|
|
|
get_ordered_service_dependencies() {
|
|
local services=("$@")
|
|
|
|
if [ -z "${services[*]}" ]; then
|
|
print_syntax_error "$FUNCNAME: no arguments"
|
|
fi
|
|
|
|
declare -A depths
|
|
visited=()
|
|
heads=("${services[@]}")
|
|
while [ "${#heads[@]}" != 0 ]; do
|
|
array_pop heads head
|
|
visited+=("$head")
|
|
_rec_get_depth "$head" || return 1
|
|
done
|
|
|
|
i=0
|
|
while [ "${#depths[@]}" != 0 ]; do
|
|
for key in "${!depths[@]}"; do
|
|
value="${depths[$key]}"
|
|
if [ "$value" == "$i" ]; then
|
|
echo "$key"
|
|
unset depths[$key]
|
|
fi
|
|
done
|
|
i=$((i + 1))
|
|
done
|
|
}
|
|
|
|
run_service_hook () {
|
|
local service="$1" action="$2"
|
|
|
|
services=$(get_ordered_service_dependencies "$service") || return 1
|
|
## init in order
|
|
for service in $services; do
|
|
TARGET_SCRIPT="$COMPOSE_YML_PATH/service/$service/hooks/$2"
|
|
[ -e "$TARGET_SCRIPT" ] && {
|
|
[ "$verbose" ] && echo "Init $service"
|
|
SERVICE_NAME=$service \
|
|
DOCKER_BASE_IMAGE=$(service_base_docker_image "$service") \
|
|
SERVICE_DATASTORE="$DATASTORE/$service" \
|
|
SERVICE_CONFIGSTORE="$CONFIGSTORE/$service" \
|
|
"$TARGET_SCRIPT"
|
|
}
|
|
done
|
|
}
|
|
|
|
run_service_action () {
|
|
local service="$1" action="$2"
|
|
shift shift
|
|
run_service_hook "$service" init
|
|
|
|
services=$(get_ordered_service_dependencies "$service") || return 1
|
|
for service in $services; do
|
|
TARGET_SCRIPT="$COMPOSE_YML_PATH/service/$service/actions/$2"
|
|
if [ -e "$TARGET_SCRIPT" ]; then
|
|
[ "$verbose" ] && echo "Init $service"
|
|
SERVICE_NAME=$service \
|
|
CONTAINER_NAME=$(get_container_name "$service") \
|
|
DOCKER_BASE_IMAGE=$(service_base_docker_image "$service") \
|
|
SERVICE_DATASTORE="$DATASTORE/$service" \
|
|
SERVICE_CONFIGSTORE="$CONFIGSTORE/$service" \
|
|
echo "$TARGET_SCRIPT" "$@"
|
|
else
|
|
echo "Service '$service' does not have any action '$action' defined." >&2
|
|
return 1
|
|
fi
|
|
done
|
|
}
|
|
|
|
get_docker_compose_entry_from_metadata() {
|
|
local service="$1"
|
|
metadata="$(cat -)"
|
|
|
|
export DATASTORE CONFIGSTORE
|
|
## resources to volumes
|
|
volumes=$(
|
|
for resource_type in data config; do
|
|
while read-0 resource; do
|
|
eval "echo \" - \$${resource_type^^}STORE/\$service\$resource:\$resource:rw\""
|
|
done < <(echo "$metadata" | shyaml get-values-0 "${resource_type}-resources" 2>/dev/null)
|
|
done
|
|
while read-0 resource; do
|
|
if [[ "$resource" == *:* ]]; then
|
|
echo " - $resource:rw"
|
|
else
|
|
echo " - $resource:$resource:rw"
|
|
fi
|
|
done < <(echo "$metadata" | shyaml get-values-0 "host-resources" 2>/dev/null)
|
|
)
|
|
if [ "$volumes" ]; then
|
|
echo "volumes:"
|
|
echo "$volumes"
|
|
fi
|
|
|
|
## resources to volumes
|
|
image=$(echo "$metadata" | shyaml get-values "docker-image" 2>/dev/null)
|
|
if [ "$image" ]; then
|
|
echo "image: $image"
|
|
else
|
|
if ! [ -d "$CHARM_STORE/$service/build" ]; then
|
|
die "No 'docker-image' value set in 'metadata.yml' nor 'build/' directory found in charm $DARKYELLOW$service$NORMAL."
|
|
fi
|
|
echo "build: $service/build"
|
|
fi
|
|
}
|
|
export -f get_docker_compose_entry_from_metadata
|
|
|
|
launch_docker_compose() {
|
|
debug "Creating temporary docker-compose directory in '$tmpdir'."
|
|
tmpdir=$(mktemp -d -t tmp.XXXXXXXXXX)
|
|
function finish {
|
|
debug "Removing temporary docker-compose directory in '$tmpdir'."
|
|
rm -rf "$tmpdir"
|
|
}
|
|
trap finish EXIT
|
|
get_compose_def > "$tmpdir/docker-compose.yml" || return 1
|
|
## XXXvlab: could be more specific and only link the needed charms
|
|
ln -sf "$CHARM_STORE/"* "$tmpdir/"
|
|
cd "$tmpdir" && docker-compose "$@"
|
|
}
|
|
|
|
|
|
|
|
##
|
|
## Argument parsing
|
|
##
|
|
|
|
fullargs=()
|
|
opts=()
|
|
posargs=()
|
|
no_hooks=
|
|
no_init=
|
|
while [ "$#" != 0 ]; do
|
|
case "$1" in
|
|
# --help|-h)
|
|
# print_help
|
|
# exit 0
|
|
# ;;
|
|
--verbose|-v)
|
|
fullargs+=("$1")
|
|
export VERBOSE=true
|
|
;;
|
|
--no-hooks)
|
|
export no_hooks=true
|
|
;;
|
|
--no-init)
|
|
export no_init=true
|
|
;;
|
|
--debug)
|
|
export DEBUG=true
|
|
export VERBOSE=true
|
|
;;
|
|
--)
|
|
fullargs+=("$1")
|
|
shift
|
|
opts=("${opts[@]}" "$@")
|
|
break 2
|
|
;;
|
|
-*)
|
|
fullargs+=("$1")
|
|
opts=("${opts[@]}" "$1" "$2")
|
|
shift
|
|
;;
|
|
*)
|
|
fullargs+=("$1")
|
|
posargs=("${posargs[@]}" "$1")
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
|
|
##
|
|
## Actual code
|
|
##
|
|
|
|
export CHARM_STORE=${CHARM_STORE:-/srv/charm-store}
|
|
export DOCKER_DATASTORE=${DOCKER_DATASTORE:-/srv/docker-datastore}
|
|
|
|
|
|
## XXXvlab: should provide YML config opportunities in possible parent dirs ?
|
|
## userdir ? and global /etc/compose.yml ?
|
|
. /etc/compose.conf
|
|
. /etc/compose.local.conf
|
|
|
|
if ! [ -d "$CHARM_STORE" ]; then
|
|
err "Charm store path $YELLOW$CHARM_STORE$NORMAL does not exists. "
|
|
err "Please check your $YELLOW\$CHARM_STORE$NORMAL variable value."
|
|
exit 1
|
|
fi
|
|
|
|
|
|
if [ -z "$(cd "$CHARM_STORE"; ls)" ]; then
|
|
err "no available charms in charm store $YELLOW$CHARM_STORE$NORMAL. Either:"
|
|
err " - check $YELLOW\$CHARM_STORE$NORMAL variable value"
|
|
err " - download charms in $CHARM_STORE"
|
|
print_error "Charm store is empty. Cannot continue."
|
|
fi
|
|
|
|
|
|
action="${posargs[0]}"
|
|
case "$action" in
|
|
load|save)
|
|
service="${posargs[1]}"
|
|
run_service_action "$service" "$action" "${opts[@]}" "${posargs[@]:2}"
|
|
;;
|
|
up)
|
|
service="${posargs[1]}"
|
|
|
|
## init in order
|
|
[ "$no_init" ] || run_service_hook "$service" init
|
|
|
|
## XXXvlab: to be removed when all relation and service stuff is resolved
|
|
if [ -z "$no_hooks" ]; then
|
|
for script in "$CHARM_STORE/"*/hooks.d/*.sh; do
|
|
[ -e "$script" ] || continue
|
|
[ -x "$script" ] || { echo "compose: script $script is not executable." >&2; exit 1; }
|
|
(
|
|
cd "$(dirname "$script/..")";
|
|
"$script" "$@"
|
|
) || { echo "compose: hook $script failed. Stopping." >&2; exit 1; }
|
|
done
|
|
fi
|
|
|
|
launch_docker_compose "${fullargs[@]}"
|
|
;;
|
|
*) launch_docker_compose "${fullargs[@]}";;
|
|
esac
|