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 in the context of the last bootstrapped image"
14 echo " rebuild: Rebuild a container (destroy old, bootstrap, start new)"
15 echo " cleanup: Remove all containers that have stopped for > 24 hours"
16 echo " start-cmd: Generate docker command used to start container"
19 echo " --skip-prereqs Don't check launcher prerequisites"
20 echo " --docker-args Extra arguments to pass when running docker"
21 echo " --skip-mac-address Don't assign a mac address"
22 echo " --run-image Override the image used for running the container"
31 if [[ $command == "run" ]]; then
35 while [ ${#} -gt 0 ]; do
59 if [ -z "$command" -o -z "$config" -a "$command" != "cleanup" ]; then
64 # Docker doesn't like uppercase characters, spaces or special characters, catch it now before we build everything out and then find out
65 re
='[[:upper:]/ !@#$%^&*()+~`=]'
66 if [[ $config =~
$re ]];
69 echo "ERROR: Config name '$config' must not contain upper case characters, spaces or special characters. Correct config name and rerun $0."
76 docker_min_version
='17.03.1'
77 docker_rec_version
='17.06.2'
78 git_min_version
='1.8.0'
79 git_rec_version
='1.8.0'
81 config_file
=containers
/"$config".yml
82 cidbootstrap
=cids
/"$config"_bootstrap.cid
83 local_discourse
=local_discourse
84 image
="discourse/base:2.0.20190429-1058"
85 docker_path
=`which docker.io 2> /dev/null || which docker`
88 if [ "${SUPERVISED}" = "true" ]; then
89 restart_policy
="--restart=no"
91 attach_on_run
="-a stdout -a stderr"
96 if [ -n "$DOCKER_HOST" ]; then
97 docker_ip
=`sed -e 's/^tcp:\/\/\(.*\):.*$/\1/' <<< "$DOCKER_HOST"`
98 elif [ -x "$(which ip 2>/dev/null)" ]; then
99 docker_ip
=`ip addr show docker0 | \
101 awk '{ split($2,a,"/"); print a[1] }';`
103 docker_ip
=`ifconfig | \
104 grep -B1 "inet addr" | \
105 awk '{ if ( $1 == "inet" ) { print $2 } else if ( $2 == "Link" ) { printf "%s:" ,$1 } }' | \
107 awk -F: '{ print $3 }';`
110 # From https://stackoverflow.com/a/44660519/702738
112 if [[ $1 == $2 ]]; then
116 local i a
=(${1%%[^0-9.]*}) b
=(${2%%[^0-9.]*})
117 local arem
=${1#${1%%[^0-9.]*}} brem=${2#${2%%[^0-9.]*}}
118 for ((i
=0; i
<${#a[@]} || i
<${#b[@]}; i
++)); do
119 if ((10#${a[i]:-0} < 10#${b[i]:-0})); then
121 elif ((10#${a[i]:-0} > 10#${b[i]:-0})); then
125 if [ "$arem" '<' "$brem" ]; then
127 elif [ "$arem" '>' "$brem" ]; then
135 echo "Docker is not installed, you will need to install Docker in order to run Launcher"
136 echo "See https://docs.docker.com/installation/"
142 if [ -z $docker_path ]; then
146 # 1. docker daemon running?
147 # we send stderr to /dev/null cause we don't care about warnings,
148 # it usually complains about swap which does not matter
149 test=`$docker_path info 2> /dev/null`
150 if [[ $?
-ne 0 ]] ; then
151 echo "Cannot connect to the docker daemon - verify it is running and you have access"
155 # 2. running an approved storage driver?
156 if ! $docker_path info
2> /dev
/null |
egrep -q '^Storage Driver: (aufs|zfs|overlay2)$'; then
157 echo "Your Docker installation is not using a supported storage driver. If we were to proceed you may have a broken install."
158 echo "aufs is the recommended storage driver, although zfs/btrfs/overlay and overlay2 may work as well."
159 echo "Other storage drivers are known to be problematic."
160 echo "You can tell what filesystem you are using by running \"docker info\" and looking at the 'Storage Driver' line."
162 echo "If you wish to continue anyway using your existing unsupported storage driver,"
163 echo "read the source code of launcher and figure out how to bypass this check."
167 # 3. running recommended docker version
168 test=($
($docker_path --version)) # Get docker version string
169 test=${test[2]//,/} # Get version alone and strip comma if exists
171 # At least minimum docker version
172 if compare_version
"${docker_min_version}" "${test}"; then
173 echo "ERROR: Docker version ${test} not supported, please upgrade to at least ${docker_min_version}, or recommended ${docker_rec_version}"
177 # Recommend newer docker version
178 if compare_version
"${docker_rec_version}" "${test}"; then
179 echo "WARNING: Docker version ${test} deprecated, recommend upgrade to ${docker_rec_version} or newer."
182 # 4. discourse docker image is downloaded
183 test=`$docker_path images | awk '{print $1 ":" $2 }' | grep "$image"`
185 if [ -z "$test" ]; then
187 echo "WARNING: We are about to start downloading the Discourse base image"
188 echo "This process may take anywhere between a few minutes to an hour, depending on your network speed"
190 echo "Please be patient"
194 # 5. running recommended git version
195 test=($
($git_path --version)) # Get git version string
196 test=${test[2]//,/} # Get version alone and strip comma if exists
198 # At least minimum version
199 if compare_version
"${git_min_version}" "${test}"; then
200 echo "ERROR: Git version ${test} not supported, please upgrade to at least ${git_min_version}, or recommended ${git_rec_version}"
204 # Recommend best version
205 if compare_version
"${git_rec_version}" "${test}"; then
206 echo "WARNING: Git version ${test} deprecated, recommend upgrade to ${git_rec_version} or newer."
209 # 6. able to attach stderr / out / tty
210 test=`$docker_path run $user_args -i --rm -a stdout -a stderr $image echo working`
211 if [[ "$test" =~
"working" ]] ; then : ; else
212 echo "Your Docker installation is not working correctly"
214 echo "See: https://meta.discourse.org/t/docker-error-on-bootstrap/13657/18?u=sam"
218 # 7. enough space for the bootstrap on docker folder
219 folder
=`$docker_path info --format '{{.DockerRootDir}}'`
220 safe_folder
=${folder:-/var/lib/docker}
221 test=$
(($
(stat
-f --format="%a*%S" $safe_folder)/1024**3 < 5))
222 if [[ $test -ne 0 ]] ; then
223 echo "You have less than 5GB of free space on the disk where $safe_folder is located. You will need more space to continue"
226 read -p "Would you like to attempt to recover space by cleaning docker images and containers in the system?(y/N)" -n 1 -r
228 if [[ $REPLY =~ ^
[Yy
]$
]]
230 $docker_path system prune
-af
231 echo "If the cleanup was successful, you may try again now"
238 if [ -z "$SKIP_PREREQS" ] && [ "$command" != "cleanup" ]; then
243 read -r -d '' env_ruby
<< 'RUBY'
246 input
= STDIN.readlines.
join
247 yaml
= YAML.load
(input
)
249 if host_run
= yaml
['host_run']
250 params
= yaml
['params'] ||
{}
251 host_run.each
do |run|
253 run
= run.gsub
("$#{k}", v
)
255 STDOUT.
write "#{run}--SEP--"
260 host_run
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e "$env_ruby"`
262 while [ "$host_run" ] ; do
263 iter
=${host_run%%--SEP--*}
265 echo "Host run: $iter"
268 host_run
="${host_run#*--SEP--}"
274 volumes
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
275 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['volumes'].map{|v| '-v ' << v['volume']['host'] << ':' << v['volume']['guest'] << ' '}.join"`
279 links
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
280 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['links'].map{|l| '--link ' << l['link']['name'] << ':' << l['link']['alias'] << ' '}.join"`
284 local templates
=`cat $1 | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
285 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['templates']"`
287 local arrTemplates
=${templates// / }
289 if [ ! -z "$templates" ]; then
290 for template
in "${arrTemplates[@]}"
292 local nested_templates
=$
(find_templates
$template)
294 if [ ! -z "$nested_templates" ]; then
295 templates
="$templates $nested_templates"
305 set_template_info
() {
306 templates
=$
(find_templates
$config_file)
308 arrTemplates
=(${templates// / })
309 config_data
=$
(cat $config_file)
313 for template
in "${arrTemplates[@]}"
315 [ ! -z $template ] && {
316 input
="$input _FILE_SEPERATOR_ $(cat $template)"
320 # we always want our config file last so it takes priority
321 input
="$input _FILE_SEPERATOR_ $config_data"
323 read -r -d '' env_ruby
<< 'RUBY'
326 input
=STDIN.readlines.
join
327 # default to UTF-8 for the dbs sake
328 env
= {'LANG' => 'en_US.UTF-8'}
329 input.
split('_FILE_SEPERATOR_').each
do |yml|
332 env.merge
!(YAML.load
(yml
)['env'] ||
{})
333 rescue Psych
::SyntaxError
=> e
341 puts env.map
{|k
,v|
"-e\n#{k}=#{v}" }.
join("\n")
344 tmp_input_file
=$
(mktemp
)
346 echo "$input" > "$tmp_input_file"
347 raw
=`exec cat "$tmp_input_file" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$env_ruby"`
349 rm -f "$tmp_input_file"
354 if [ "$i" == "*ERROR." ]; then
356 elif [ -n "$i" ]; then
357 env
[${#env[@]}]="${i//\{\{config\}\}/${config}}"
361 if [ "$ok" -ne 1 ]; then
363 echo "YAML syntax error. Please check your containers
/*.yml config files.
"
368 read -r -d '' labels_ruby << 'RUBY'
371 input=STDIN.readlines.join
373 input.split('_FILE_SEPERATOR_').each do |yml|
376 labels.merge!(YAML.load(yml)['labels'] || {})
377 rescue Psych::SyntaxError => e
385 puts labels.map{|k,v| "-l\n#{k}=#{v}" }.join("\n")
388 tmp_input_file
=$
(mktemp
)
390 echo "$input" > "$tmp_input_file"
391 raw
=`exec cat "$tmp_input_file" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$labels_ruby"`
393 rm -f "$tmp_input_file"
398 if [ "$i" == "*ERROR." ]; then
400 elif [ -n "$i" ]; then
401 labels
[${#labels[@]}]=$
(echo $i |
sed s
/{{config
}}/${config}/g
)
405 if [ "$ok" -ne 1 ]; then
407 echo "YAML syntax error. Please check your containers/*.yml config files."
412 read -r -d '' ports_ruby
<< 'RUBY'
415 input
=STDIN.readlines.
join
417 input.
split('_FILE_SEPERATOR_').each
do |yml|
420 ports
+= (YAML.load
(yml
)['expose'] ||
[])
421 rescue Psych
::SyntaxError
=> e
429 puts ports.map
{ |p| p.to_s.include?
(':') ?
"-p\n#{p}" : "--expose\n#{p}" }.
join("\n")
432 tmp_input_file
=$
(mktemp
)
434 echo "$input" > "$tmp_input_file"
435 raw
=`exec cat "$tmp_input_file" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$ports_ruby"`
437 rm -f "$tmp_input_file"
442 if [ "$i" == "*ERROR." ]; then
444 elif [ -n "$i" ]; then
445 ports
[${#ports[@]}]=$i
449 if [ "$ok" -ne 1 ]; then
451 echo "YAML syntax error. Please check your containers/*.yml config files."
458 if [ -z $docker_path ]; then
462 [ "$command" == "cleanup" ] && {
463 $docker_path system prune
-a
465 if [ -d /var
/discourse
/shared
/standalone
/postgres_data_old
]; then
467 echo "Old PostgreSQL backup data cluster detected taking up $(du -hs /var/discourse/shared/standalone/postgres_data_old | awk '{print $1}') detected"
468 read -p "Would you like to remove it? (Y/n): " -n 1 -r && echo
470 if [[ $REPLY =~ ^
[Yy
]$
]]; then
471 echo "removing old PostgreSQL data cluster at /var/discourse/shared/standalone/postgres_data_old..."
472 rm -rf /var
/discourse
/shared
/standalone
/postgres_data_old
481 if [ ! "$command" == "setup" ]; then
482 if [[ ! -e $config_file ]]; then
483 echo "Config file was not found, ensure $config_file exists"
485 echo "Available configs ( `cd containers && ls -dm *.yml | tr -s '\n' ' ' | awk '{ gsub(/\.yml/, ""); print }'`)"
490 docker_version
=($
($docker_path --version))
491 docker_version
=${test[2]//,/}
492 restart_policy
=${restart_policy:---restart=always}
494 set_existing_container
(){
495 existing
=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
500 set_existing_container
502 if [ ! -z $existing ]
506 $docker_path stop
-t 10 $config
509 echo "$config was not started !"
515 run_image
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
516 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['run_image']"`
518 if [ -n "$user_run_image" ]; then
519 run_image
=$user_run_image
520 elif [ -z "$run_image" ]; then
521 run_image
="$local_discourse/$config"
526 boot_command
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
527 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['boot_command']"`
529 if [ -z "$boot_command" ]; then
531 no_boot_command
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
532 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['no_boot_command']"`
534 if [ -z "$no_boot_command" ]; then
535 boot_command
="/sbin/boot"
543 docker_args
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
544 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['docker_args']"`
546 if [[ -n "$docker_args" ]]; then
547 user_args
="$user_args $docker_args"
553 if [ -z "$START_CMD_ONLY" ]
555 existing
=`$docker_path ps | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
557 if [ ! -z $existing ]
559 echo "Nothing to do, your container has already started!"
563 existing
=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
564 if [ ! -z $existing ]
566 echo "starting up existing container"
569 $docker_path start
$config
583 # get hostname and settings from container configuration
584 for envar
in "${env[@]}"
586 if [[ $envar == DOCKER_USE_HOSTNAME
* ]] ||
[[ $envar == DISCOURSE_HOSTNAME
* ]]
588 # use as environment variable
594 hostname
=`hostname -s`
596 if [ "$DOCKER_USE_HOSTNAME" = "true" ]
598 hostname
=$DISCOURSE_HOSTNAME
600 hostname
=$hostname-$config
603 # we got to normalize so we only have allowed strings, this is more comprehensive but lets see how bash does first
604 # hostname=`$docker_path run $user_args --rm $image ruby -e 'print ARGV[0].gsub(/[^a-zA-Z-]/, "-")' $hostname`
605 # docker added more hostname rules
606 hostname
=${hostname//_/-}
609 if [ -z "$SKIP_MAC_ADDRESS" ] ; then
610 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/'")"
613 if [ ! -z "$START_CMD_ONLY" ] ; then
619 $docker_path run
--shm-size=512m
$links $attach_on_run $restart_policy "${env[@]}" "${labels[@]}" -h "$hostname" \
620 -e DOCKER_HOST_IP
="$docker_ip" --name $config -t "${ports[@]}" $volumes $mac_address $user_args \
621 $run_image $boot_command
635 (exec $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 $run_image \
636 /bin
/bash
-c "$run_command") || ERR
=$?
638 if [[ $ERR > 0 ]]; then
646 # Is the image available?
647 # If not, pull it here so the user is aware what's happening.
648 $docker_path history $image >/dev
/null
2>&1 ||
$docker_path pull
$image
652 base_image
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
653 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['base_image']"`
655 update_pups
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
656 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['update_pups']"`
658 if [[ ! X
"" = X
"$base_image" ]]; then
667 run_command
="cd /pups &&"
668 if [[ ! "false" = $update_pups ]]; then
669 run_command
="$run_command git pull &&"
671 run_command
="$run_command /pups/bin/pups --stdin"
677 tmp_input_file
=$
(mktemp
)
679 echo "$input" > "$tmp_input_file"
680 (exec cat "$tmp_input_file" |
$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 \
681 /bin
/bash
-c "$run_command") || ERR
=$?
683 rm -f "$tmp_input_file"
686 # magic exit code that indicates a retry
687 if [[ "$ERR" == 77 ]]; then
688 $docker_path rm `cat $cidbootstrap`
691 elif [[ "$ERR" > 0 ]]; then
695 if [[ $FAILED = "TRUE" ]]; then
696 if [[ ! -z "$DEBUG" ]]; then
697 $docker_path commit
`cat $cidbootstrap` $local_discourse/$config-debug ||
echo 'FAILED TO COMMIT'
698 echo "** DEBUG ** Maintaining image for diagnostics $local_discourse/$config-debug"
701 $docker_path rm `cat $cidbootstrap`
703 echo "** FAILED TO BOOTSTRAP ** please scroll up and look for earlier error messages, there may be more than one"
709 $docker_path commit
`cat $cidbootstrap` $local_discourse/$config ||
echo 'FAILED TO COMMIT'
710 $docker_path rm `cat $cidbootstrap` && rm $cidbootstrap
716 echo "Successfully bootstrapped, to startup use ./launcher start $config"
726 exec $docker_path exec -it $config /bin
/bash
--login
736 $docker_path logs
$config
758 if [ "$(git symbolic-ref --short HEAD)" == "master" ]; then
759 echo "Ensuring launcher is up to date"
763 LOCAL
=$
(git rev-parse HEAD
)
764 REMOTE
=$
(git rev-parse @
{u
})
765 BASE
=$
(git merge-base HEAD @
{u
})
767 if [ $LOCAL = $REMOTE ]; then
768 echo "Launcher is up-to-date"
770 elif [ $LOCAL = $BASE ]; then
771 echo "Updating Launcher"
772 git pull ||
(echo 'failed to update' && exit 1)
774 for (( i
=${#BASH_ARGV[@]}-1,j
=0; i
>=0,j
<${#BASH_ARGV[@]}; i--
,j
++ ))
776 args
[$j]=${BASH_ARGV[$i]}
778 exec /bin
/bash
$0 "${args[@]}" # $@ is empty, because of shift at the beginning. Use BASH_ARGV instead.
780 elif [ $REMOTE = $BASE ]; then
781 echo "Your version of Launcher is ahead of origin"
784 echo "Launcher has diverged source, this is only expected in Dev mode"
789 set_existing_container
791 if [ ! -z $existing ]
793 echo "Stopping old container"
796 $docker_path stop
-t 10 $config
802 if [ ! -z $existing ]
804 echo "Removing old container"
807 $docker_path rm $config
817 (set -x; $docker_path stop
-t 10 $config && $docker_path rm $config) ||
(echo "$config was not found" && exit 0)