4 echo "Usage: launcher COMMAND CONFIG [--skip-prereqs] [--docker-args STRING]"
6 echo " start: Start/initialize a container"
7 echo " stop: Stop a running container"
8 echo " restart: Restart a container"
9 echo " destroy: Stop and remove a container"
10 echo " enter: Open a shell to run commands inside the container"
11 echo " logs: View the Docker logs for a container"
12 echo " bootstrap: Bootstrap a container for the config based on a template"
13 echo " rebuild: Rebuild a container (destroy old, bootstrap, start new)"
14 echo " cleanup: Remove all containers that have stopped for > 24 hours"
17 echo " --skip-prereqs Don't check launcher prerequisites"
18 echo " --docker-args Extra arguments to pass when running docker"
19 echo " --skip-mac-address Don't assign a mac address"
27 while [ ${#} -gt 0 ]; do
47 # Docker doesn't like uppercase characters, spaces or special characters, catch it now before we build everything out and then find out
48 re
='[[:upper:]/ !@#$%^&*()+~`=]'
49 if [[ $config =~
$re ]];
52 echo "ERROR: Config name '$config' must not contain upper case characters, spaces or special characters. Correct config name and rerun $0."
59 docker_min_version
='17.03.1'
60 docker_rec_version
='17.06.2'
61 git_min_version
='1.8.0'
62 git_rec_version
='1.8.0'
64 config_file
=containers
/"$config".yml
65 cidbootstrap
=cids
/"$config"_bootstrap.cid
66 local_discourse
=local_discourse
67 image
=discourse
/base
:2.0.20180717
68 docker_path
=`which docker.io || which docker`
71 if [ "${SUPERVISED}" = "true" ]; then
72 restart_policy
="--restart=no"
74 attach_on_run
="-a stdout -a stderr"
79 if [ -n "$DOCKER_HOST" ]; then
80 docker_ip
=`sed -e 's/^tcp:\/\/\(.*\):.*$/\1/' <<< "$DOCKER_HOST"`
81 elif [ -x "$(which ip 2>/dev/null)" ]; then
82 docker_ip
=`ip addr show docker0 | \
84 awk '{ split($2,a,"/"); print a[1] }';`
86 docker_ip
=`ifconfig | \
87 grep -B1 "inet addr" | \
88 awk '{ if ( $1 == "inet" ) { print $2 } else if ( $2 == "Link" ) { printf "%s:" ,$1 } }' | \
90 awk -F: '{ print $3 }';`
93 # From https://stackoverflow.com/a/44660519/702738
95 if [[ $1 == $2 ]]; then
99 local i a
=(${1%%[^0-9.]*}) b
=(${2%%[^0-9.]*})
100 local arem
=${1#${1%%[^0-9.]*}} brem=${2#${2%%[^0-9.]*}}
101 for ((i
=0; i
<${#a[@]} || i
<${#b[@]}; i
++)); do
102 if ((10#${a[i]:-0} < 10#${b[i]:-0})); then
104 elif ((10#${a[i]:-0} > 10#${b[i]:-0})); then
108 if [ "$arem" '<' "$brem" ]; then
110 elif [ "$arem" '>' "$brem" ]; then
118 echo "Docker is not installed, you will need to install Docker in order to run Launcher"
119 echo "See https://docs.docker.com/installation/"
125 if [ -z $docker_path ]; then
129 # 1. docker daemon running?
130 # we send stderr to /dev/null cause we don't care about warnings,
131 # it usually complains about swap which does not matter
132 test=`$docker_path info 2> /dev/null`
133 if [[ $?
-ne 0 ]] ; then
134 echo "Cannot connect to the docker daemon - verify it is running and you have access"
138 # 2. running an approved storage driver?
139 if ! $docker_path info
2> /dev
/null |
egrep -q '^Storage Driver: (aufs|btrfs|zfs|overlay|overlay2)$'; then
140 echo "Your Docker installation is not using a supported storage driver. If we were to proceed you may have a broken install."
141 echo "aufs is the recommended storage driver, although zfs/btrfs/overlay and overlay2 may work as well."
142 echo "Other storage drivers are known to be problematic."
143 echo "You can tell what filesystem you are using by running \"docker info\" and looking at the 'Storage Driver' line."
145 echo "If you wish to continue anyway using your existing unsupported storage driver,"
146 echo "read the source code of launcher and figure out how to bypass this check."
150 # 3. running recommended docker version
151 test=($
($docker_path --version)) # Get docker version string
152 test=${test[2]//,/} # Get version alone and strip comma if exists
154 # At least minimum docker version
155 if compare_version
"${docker_min_version}" "${test}"; then
156 echo "ERROR: Docker version ${test} not supported, please upgrade to at least ${docker_min_version}, or recommended ${docker_rec_version}"
160 # Recommend newer docker version
161 if compare_version
"${docker_rec_version}" "${test}"; then
162 echo "WARNING: Docker version ${test} deprecated, recommend upgrade to ${docker_rec_version} or newer."
165 # 4. discourse docker image is downloaded
166 test=`$docker_path images | awk '{print $1 ":" $2 }' | grep "$image"`
168 if [ -z "$test" ]; then
170 echo "WARNING: We are about to start downloading the Discourse base image"
171 echo "This process may take anywhere between a few minutes to an hour, depending on your network speed"
173 echo "Please be patient"
177 # 5. running recommended git version
178 test=($
($git_path --version)) # Get git version string
179 test=${test[2]//,/} # Get version alone and strip comma if exists
181 # At least minimum version
182 if compare_version
"${git_min_version}" "${test}"; then
183 echo "ERROR: Git version ${test} not supported, please upgrade to at least ${git_min_version}, or recommended ${git_rec_version}"
187 # Recommend best version
188 if compare_version
"${git_rec_version}" "${test}"; then
189 echo "WARNING: Git version ${test} deprecated, recommend upgrade to ${git_rec_version} or newer."
192 # 6. able to attach stderr / out / tty
193 test=`$docker_path run $user_args -i --rm -a stdout -a stderr $image echo working`
194 if [[ "$test" =~
"working" ]] ; then : ; else
195 echo "Your Docker installation is not working correctly"
197 echo "See: https://meta.discourse.org/t/docker-error-on-bootstrap/13657/18?u=sam"
201 # 7. enough space for the bootstrap on docker folder
202 folder
=`$docker_path info --format '{{.DockerRootDir}}'`
203 safe_folder
=${folder:-/var/lib/docker}
204 test=$
(($
(stat
-f --format="%a*%S" $safe_folder)/1024**3 < 5))
205 if [[ $test -ne 0 ]] ; then
206 echo "You have less than 5GB of free space on the disk where $safe_folder is located. You will need more space to continue"
209 read -p "Would you like to attempt to recover space by cleaning docker images and containers in the system?(y/N)" -n 1 -r
211 if [[ $REPLY =~ ^
[Yy
]$
]]
214 echo "If the cleanup was successful, you may try again now"
221 if [ -z "$SKIP_PREREQS" ] && [ "$command" != "cleanup" ]; then
226 read -r -d '' env_ruby
<< 'RUBY'
229 input
= STDIN.readlines.
join
230 yaml
= YAML.load
(input
)
232 if host_run
= yaml
['host_run']
233 params
= yaml
['params'] ||
{}
234 host_run.each
do |run|
236 run
= run.gsub
("$#{k}", v
)
238 STDOUT.
write "#{run}--SEP--"
243 host_run
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e "$env_ruby"`
245 while [ "$host_run" ] ; do
246 iter
=${host_run%%--SEP--*}
248 echo "Host run: $iter"
251 host_run
="${host_run#*--SEP--}"
257 volumes
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
258 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['volumes'].map{|v| '-v ' << v['volume']['host'] << ':' << v['volume']['guest'] << ' '}.join"`
262 links
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
263 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['links'].map{|l| '--link ' << l['link']['name'] << ':' << l['link']['alias'] << ' '}.join"`
267 local templates
=`cat $1 | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
268 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['templates']"`
270 local arrTemplates
=${templates// / }
272 if [ ! -z "$templates" ]; then
273 for template
in "${arrTemplates[@]}"
275 local nested_templates
=$
(find_templates
$template)
277 if [ ! -z "$nested_templates" ]; then
278 templates
="$templates $nested_templates"
288 set_template_info
() {
289 templates
=$
(find_templates
$config_file)
291 arrTemplates
=(${templates// / })
292 config_data
=$
(cat $config_file)
296 for template
in "${arrTemplates[@]}"
298 [ ! -z $template ] && {
299 input
="$input _FILE_SEPERATOR_ $(cat $template)"
303 # we always want our config file last so it takes priority
304 input
="$input _FILE_SEPERATOR_ $config_data"
306 read -r -d '' env_ruby
<< 'RUBY'
309 input
=STDIN.readlines.
join
310 # default to UTF-8 for the dbs sake
311 env
= {'LANG' => 'en_US.UTF-8'}
312 input.
split('_FILE_SEPERATOR_').each
do |yml|
315 env.merge
!(YAML.load
(yml
)['env'] ||
{})
316 rescue Psych
::SyntaxError
=> e
324 puts env.map
{|k
,v|
"-e\n#{k}=#{v}" }.
join("\n")
327 raw
=`exec echo "$input" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$env_ruby"`
332 if [ "$i" == "*ERROR." ]; then
334 elif [ -n "$i" ]; then
339 if [ "$ok" -ne 1 ]; then
341 echo "YAML syntax error. Please check your containers/*.yml config files."
345 read -r -d '' labels_ruby
<< 'RUBY'
348 input
=STDIN.readlines.
join
349 # default to UTF-8 for the dbs sake
351 input.
split('_FILE_SEPERATOR_').each
do |yml|
354 labels.merge
!(YAML.load
(yml
)['labels'] ||
{})
355 rescue Psych
::SyntaxError
=> e
363 puts labels.map
{|k
,v|
"-l\n#{k}=#{v}" }.
join("\n")
366 raw
=`exec echo "$input" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$labels_ruby"`
371 if [ "$i" == "*ERROR." ]; then
373 elif [ -n "$i" ]; then
374 labels
[${#labels[@]}]=$
(echo $i |
sed s
/{{config
}}/${config}/g
)
378 if [ "$ok" -ne 1 ]; then
380 echo "YAML syntax error. Please check your containers/*.yml config files."
385 if [ -z $docker_path ]; then
389 [ "$command" == "cleanup" ] && {
391 echo "The following command will"
392 echo "- Delete all docker images for old containers"
393 echo "- Delete all stopped and orphan containers"
395 read -p "Are you sure (Y/n): " -n 1 -r && echo
396 if [[ $REPLY =~ ^
[Yy
]$ ||
! $REPLY ]]
398 space
=$
(df
/var
/lib
/docker |
awk '{ print $4 }' |
grep -v Available
)
399 echo "Starting Cleanup (bytes free $space)"
401 STATE_DIR
=.
/.gc-state
scripts
/docker-gc
403 rm -f shared
/standalone
/log
/var-log
/*.txt
405 space
=$
(df
/var
/lib
/docker |
awk '{ print $4 }' |
grep -v Available
)
406 echo "Finished Cleanup (bytes free $space)"
412 if [ -d /var
/discourse
/shared
/standalone
/postgres_data_old
]; then
414 echo "Old PostgreSQL backup data cluster detected taking up $(du -hs /var/discourse/shared/standalone/postgres_data_old | awk '{print $1}') detected"
415 read -p "Would you like to remove it? (Y/n): " -n 1 -r && echo
417 if [[ $REPLY =~ ^
[Yy
]$
]]; then
418 echo "removing old PostgreSQL data cluster at /var/discourse/shared/standalone/postgres_data_old..."
419 rm -rf /var
/discourse
/shared
/standalone
/postgres_data_old
428 if [ -z "$command" -a -z "$config" ]; then
432 if [ ! "$command" == "setup" ]; then
433 if [[ ! -e $config_file ]]; then
434 echo "Config file was not found, ensure $config_file exists"
436 echo "Available configs ( `cd containers && ls -dm *.yml | tr -s '\n' ' ' | awk '{ gsub(/\.yml/, ""); print }'`)"
441 docker_version
=($
($docker_path --version))
442 docker_version
=${test[2]//,/}
443 restart_policy
=${restart_policy:---restart=always}
445 set_existing_container
(){
446 existing
=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
451 set_existing_container
453 if [ ! -z $existing ]
457 $docker_path stop
-t 10 $config
460 echo "$config was not started !"
466 run_image
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
467 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['run_image']"`
469 if [ -z "$run_image" ]; then
470 run_image
="$local_discourse/$config"
475 boot_command
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
476 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['boot_command']"`
478 if [ -z "$boot_command" ]; then
480 no_boot_command
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
481 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['no_boot_command']"`
483 if [ -z "$no_boot_command" ]; then
484 boot_command
="/sbin/boot"
491 existing
=`$docker_path ps | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
493 if [ ! -z $existing ]
495 echo "Nothing to do, your container has already started!"
499 existing
=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
500 if [ ! -z $existing ]
502 echo "starting up existing container"
505 $docker_path start
$config
512 ports
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
513 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['expose'].map{|p| \"-p #{p}\"}.join(' ')"`
515 docker_args
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
516 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['docker_args']"`
524 # get hostname and settings from container configuration
525 for envar
in "${env[@]}"
527 if [[ $envar == DOCKER_USE_HOSTNAME
* ]] ||
[[ $envar == DISCOURSE_HOSTNAME
* ]]
529 # use as environment variable
535 hostname
=`hostname -s`
537 if [ "$DOCKER_USE_HOSTNAME" = "true" ]
539 hostname
=$DISCOURSE_HOSTNAME
541 hostname
=$hostname-$config
544 # we got to normalize so we only have allowed strings, this is more comprehensive but lets see how bash does first
545 # hostname=`$docker_path run $user_args --rm $image ruby -e 'print ARGV[0].gsub(/[^a-zA-Z-]/, "-")' $hostname`
546 # docker added more hostname rules
547 hostname
=${hostname//_/-}
550 if [ -z "$SKIP_MAC_ADDRESS" ] ; then
551 mac_address
="--mac-address $($docker_path run $user_args -i --rm -a stdout -a stderr $image /bin/sh -c "echo $hostname |
md5sum |
sed 's/^\(..\)\(..\)\(..\)\(..\)\(..\).*$/02:\1:\2:\3:\4:\5/'")"
555 $docker_path run
--shm-size=512m
$links $attach_on_run $restart_policy "${env[@]}" "${labels[@]}" -h "$hostname" \
556 -e DOCKER_HOST_IP
="$docker_ip" --name $config -t $ports $volumes $mac_address $docker_args $user_args \
557 $run_image $boot_command
567 # I got no frigging clue what this does, ask Sam Saffron. It RUNS STUFF ON THE HOST I GUESS?
570 # Is the image available?
571 # If not, pull it here so the user is aware what's happening.
572 $docker_path history $image >/dev
/null
2>&1 ||
$docker_path pull
$image
576 base_image
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
577 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['base_image']"`
579 update_pups
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
580 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['update_pups']"`
582 if [[ ! X
"" = X
"$base_image" ]]; then
591 run_command
="cd /pups &&"
592 if [[ ! "false" = $update_pups ]]; then
593 run_command
="$run_command git pull &&"
595 run_command
="$run_command /pups/bin/pups --stdin"
600 (exec echo "$input" |
$docker_path run
--shm-size=512m
$user_args $links "${env[@]}" -e DOCKER_HOST_IP
="$docker_ip" --cidfile $cidbootstrap -i -a stdin
-a stdout
-a stderr
$volumes $image \
601 /bin
/bash
-c "$run_command") || ERR
=$?
604 # magic exit code that indicates a retry
605 if [[ "$ERR" == 77 ]]; then
606 $docker_path rm `cat $cidbootstrap`
609 elif [[ "$ERR" > 0 ]]; then
613 if [[ $FAILED = "TRUE" ]]; then
614 if [[ ! -z "$DEBUG" ]]; then
615 $docker_path commit
`cat $cidbootstrap` $local_discourse/$config-debug ||
echo 'FAILED TO COMMIT'
616 echo "** DEBUG ** Maintaining image for diagnostics $local_discourse/$config-debug"
619 $docker_path rm `cat $cidbootstrap`
621 echo "** FAILED TO BOOTSTRAP ** please scroll up and look for earlier error messages, there may be more than one"
627 $docker_path commit
`cat $cidbootstrap` $local_discourse/$config ||
echo 'FAILED TO COMMIT'
628 $docker_path rm `cat $cidbootstrap` && rm $cidbootstrap
636 echo "Successfully bootstrapped, to startup use ./launcher start $config"
641 exec $docker_path exec -it $config /bin
/bash
--login
651 $docker_path logs
$config
667 if [ "$(git symbolic-ref --short HEAD)" == "master" ]; then
668 echo "Ensuring launcher is up to date"
672 LOCAL
=$
(git rev-parse @
)
673 REMOTE
=$
(git rev-parse @
{u
})
674 BASE
=$
(git merge-base @ @
{u
})
676 if [ $LOCAL = $REMOTE ]; then
677 echo "Launcher is up-to-date"
679 elif [ $LOCAL = $BASE ]; then
680 echo "Updating Launcher"
681 git pull ||
(echo 'failed to update' && exit 1)
683 for (( i
=${#BASH_ARGV[@]}-1,j
=0; i
>=0,j
<${#BASH_ARGV[@]}; i--
,j
++ ))
685 args
[$j]=${BASH_ARGV[$i]}
687 exec /bin
/bash
$0 "${args[@]}" # $@ is empty, because of shift at the beginning. Use BASH_ARGV instead.
689 elif [ $REMOTE = $BASE ]; then
690 echo "Your version of Launcher is ahead of origin"
693 echo "Launcher has diverged source, this is only expected in Dev mode"
698 set_existing_container
700 if [ ! -z $existing ]
702 echo "Stopping old container"
705 $docker_path stop
-t 10 $config
711 if [ ! -z $existing ]
713 echo "Removing old container"
716 $docker_path rm $config
726 (set -x; $docker_path stop
-t 10 $config && $docker_path rm $config) ||
(echo "$config was not found" && exit 0)