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 " run: Run the given command with the config for the given template."
14 echo " rebuild: Rebuild a container (destroy old, bootstrap, start new)"
15 echo " cleanup: Remove all containers that have stopped for > 24 hours"
18 echo " --skip-prereqs Don't check launcher prerequisites"
19 echo " --docker-args Extra arguments to pass when running docker"
20 echo " --skip-mac-address Don't assign a mac address"
28 if [[ $command == "run" ]]; then
32 while [ ${#} -gt 0 ]; do
52 # Docker doesn't like uppercase characters, spaces or special characters, catch it now before we build everything out and then find out
53 re
='[[:upper:]/ !@#$%^&*()+~`=]'
54 if [[ $config =~
$re ]];
57 echo "ERROR: Config name '$config' must not contain upper case characters, spaces or special characters. Correct config name and rerun $0."
64 docker_min_version
='17.03.1'
65 docker_rec_version
='17.06.2'
66 git_min_version
='1.8.0'
67 git_rec_version
='1.8.0'
69 config_file
=containers
/"$config".yml
70 cidbootstrap
=cids
/"$config"_bootstrap.cid
71 local_discourse
=local_discourse
72 image
=discourse
/base
:2.0.20180802
73 docker_path
=`which docker.io || which docker`
76 if [ "${SUPERVISED}" = "true" ]; then
77 restart_policy
="--restart=no"
79 attach_on_run
="-a stdout -a stderr"
84 if [ -n "$DOCKER_HOST" ]; then
85 docker_ip
=`sed -e 's/^tcp:\/\/\(.*\):.*$/\1/' <<< "$DOCKER_HOST"`
86 elif [ -x "$(which ip 2>/dev/null)" ]; then
87 docker_ip
=`ip addr show docker0 | \
89 awk '{ split($2,a,"/"); print a[1] }';`
91 docker_ip
=`ifconfig | \
92 grep -B1 "inet addr" | \
93 awk '{ if ( $1 == "inet" ) { print $2 } else if ( $2 == "Link" ) { printf "%s:" ,$1 } }' | \
95 awk -F: '{ print $3 }';`
98 # From https://stackoverflow.com/a/44660519/702738
100 if [[ $1 == $2 ]]; then
104 local i a
=(${1%%[^0-9.]*}) b
=(${2%%[^0-9.]*})
105 local arem
=${1#${1%%[^0-9.]*}} brem=${2#${2%%[^0-9.]*}}
106 for ((i
=0; i
<${#a[@]} || i
<${#b[@]}; i
++)); do
107 if ((10#${a[i]:-0} < 10#${b[i]:-0})); then
109 elif ((10#${a[i]:-0} > 10#${b[i]:-0})); then
113 if [ "$arem" '<' "$brem" ]; then
115 elif [ "$arem" '>' "$brem" ]; then
123 echo "Docker is not installed, you will need to install Docker in order to run Launcher"
124 echo "See https://docs.docker.com/installation/"
130 if [ -z $docker_path ]; then
134 # 1. docker daemon running?
135 # we send stderr to /dev/null cause we don't care about warnings,
136 # it usually complains about swap which does not matter
137 test=`$docker_path info 2> /dev/null`
138 if [[ $?
-ne 0 ]] ; then
139 echo "Cannot connect to the docker daemon - verify it is running and you have access"
143 # 2. running an approved storage driver?
144 if ! $docker_path info
2> /dev
/null |
egrep -q '^Storage Driver: (aufs|btrfs|zfs|overlay|overlay2)$'; then
145 echo "Your Docker installation is not using a supported storage driver. If we were to proceed you may have a broken install."
146 echo "aufs is the recommended storage driver, although zfs/btrfs/overlay and overlay2 may work as well."
147 echo "Other storage drivers are known to be problematic."
148 echo "You can tell what filesystem you are using by running \"docker info\" and looking at the 'Storage Driver' line."
150 echo "If you wish to continue anyway using your existing unsupported storage driver,"
151 echo "read the source code of launcher and figure out how to bypass this check."
155 # 3. running recommended docker version
156 test=($
($docker_path --version)) # Get docker version string
157 test=${test[2]//,/} # Get version alone and strip comma if exists
159 # At least minimum docker version
160 if compare_version
"${docker_min_version}" "${test}"; then
161 echo "ERROR: Docker version ${test} not supported, please upgrade to at least ${docker_min_version}, or recommended ${docker_rec_version}"
165 # Recommend newer docker version
166 if compare_version
"${docker_rec_version}" "${test}"; then
167 echo "WARNING: Docker version ${test} deprecated, recommend upgrade to ${docker_rec_version} or newer."
170 # 4. discourse docker image is downloaded
171 test=`$docker_path images | awk '{print $1 ":" $2 }' | grep "$image"`
173 if [ -z "$test" ]; then
175 echo "WARNING: We are about to start downloading the Discourse base image"
176 echo "This process may take anywhere between a few minutes to an hour, depending on your network speed"
178 echo "Please be patient"
182 # 5. running recommended git version
183 test=($
($git_path --version)) # Get git version string
184 test=${test[2]//,/} # Get version alone and strip comma if exists
186 # At least minimum version
187 if compare_version
"${git_min_version}" "${test}"; then
188 echo "ERROR: Git version ${test} not supported, please upgrade to at least ${git_min_version}, or recommended ${git_rec_version}"
192 # Recommend best version
193 if compare_version
"${git_rec_version}" "${test}"; then
194 echo "WARNING: Git version ${test} deprecated, recommend upgrade to ${git_rec_version} or newer."
197 # 6. able to attach stderr / out / tty
198 test=`$docker_path run $user_args -i --rm -a stdout -a stderr $image echo working`
199 if [[ "$test" =~
"working" ]] ; then : ; else
200 echo "Your Docker installation is not working correctly"
202 echo "See: https://meta.discourse.org/t/docker-error-on-bootstrap/13657/18?u=sam"
206 # 7. enough space for the bootstrap on docker folder
207 folder
=`$docker_path info --format '{{.DockerRootDir}}'`
208 safe_folder
=${folder:-/var/lib/docker}
209 test=$
(($
(stat
-f --format="%a*%S" $safe_folder)/1024**3 < 5))
210 if [[ $test -ne 0 ]] ; then
211 echo "You have less than 5GB of free space on the disk where $safe_folder is located. You will need more space to continue"
214 read -p "Would you like to attempt to recover space by cleaning docker images and containers in the system?(y/N)" -n 1 -r
216 if [[ $REPLY =~ ^
[Yy
]$
]]
219 echo "If the cleanup was successful, you may try again now"
226 if [ -z "$SKIP_PREREQS" ] && [ "$command" != "cleanup" ]; then
231 read -r -d '' env_ruby
<< 'RUBY'
234 input
= STDIN.readlines.
join
235 yaml
= YAML.load
(input
)
237 if host_run
= yaml
['host_run']
238 params
= yaml
['params'] ||
{}
239 host_run.each
do |run|
241 run
= run.gsub
("$#{k}", v
)
243 STDOUT.
write "#{run}--SEP--"
248 host_run
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e "$env_ruby"`
250 while [ "$host_run" ] ; do
251 iter
=${host_run%%--SEP--*}
253 echo "Host run: $iter"
256 host_run
="${host_run#*--SEP--}"
262 volumes
=`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)['volumes'].map{|v| '-v ' << v['volume']['host'] << ':' << v['volume']['guest'] << ' '}.join"`
267 links
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
268 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['links'].map{|l| '--link ' << l['link']['name'] << ':' << l['link']['alias'] << ' '}.join"`
272 local templates
=`cat $1 | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
273 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['templates']"`
275 local arrTemplates
=${templates// / }
277 if [ ! -z "$templates" ]; then
278 for template
in "${arrTemplates[@]}"
280 local nested_templates
=$
(find_templates
$template)
282 if [ ! -z "$nested_templates" ]; then
283 templates
="$templates $nested_templates"
293 set_template_info
() {
294 templates
=$
(find_templates
$config_file)
296 arrTemplates
=(${templates// / })
297 config_data
=$
(cat $config_file)
301 for template
in "${arrTemplates[@]}"
303 [ ! -z $template ] && {
304 input
="$input _FILE_SEPERATOR_ $(cat $template)"
308 # we always want our config file last so it takes priority
309 input
="$input _FILE_SEPERATOR_ $config_data"
311 read -r -d '' env_ruby
<< 'RUBY'
314 input
=STDIN.readlines.
join
315 # default to UTF-8 for the dbs sake
316 env
= {'LANG' => 'en_US.UTF-8'}
317 input.
split('_FILE_SEPERATOR_').each
do |yml|
320 env.merge
!(YAML.load
(yml
)['env'] ||
{})
321 rescue Psych
::SyntaxError
=> e
329 puts env.map
{|k
,v|
"-e\n#{k}=#{v}" }.
join("\n")
332 raw
=`exec echo "$input" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$env_ruby"`
337 if [ "$i" == "*ERROR." ]; then
339 elif [ -n "$i" ]; then
344 if [ "$ok" -ne 1 ]; then
346 echo "YAML syntax error. Please check your containers/*.yml config files."
350 read -r -d '' labels_ruby
<< 'RUBY'
353 input
=STDIN.readlines.
join
354 # default to UTF-8 for the dbs sake
356 input.
split('_FILE_SEPERATOR_').each
do |yml|
359 labels.merge
!(YAML.load
(yml
)['labels'] ||
{})
360 rescue Psych
::SyntaxError
=> e
368 puts labels.map
{|k
,v|
"-l\n#{k}=#{v}" }.
join("\n")
371 raw
=`exec echo "$input" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$labels_ruby"`
376 if [ "$i" == "*ERROR." ]; then
378 elif [ -n "$i" ]; then
379 labels
[${#labels[@]}]=$
(echo $i |
sed s
/{{config
}}/${config}/g
)
383 if [ "$ok" -ne 1 ]; then
385 echo "YAML syntax error. Please check your containers/*.yml config files."
390 if [ -z $docker_path ]; then
394 [ "$command" == "cleanup" ] && {
396 echo "The following command will"
397 echo "- Delete all docker images for old containers"
398 echo "- Delete all stopped and orphan containers"
400 read -p "Are you sure (Y/n): " -n 1 -r && echo
401 if [[ $REPLY =~ ^
[Yy
]$ ||
! $REPLY ]]
403 space
=$
(df
/var
/lib
/docker |
awk '{ print $4 }' |
grep -v Available
)
404 echo "Starting Cleanup (bytes free $space)"
406 STATE_DIR
=.
/.gc-state
scripts
/docker-gc
408 rm -f shared
/standalone
/log
/var-log
/*.txt
410 space
=$
(df
/var
/lib
/docker |
awk '{ print $4 }' |
grep -v Available
)
411 echo "Finished Cleanup (bytes free $space)"
417 if [ -d /var
/discourse
/shared
/standalone
/postgres_data_old
]; then
419 echo "Old PostgreSQL backup data cluster detected taking up $(du -hs /var/discourse/shared/standalone/postgres_data_old | awk '{print $1}') detected"
420 read -p "Would you like to remove it? (Y/n): " -n 1 -r && echo
422 if [[ $REPLY =~ ^
[Yy
]$
]]; then
423 echo "removing old PostgreSQL data cluster at /var/discourse/shared/standalone/postgres_data_old..."
424 rm -rf /var
/discourse
/shared
/standalone
/postgres_data_old
433 if [ -z "$command" -a -z "$config" ]; then
437 if [ ! "$command" == "setup" ]; then
438 if [[ ! -e $config_file ]]; then
439 echo "Config file was not found, ensure $config_file exists"
441 echo "Available configs ( `cd containers && ls -dm *.yml | tr -s '\n' ' ' | awk '{ gsub(/\.yml/, ""); print }'`)"
446 docker_version
=($
($docker_path --version))
447 docker_version
=${test[2]//,/}
448 restart_policy
=${restart_policy:---restart=always}
450 set_existing_container
(){
451 existing
=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
456 set_existing_container
458 if [ ! -z $existing ]
462 $docker_path stop
-t 10 $config
465 echo "$config was not started !"
471 run_image
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
472 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['run_image']"`
474 if [ -z "$run_image" ]; then
475 run_image
="$local_discourse/$config"
480 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)['boot_command']"`
483 if [ -z "$boot_command" ]; then
485 no_boot_command
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
486 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['no_boot_command']"`
488 if [ -z "$no_boot_command" ]; then
489 boot_command
="/sbin/boot"
496 existing
=`$docker_path ps | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
498 if [ ! -z $existing ]
500 echo "Nothing to do, your container has already started!"
504 existing
=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
505 if [ ! -z $existing ]
507 echo "starting up existing container"
510 $docker_path start
$config
517 ports
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
518 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['expose'].map{|p| \"-p #{p}\"}.join(' ')"`
520 docker_args
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
521 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['docker_args']"`
529 # get hostname and settings from container configuration
530 for envar
in "${env[@]}"
532 if [[ $envar == DOCKER_USE_HOSTNAME
* ]] ||
[[ $envar == DISCOURSE_HOSTNAME
* ]]
534 # use as environment variable
540 hostname
=`hostname -s`
542 if [ "$DOCKER_USE_HOSTNAME" = "true" ]
544 hostname
=$DISCOURSE_HOSTNAME
546 hostname
=$hostname-$config
549 # we got to normalize so we only have allowed strings, this is more comprehensive but lets see how bash does first
550 # hostname=`$docker_path run $user_args --rm $image ruby -e 'print ARGV[0].gsub(/[^a-zA-Z-]/, "-")' $hostname`
551 # docker added more hostname rules
552 hostname
=${hostname//_/-}
555 if [ -z "$SKIP_MAC_ADDRESS" ] ; then
556 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/'")"
560 $docker_path run
--shm-size=512m
$links $attach_on_run $restart_policy "${env[@]}" "${labels[@]}" -h "$hostname" \
561 -e DOCKER_HOST_IP
="$docker_ip" --name $config -t $ports $volumes $mac_address $docker_args $user_args \
562 $run_image $boot_command
572 # Is the image available?
573 # If not, pull it here so the user is aware what's happening.
574 $docker_path history $image >/dev
/null
2>&1 ||
$docker_path pull
$image
578 base_image
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
579 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['base_image']"`
581 update_pups
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
582 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['update_pups']"`
584 if [[ ! X
"" = X
"$base_image" ]]; then
595 $docker_path run
--rm --shm-size=512m
$user_args $links "${env[@]}" -e DOCKER_HOST_IP
="$docker_ip" -i -a stdin
-a stdout
-a stderr
$volumes $image \
596 /bin
/bash
-c "$run_command"
603 run_command
="cd /pups &&"
604 if [[ ! "false" = $update_pups ]]; then
605 run_command
="$run_command git pull &&"
607 run_command
="$run_command /pups/bin/pups --stdin"
612 (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 \
613 /bin
/bash
-c "$run_command") || ERR
=$?
616 # magic exit code that indicates a retry
617 if [[ "$ERR" == 77 ]]; then
618 $docker_path rm `cat $cidbootstrap`
621 elif [[ "$ERR" > 0 ]]; then
625 if [[ $FAILED = "TRUE" ]]; then
626 if [[ ! -z "$DEBUG" ]]; then
627 $docker_path commit
`cat $cidbootstrap` $local_discourse/$config-debug ||
echo 'FAILED TO COMMIT'
628 echo "** DEBUG ** Maintaining image for diagnostics $local_discourse/$config-debug"
631 $docker_path rm `cat $cidbootstrap`
633 echo "** FAILED TO BOOTSTRAP ** please scroll up and look for earlier error messages, there may be more than one"
639 $docker_path commit
`cat $cidbootstrap` $local_discourse/$config ||
echo 'FAILED TO COMMIT'
640 $docker_path rm `cat $cidbootstrap` && rm $cidbootstrap
648 echo "Successfully bootstrapped, to startup use ./launcher start $config"
658 exec $docker_path exec -it $config /bin
/bash
--login
668 $docker_path logs
$config
684 if [ "$(git symbolic-ref --short HEAD)" == "master" ]; then
685 echo "Ensuring launcher is up to date"
689 LOCAL
=$
(git rev-parse @
)
690 REMOTE
=$
(git rev-parse @
{u
})
691 BASE
=$
(git merge-base @ @
{u
})
693 if [ $LOCAL = $REMOTE ]; then
694 echo "Launcher is up-to-date"
696 elif [ $LOCAL = $BASE ]; then
697 echo "Updating Launcher"
698 git pull ||
(echo 'failed to update' && exit 1)
700 for (( i
=${#BASH_ARGV[@]}-1,j
=0; i
>=0,j
<${#BASH_ARGV[@]}; i--
,j
++ ))
702 args
[$j]=${BASH_ARGV[$i]}
704 exec /bin
/bash
$0 "${args[@]}" # $@ is empty, because of shift at the beginning. Use BASH_ARGV instead.
706 elif [ $REMOTE = $BASE ]; then
707 echo "Your version of Launcher is ahead of origin"
710 echo "Launcher has diverged source, this is only expected in Dev mode"
715 set_existing_container
717 if [ ! -z $existing ]
719 echo "Stopping old container"
722 $docker_path stop
-t 10 $config
728 if [ ! -z $existing ]
730 echo "Removing old container"
733 $docker_path rm $config
743 (set -x; $docker_path stop
-t 10 $config && $docker_path rm $config) ||
(echo "$config was not found" && exit 0)